FEATURE: Add group and category restrictions to house ads (#205)
# 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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setCategoryIds(categoryArray) {
|
||||
this.selectedCategories = categoryArray;
|
||||
this.buffered.category_ids = categoryArray.map((c) => c.id);
|
||||
this.setCategoriesForBuffered();
|
||||
}
|
||||
|
||||
@action
|
||||
setGroupIds(groupIds) {
|
||||
this.selectedGroups = groupIds;
|
||||
this.buffered.group_ids = groupIds.map((id) => id);
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.buffered = new TrackedObject({ ...this.model });
|
||||
this.selectedCategories = this.model.categories || [];
|
||||
this.selectedGroups = this.model.group_ids || [];
|
||||
this.setCategoriesForBuffered();
|
||||
}
|
||||
|
||||
@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)
|
||||
)
|
||||
.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: "",
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackBuffer();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const houseAds = this.get("adminPluginsHouseAds.model");
|
||||
const model = this.get("model");
|
||||
|
||||
if (!model.get("id")) {
|
||||
this.router.transitionTo("adminPlugins.houseAds.index");
|
||||
return;
|
||||
}
|
||||
|
||||
ajax(`/admin/plugins/pluginad/house_creatives/${model.get("id")}`, {
|
||||
type: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
houseAds.removeObject(model);
|
||||
this.router.transitionTo("adminPlugins.houseAds.index");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
},
|
||||
});
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user