Skip to content

Commit

Permalink
FEATURE: Add group and category restrictions to house ads (#205)
Browse files Browse the repository at this point in the history
# Description

This PR adds the ability to apply **group** and **category** restrictions to a **house ad**.

# What is included
- In order to get the group and category selectors to work within `admin/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js` I needed to modernize the file. 
- I dropped the `bufferedProperty` implementation in favor of a vanilla ember approach
- I added `category_ids` and `group_ids` to our house ads model
- I added tests for group / category restrictions
- I added a preview button to display the house ad
- `/site.json` would return a object called `house_creatives` and a list of key value pairs that matched the ad name with the html, like so:
```js
{ AD_KEY: ad.html }
```
I need access to the category ids on the client to conditionally render the house ads so the new format will be: 
```js
{ AD_KEY: { html: ad.html, category_ids: ad.category_ids } }
```

# Screenshots
<img width="658" alt="Screenshot 2024-04-08 at 2 39 22 PM" src="https://github.com/discourse/discourse-adplugin/assets/50783505/b44b386d-65a1-4a2a-a487-d735b13357dd">

# Preview Video

https://github.com/discourse/discourse-adplugin/assets/50783505/6d0d8253-afef-4e15-b6fc-c6f696efd169
  • Loading branch information
janzenisaac committed Apr 9, 2024
1 parent c4227de commit 554f03f
Show file tree
Hide file tree
Showing 17 changed files with 600 additions and 149 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import CategorySelector from "select-kit/components/category-selector";

export default class HouseAdsCategorySelector extends CategorySelector {
get value() {
return this.selectedCategories.map((c) => c.id);
}
}
18 changes: 18 additions & 0 deletions admin/assets/javascripts/discourse/components/modal/preview.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { htmlSafe } from "@ember/template";
import DModal from "discourse/components/d-modal";
import i18n from "discourse-common/helpers/i18n";

const Preview = <template>
<DModal
@closeModal={{@closeModal}}
@title={{i18n "admin.adplugin.house_ads.preview"}}
>
<:body>
<div class="house-ad-preview">
{{htmlSafe @model.html}}
</div>
</:body>
</DModal>
</template>;

export default Preview;
Original file line number Diff line number Diff line change
@@ -1,115 +1,156 @@
import { tracked } from "@glimmer/tracking";
import Controller, { inject as controller } from "@ember/controller";
import { not, or } from "@ember/object/computed";
import { inject as service } from "@ember/service";
import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { observes } from "@ember-decorators/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyNotEqual } from "discourse/lib/computed";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import Category from "discourse/models/category";
import I18n from "I18n";
import Preview from "../components/modal/preview";

export default Controller.extend(bufferedProperty("model"), {
adminPluginsHouseAds: controller("adminPlugins.houseAds"),
router: service(),
export default class adminPluginsHouseAdsShow extends Controller {
@service router;
@service modal;

saving: false,
savingStatus: "",
@controller("adminPlugins.houseAds") houseAdsController;

nameDirty: propertyNotEqual("buffered.name", "model.name"),
htmlDirty: propertyNotEqual("buffered.html", "model.html"),
visibleToAnonsDirty: propertyNotEqual(
"buffered.visible_to_anons",
"model.visible_to_anons"
),
visibleToLoggedInDirty: propertyNotEqual(
"buffered.visible_to_logged_in_users",
"model.visible_to_logged_in_users"
),
dirty: or(
"nameDirty",
"htmlDirty",
"visibleToLoggedInDirty",
"visibleToAnonsDirty"
),
disableSave: not("dirty"),
@tracked selectedCategories = [];
@tracked selectedGroups = [];
@tracked saving = false;
@tracked savingStatus = "";
@tracked buffered;
actions: {
save() {
if (!this.get("saving")) {
this.setProperties({
saving: true,
savingStatus: I18n.t("saving"),
});
@observes("model")
modelChanged() {
this.buffered = new TrackedObject({ ...this.model });
this.selectedCategories = this.model.categories || [];
this.selectedGroups = this.model.group_ids || [];
}
const data = {},
buffered = this.get("buffered"),
newRecord = !buffered.get("id");

if (!newRecord) {
data.id = buffered.get("id");
}
data.name = buffered.get("name");
data.html = buffered.get("html");
data.visible_to_logged_in_users = buffered.get(
"visible_to_logged_in_users"
);
data.visible_to_anons = buffered.get("visible_to_anons");
get disabledSave() {
for (const key in this.buffered) {
// we don't want to compare the categories array
if (key !== "categories" && this.buffered[key] !== this.model[key]) {
return false;
}
}
return true;
}
ajax(
@action
async save() {
if (!this.saving) {
this.saving = true;
this.savingStatus = I18n.t("saving");
const data = {};
const newRecord = !this.buffered.id;
if (!newRecord) {
data.id = this.buffered.id;
}
data.name = this.buffered.name;
data.html = this.buffered.html;
data.visible_to_logged_in_users =
this.buffered.visible_to_logged_in_users;
data.visible_to_anons = this.buffered.visible_to_anons;
data.category_ids = this.buffered.category_ids;
data.group_ids = this.buffered.group_ids;
try {
const ajaxData = await ajax(
newRecord
? `/admin/plugins/pluginad/house_creatives`
: `/admin/plugins/pluginad/house_creatives/${buffered.get("id")}`,
: `/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{
type: newRecord ? "POST" : "PUT",
data,
}
)
.then((ajaxData) => {
this.commitBuffer();
this.set("savingStatus", I18n.t("saved"));
if (newRecord) {
const model = this.get("model");
model.set("id", ajaxData.house_ad.id);
const houseAds = this.get("adminPluginsHouseAds.model");
if (!houseAds.includes(model)) {
houseAds.pushObject(model);
}
this.router.transitionTo(
"adminPlugins.houseAds.show",
model.get("id")
);
}
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: "",
});
});
);
this.savingStatus = I18n.t("saved");
const houseAds = this.houseAdsController.model;
if (newRecord) {
this.buffered.id = ajaxData.house_ad.id;
if (!houseAds.includes(this.buffered)) {
houseAds.pushObject(EmberObject.create(this.buffered));
}
this.router.transitionTo(
"adminPlugins.houseAds.show",
this.buffered.id
);
} else {
houseAds
.find((ad) => ad.id === this.buffered.id)
.setProperties(this.buffered);
}
} catch (error) {
popupAjaxError(error);
} finally {
this.set("model", this.buffered);
this.saving = false;
this.savingStatus = "";
}
},
}
}
cancel() {
this.rollbackBuffer();
},
@action
setCategoryIds(categoryArray) {
this.selectedCategories = categoryArray;
this.buffered.category_ids = categoryArray.map((c) => c.id);
this.setCategoriesForBuffered();
}
destroy() {
const houseAds = this.get("adminPluginsHouseAds.model");
const model = this.get("model");
@action
setGroupIds(groupIds) {
this.selectedGroups = groupIds;
this.buffered.group_ids = groupIds.map((id) => id);
}
if (!model.get("id")) {
this.router.transitionTo("adminPlugins.houseAds.index");
return;
}
@action
cancel() {
this.buffered = new TrackedObject({ ...this.model });
this.selectedCategories = this.model.categories || [];
this.selectedGroups = this.model.group_ids || [];
this.setCategoriesForBuffered();
}
ajax(`/admin/plugins/pluginad/house_creatives/${model.get("id")}`, {
type: "DELETE",
})
.then(() => {
houseAds.removeObject(model);
this.router.transitionTo("adminPlugins.houseAds.index");
})
.catch(popupAjaxError);
},
},
});
@action
async destroy() {
if (!this.buffered.id) {
this.router.transitionTo("adminPlugins.houseAds.index");
return;
}
try {
await ajax(
`/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{
type: "DELETE",
}
);
this.houseAdsController.model.removeObject(
this.houseAdsController.model.findBy("id", this.buffered.id)
);
this.router.transitionTo("adminPlugins.houseAds.index");
} catch (error) {
popupAjaxError(error);
}
}

@action
openPreview() {
this.modal.show(Preview, {
model: {
html: this.buffered.html,
},
});
}

setCategoriesForBuffered() {
// we need to fetch the categories because the serializer is not being used
// to attach the category object to the house ads
this.buffered.categories = this.buffered.category_ids
? this.buffered.category_ids.map((categoryId) =>
Category.findById(categoryId)
)
: [];
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import EmberObject from "@ember/object";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n";

export default DiscourseRoute.extend({
model(params) {
if (params.ad_id === "new") {
return EmberObject.create({
return new TrackedObject({
name: I18n.t("admin.adplugin.house_ads.new_name"),
html: "",
visible_to_logged_in_users: true,
visible_to_anons: true,
});
} else {
return this.modelFor("adminPlugins.houseAds").findBy(
"id",
parseInt(params.ad_id, 10)
return new TrackedObject(
this.modelFor("adminPlugins.houseAds").findBy(
"id",
parseInt(params.ad_id, 10)
)
);
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<section class="edit-house-ad content-body">
<h1><TextField @value={{buffered.name}} class="house-ad-name" /></h1>
<h1><TextField @value={{this.buffered.name}} class="house-ad-name" /></h1>
<div class="controls">
<AceEditor @content={{buffered.html}} @mode="html" />
<AceEditor @content={{this.buffered.html}} @mode="html" />
</div>
<div class="controls">
<div class="visibility-settings">
Expand All @@ -22,25 +22,51 @@
/>
<span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span>
</div>

<HouseAdsCategorySelector
@categories={{this.site.categories}}
@selectedCategories={{this.selectedCategories}}
@onChange={{this.setCategoryIds}}
@options={{hash allowAny=true}}
class="house-ads-categories"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.category_chooser_description"}}
</div>

<GroupChooser
@content={{this.site.groups}}
@onChange={{this.setGroupIds}}
@value={{this.selectedGroups}}
class="banner-groups"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.group_chooser_description"}}
</div>
</div>

<DButton
@action={{action "save"}}
@disabled={{disableSave}}
@action={{this.save}}
@disabled={{this.disabledSave}}
@label="admin.adplugin.house_ads.save"
class="btn-primary save-button"
/>

{{#if saving}}
{{savingStatus}}
{{#if this.saving}}
{{this.savingStatus}}
{{else}}
{{#if dirty}}
<a href {{action "cancel"}}>{{i18n "cancel"}}</a>
{{/if}}
{{#unless this.disabledSave}}
<DButton @action={{this.cancel}} @label="cancel" />
{{/unless}}
{{/if}}

<DButton
@action={{action "destroy"}}
@action={{this.openPreview}}
@label="admin.adplugin.house_ads.preview"
/>

<DButton
@action={{this.destroy}}
@label="admin.adplugin.house_ads.delete"
class="btn-danger delete-button"
/>
Expand Down
Loading

0 comments on commit 554f03f

Please sign in to comment.