Author SHA1 Message Date
David Taylor 3ca4398bad DEV: Restructure file locations
- Colocate components / connectors
- Move admin UI files to `admin/assets` so they are only served to admins
2023-11-07 18:14:24 +00:00
137 changed files with 4187 additions and 6195 deletions
+1 -3
View File
@@ -1,4 +1,2 @@
< 3.4.0.beta2-dev: 7685ebf396c93e8accc5a76a81fcec4384a73fa3
< 3.4.0.beta1-dev: da38a4c053b68d7076db4441cfe86b473979945d
< 3.3.0.beta1-dev: 062efc8a547c19474eaeefae464837d63a5e5fb3
3.1.999: 30c49eb50d6d16fcf82bace3da0e1972b7b220c0 3.1.999: 30c49eb50d6d16fcf82bace3da0e1972b7b220c0
+1
View File
@@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/eslint");
-2
View File
@@ -1,2 +0,0 @@
engine-strict = true
auto-install-peers = false
+21 -54
View File
@@ -1,87 +1,54 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (7.1.3.3)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2) ast (2.4.2)
base64 (0.2.0) json (2.6.3)
bigdecimal (3.1.8)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
drb (2.2.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
json (2.7.2)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
minitest (5.23.1) parallel (1.23.0)
mutex_m (0.2.0) parser (3.2.2.4)
parallel (1.24.0)
parser (3.3.1.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
prettier_print (1.2.1) prettier_print (1.2.1)
racc (1.8.0) racc (1.7.3)
rack (3.0.11)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.9.2) regexp_parser (2.8.2)
rexml (3.3.9) rexml (3.2.6)
rubocop (1.64.0) rubocop (1.57.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3) rubocop-ast (1.30.0)
parser (>= 3.3.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.20.0) rubocop-capybara (2.19.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-discourse (3.8.0) rubocop-discourse (3.4.0)
activesupport (>= 6.1) rubocop (>= 1.1.0)
rubocop (>= 1.59.0) rubocop-rspec (>= 2.0.0)
rubocop-capybara (>= 2.0.0) rubocop-factory_bot (2.24.0)
rubocop-factory_bot (>= 2.0.0) rubocop (~> 1.33)
rubocop-rails (>= 2.25.0) rubocop-rspec (2.25.0)
rubocop-rspec (>= 2.25.0)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-rails (2.25.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.29.2)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
syntax_tree (6.2.0) syntax_tree (6.2.0)
prettier_print (>= 1.2.0) prettier_print (>= 1.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
PLATFORMS PLATFORMS
ruby arm64-darwin-21
x86_64-linux
DEPENDENCIES DEPENDENCIES
rubocop-discourse rubocop-discourse
syntax_tree syntax_tree
BUNDLED WITH BUNDLED WITH
2.5.10 2.4.21
@@ -1,7 +0,0 @@
import CategorySelector from "select-kit/components/category-selector";
export default class HouseAdsCategorySelector extends CategorySelector {
get value() {
return this.selectedCategories.map((c) => c.id);
}
}
@@ -1,44 +1,42 @@
import { action, computed } from "@ember/object"; import Ember from "ember";
import { classNames } from "@ember-decorators/component";
import { makeArray } from "discourse-common/lib/helpers";
import MultiSelectComponent from "select-kit/components/multi-select"; import MultiSelectComponent from "select-kit/components/multi-select";
const { makeArray } = Ember;
import { computed } from "@ember/object";
@classNames("house-ads-chooser") export default MultiSelectComponent.extend({
export default class HouseAdsChooser extends MultiSelectComponent { classNames: ["house-ads-chooser"],
filterable = true; filterable: true,
filterPlaceholder = "admin.adplugin.house_ads.filter_placeholder"; filterPlaceholder: "admin.adplugin.house_ads.filter_placeholder",
tokenSeparator = "|"; tokenSeparator: "|",
allowCreate = false; allowCreate: false,
allowAny = false; allowAny: false,
settingValue = ""; settingValue: "",
valueAttribute = null; valueAttribute: null,
nameProperty = null; nameProperty: null,
@computed("settingValue") value: computed("settingValue", function () {
get value() {
return this.settingValue return this.settingValue
.toString() .toString()
.split(this.tokenSeparator) .split(this.tokenSeparator)
.filter(Boolean); .filter(Boolean);
} }),
// TODO: kept for legacy, remove when Discourse is 2.5 // TODO: kept for legacy, remove when Discourse is 2.5
mutateValues(values) { mutateValues(values) {
this.set("settingValue", values.join(this.tokenSeparator)); this.set("settingValue", values.join(this.tokenSeparator));
} },
computeValues() { computeValues() {
return this.settingValue.split(this.tokenSeparator).filter(Boolean); return this.settingValue.split(this.tokenSeparator).filter(Boolean);
} },
@computed("choices") content: computed("choices", function () {
get content() {
return makeArray(this.choices); return makeArray(this.choices);
} }),
@action actions: {
onChange(value) { onChange(value) {
const settingValue = makeArray(value).join(this.tokenSeparator); const settingValue = makeArray(value).join(this.tokenSeparator);
this.onChange?.(settingValue); this.attrs.onChange && this.attrs.onChange(settingValue);
} },
} },
});
@@ -1,13 +1,13 @@
<label for={{this.name}}>{{this.title}}</label> <label for={{name}}>{{title}}</label>
{{house-ads-chooser {{house-ads-chooser
settingValue=this.adValue settingValue=adValue
choices=this.adNames choices=adNames
onChange=(action (mut this.adValue)) onChange=(action (mut adValue))
}} }}
<div class="setting-controls"> <div class="setting-controls">
{{#if this.changed}} {{#if changed}}
<DButton class="ok" @action={{action "save"}} @icon="check" /> <DButton class="ok" @action={{action "save"}} @icon="check" />
<DButton class="cancel" @action={{action "cancel"}} @icon="times" /> <DButton class="cancel" @action={{action "cancel"}} @icon="times" />
{{/if}} {{/if}}
</div> </div>
<p class="help">{{this.help}}</p> <p class="help">{{help}}</p>
@@ -1,8 +1,7 @@
import { mapBy } from "@ember/object/computed"; import { mapBy } from "@ember/object/computed";
import { classNames } from "@ember-decorators/component";
import HouseAdsSetting from "discourse/plugins/discourse-adplugin/discourse/components/house-ads-setting"; import HouseAdsSetting from "discourse/plugins/discourse-adplugin/discourse/components/house-ads-setting";
@classNames("house-ads-setting house-ads-list-setting") export default HouseAdsSetting.extend({
export default class HouseAdsListSetting extends HouseAdsSetting { classNames: "house-ads-setting house-ads-list-setting",
@mapBy("allAds", "name") adNames; adNames: mapBy("allAds", "name"),
} });
@@ -1,9 +1,9 @@
<label for={{this.name}}>{{this.title}}</label> <label for={{name}}>{{title}}</label>
<TextField @value={{this.adValue}} @classNames="house-ads-text-input" /> <TextField @value={{adValue}} @classNames="house-ads-text-input" />
<div class="setting-controls"> <div class="setting-controls">
{{#if this.changed}} {{#if changed}}
<DButton class="ok" @action={{action "save"}} @icon="check" /> <DButton class="ok" @action={{action "save"}} @icon="check" />
<DButton class="cancel" @action={{action "cancel"}} @icon="times" /> <DButton class="cancel" @action={{action "cancel"}} @icon="times" />
{{/if}} {{/if}}
</div> </div>
<p class="help">{{this.help}}</p> <p class="help">{{help}}</p>
@@ -1,58 +1,58 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object";
import { classNames } from "@ember-decorators/component";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n, propertyNotEqual } from "discourse/lib/computed"; import { i18n, propertyNotEqual } from "discourse/lib/computed";
import I18n from "I18n"; import I18n from "I18n";
@classNames("house-ads-setting") export default Component.extend({
export default class HouseAdsSetting extends Component { classNames: "house-ads-setting",
adValue = ""; adValue: "",
saving = false; saving: false,
savingStatus = ""; savingStatus: "",
title: i18n("name", "admin.adplugin.house_ads.%@.title"),
@i18n("name", "admin.adplugin.house_ads.%@.title") title; help: i18n("name", "admin.adplugin.house_ads.%@.description"),
@i18n("name", "admin.adplugin.house_ads.%@.description") help; changed: propertyNotEqual("adValue", "value"),
@propertyNotEqual("adValue", "value") changed;
init() { init() {
super.init(...arguments); this._super(...arguments);
this.set("adValue", this.get("value")); this.set("adValue", this.get("value"));
} },
@action actions: {
save() { save() {
if (!this.get("saving")) { if (!this.get("saving")) {
this.setProperties({ this.setProperties({
saving: true, saving: true,
savingStatus: I18n.t("saving"), savingStatus: I18n.t("saving"),
});
ajax(`/admin/plugins/pluginad/house_settings/${this.get("name")}.json`, {
type: "PUT",
data: { value: this.get("adValue") },
})
.then(() => {
const adSettings = this.get("adSettings");
adSettings.set(this.get("name"), this.get("adValue"));
this.setProperties({
value: this.get("adValue"),
savingStatus: I18n.t("saved"),
});
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: "",
});
}); });
}
}
@action ajax(
cancel() { `/admin/plugins/pluginad/house_settings/${this.get("name")}.json`,
this.set("adValue", this.get("value")); {
} type: "PUT",
} data: { value: this.get("adValue") },
}
)
.then(() => {
const adSettings = this.get("adSettings");
adSettings.set(this.get("name"), this.get("adValue"));
this.setProperties({
value: this.get("adValue"),
savingStatus: I18n.t("saved"),
});
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: "",
});
});
}
},
cancel() {
this.set("adValue", this.get("value"));
},
},
});
@@ -1,18 +0,0 @@
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,8 +1,8 @@
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import { alias } from "@ember/object/computed"; import { alias } from "@ember/object/computed";
export default class AdminPluginsHouseAdsIndexController extends Controller { export default Controller.extend({
@controller("adminPlugins.houseAds") adminPluginsHouseAds; adminPluginsHouseAds: controller("adminPlugins.houseAds"),
@alias("adminPluginsHouseAds.model") houseAds; houseAds: alias("adminPluginsHouseAds.model"),
@alias("adminPluginsHouseAds.houseAdsSettings") adSettings; adSettings: alias("adminPluginsHouseAds.houseAdsSettings"),
} });
@@ -1,156 +1,113 @@
import { tracked } from "@glimmer/tracking";
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action } from "@ember/object"; import { not, or } from "@ember/object/computed";
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 { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category"; import { propertyNotEqual } from "discourse/lib/computed";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import I18n from "I18n"; import I18n from "I18n";
import Preview from "../components/modal/preview";
export default class adminPluginsHouseAdsShow extends Controller { export default Controller.extend(bufferedProperty("model"), {
@service router; adminPluginsHouseAds: controller("adminPlugins.houseAds"),
@service modal;
@controller("adminPlugins.houseAds") houseAdsController; saving: false,
savingStatus: "",
@tracked selectedCategories = []; nameDirty: propertyNotEqual("buffered.name", "model.name"),
@tracked selectedGroups = []; htmlDirty: propertyNotEqual("buffered.html", "model.html"),
@tracked saving = false; visibleToAnonsDirty: propertyNotEqual(
@tracked savingStatus = ""; "buffered.visible_to_anons",
@tracked buffered; "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"),
@observes("model") actions: {
modelChanged() { save() {
this.buffered = new TrackedObject({ ...this.model }); if (!this.get("saving")) {
this.selectedCategories = this.model.categories || []; this.setProperties({
this.selectedGroups = this.model.group_ids || []; saving: true,
} savingStatus: I18n.t("saving"),
});
get disabledSave() { const data = {},
for (const key in this.buffered) { buffered = this.get("buffered"),
// we don't want to compare the categories array newRecord = !buffered.get("id");
if (key !== "categories" && this.buffered[key] !== this.model[key]) {
return false;
}
}
return true;
}
@action if (!newRecord) {
async save() { data.id = buffered.get("id");
if (!this.saving) { }
this.saving = true; data.name = buffered.get("name");
this.savingStatus = I18n.t("saving"); data.html = buffered.get("html");
const data = {}; data.visible_to_logged_in_users = buffered.get(
const newRecord = !this.buffered.id; "visible_to_logged_in_users"
if (!newRecord) { );
data.id = this.buffered.id; data.visible_to_anons = buffered.get("visible_to_anons");
}
data.name = this.buffered.name; ajax(
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 newRecord
? `/admin/plugins/pluginad/house_creatives` ? `/admin/plugins/pluginad/house_creatives`
: `/admin/plugins/pluginad/house_creatives/${this.buffered.id}`, : `/admin/plugins/pluginad/house_creatives/${buffered.get("id")}`,
{ {
type: newRecord ? "POST" : "PUT", type: newRecord ? "POST" : "PUT",
data, 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.transitionToRoute(
"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.transitionToRoute("adminPlugins.houseAds.index");
return;
}
ajax(`/admin/plugins/pluginad/house_creatives/${model.get("id")}`, {
type: "DELETE",
})
.then(() => {
houseAds.removeObject(model);
this.transitionToRoute("adminPlugins.houseAds.index");
})
.catch(popupAjaxError);
},
},
});
@@ -1,5 +1,5 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
export default class AdminPluginsHouseAdsController extends Controller { export default Controller.extend({
loadingAds = true; loadingAds: true,
} });
@@ -1,12 +1,9 @@
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsHouseAdsIndex extends DiscourseRoute { export default DiscourseRoute.extend({
@service router;
@action @action
moreSettings() { moreSettings() {
this.router.transitionTo("adminSiteSettingsCategory", "ad_plugin"); this.transitionTo("adminSiteSettingsCategory", "ad_plugin");
} },
} });
@@ -1,23 +1,21 @@
import { TrackedObject } from "@ember-compat/tracked-built-ins"; import EmberObject from "@ember/object";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
export default class AdminPluginsHouseAdsShow extends DiscourseRoute { export default DiscourseRoute.extend({
model(params) { model(params) {
if (params.ad_id === "new") { if (params.ad_id === "new") {
return new TrackedObject({ return EmberObject.create({
name: I18n.t("admin.adplugin.house_ads.new_name"), name: I18n.t("admin.adplugin.house_ads.new_name"),
html: "", html: "",
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
visible_to_anons: true, visible_to_anons: true,
}); });
} else { } else {
return new TrackedObject( return this.modelFor("adminPlugins.houseAds").findBy(
this.modelFor("adminPlugins.houseAds").findBy( "id",
"id", parseInt(params.ad_id, 10)
parseInt(params.ad_id, 10)
)
); );
} }
} },
} });
@@ -2,15 +2,15 @@ import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsHouseAds extends DiscourseRoute { export default DiscourseRoute.extend({
settings = null; settings: null,
model() { model() {
return ajax("/admin/plugins/pluginad/house_creatives.json").then((data) => { return ajax("/admin/plugins/pluginad/house_creatives.json").then((data) => {
this.set("settings", EmberObject.create(data.settings)); this.set("settings", EmberObject.create(data.settings));
return data.house_ads.map((ad) => EmberObject.create(ad)); return data.house_ads.map((ad) => EmberObject.create(ad));
}); });
} },
setupController(controller, model) { setupController(controller, model) {
controller.setProperties({ controller.setProperties({
@@ -18,5 +18,5 @@ export default class AdminPluginsHouseAds extends DiscourseRoute {
houseAdsSettings: this.get("settings"), houseAdsSettings: this.get("settings"),
loadingAds: false, loadingAds: false,
}); });
} },
} });
@@ -1,51 +1,51 @@
<section class="house-ads-settings content-body"> <DSection @class="house-ads-settings content-body">
<div>{{i18n "admin.adplugin.house_ads.description"}}</div> <div>{{i18n "admin.adplugin.house_ads.description"}}</div>
{{#if this.houseAds.length}} {{#if houseAds.length}}
<form class="form-horizontal"> <form class="form-horizontal">
{{house-ads-list-setting {{house-ads-list-setting
name="topic_list_top" name="topic_list_top"
value=this.adSettings.topic_list_top value=adSettings.topic_list_top
allAds=this.houseAds allAds=houseAds
adSettings=this.adSettings adSettings=adSettings
}} }}
{{house-ads-list-setting {{house-ads-list-setting
name="topic_above_post_stream" name="topic_above_post_stream"
value=this.adSettings.topic_above_post_stream value=adSettings.topic_above_post_stream
allAds=this.houseAds allAds=houseAds
adSettings=this.adSettings adSettings=adSettings
}} }}
{{house-ads-list-setting {{house-ads-list-setting
name="topic_above_suggested" name="topic_above_suggested"
value=this.adSettings.topic_above_suggested value=adSettings.topic_above_suggested
allAds=this.houseAds allAds=houseAds
adSettings=this.adSettings adSettings=adSettings
}} }}
{{house-ads-list-setting {{house-ads-list-setting
name="post_bottom" name="post_bottom"
value=this.adSettings.post_bottom value=adSettings.post_bottom
allAds=this.houseAds allAds=houseAds
adSettings=this.adSettings adSettings=adSettings
}} }}
{{house-ads-list-setting {{house-ads-list-setting
name="topic_list_between" name="topic_list_between"
value=this.adSettings.topic_list_between value=adSettings.topic_list_between
allAds=this.houseAds allAds=houseAds
adSettings=this.adSettings adSettings=adSettings
}} }}
<DButton <DButton
@label="admin.adplugin.house_ads.more_settings" @label="admin.adplugin.house_ads.more_settings"
@icon="cog" @icon="cog"
@action={{route-action "moreSettings"}}
class="btn-default" class="btn-default"
@action={{route-action "moreSettings"}}
/> />
</form> </form>
{{else}} {{else}}
<p> <p>
{{#link-to route="adminPlugins.houseAds.show" model="new"}} {{#link-to "adminPlugins.houseAds.show" "new"}}
{{i18n "admin.adplugin.house_ads.get_started"}} {{i18n "admin.adplugin.house_ads.get_started"}}
{{/link-to}} {{/link-to}}
</p> </p>
{{/if}} {{/if}}
</section> </DSection>
@@ -1,19 +1,15 @@
<section class="edit-house-ad content-body"> <DSection @class="edit-house-ad content-body">
<h1><TextField @value={{this.buffered.name}} class="house-ad-name" /></h1> <h1><TextField @class="house-ad-name" @value={{buffered.name}} /></h1>
<div class="controls"> <div class="controls">
<AceEditor <AceEditor @content={{buffered.html}} @mode="html" />
@content={{this.buffered.html}}
@onChange={{fn (mut this.buffered.html)}}
@mode="html"
/>
</div> </div>
<div class="controls"> <div class="controls">
<div class="visibility-settings"> <div class="visibility-settings">
<div> <div>
<Input <Input
class="visible-to-logged-in-checkbox"
@type="checkbox" @type="checkbox"
@checked={{this.buffered.visible_to_logged_in_users}} @checked={{this.buffered.visible_to_logged_in_users}}
class="visible-to-logged-in-checkbox"
/> />
<span>{{i18n "admin.adplugin.house_ads.show_to_logged_in_users"}}</span> <span>{{i18n "admin.adplugin.house_ads.show_to_logged_in_users"}}</span>
</div> </div>
@@ -26,53 +22,27 @@
/> />
<span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span> <span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span>
</div> </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> </div>
<DButton <DButton
@action={{this.save}} @action={{action "save"}}
@disabled={{this.disabledSave}} @disabled={{disableSave}}
@label="admin.adplugin.house_ads.save"
class="btn-primary save-button" class="btn-primary save-button"
@label="admin.adplugin.house_ads.save"
/> />
{{#if this.saving}} {{#if saving}}
{{this.savingStatus}} {{savingStatus}}
{{else}} {{else}}
{{#unless this.disabledSave}} {{#if dirty}}
<DButton @action={{this.cancel}} @label="cancel" /> <a href {{action "cancel"}}>{{i18n "cancel"}}</a>
{{/unless}} {{/if}}
{{/if}} {{/if}}
<DButton <DButton
@action={{this.openPreview}} @action={{action "destroy"}}
@label="admin.adplugin.house_ads.preview"
/>
<DButton
@action={{this.destroy}}
@label="admin.adplugin.house_ads.delete"
class="btn-danger delete-button" class="btn-danger delete-button"
@label="admin.adplugin.house_ads.delete"
/> />
</div> </div>
</section> </DSection>
@@ -1,25 +1,21 @@
<div class="adplugin-mgmt"> <div class="adplugin-mgmt">
<h1>{{i18n "admin.adplugin.house_ads.title"}}</h1> <h1>{{i18n "admin.adplugin.house_ads.title"}}</h1>
{{#if this.model.length}} {{#if model.length}}
<div class="content-list"> <div class="content-list">
<div class="house-ads-actions"> <div class="house-ads-actions">
<LinkTo {{#link-to "adminPlugins.houseAds.show" "new" class="btn btn-primary"}}
@route="adminPlugins.houseAds.show"
@model="new"
class="btn btn-primary"
>
{{d-icon "plus"}} {{d-icon "plus"}}
<span>{{i18n "admin.adplugin.house_ads.new"}}</span> <span>{{i18n "admin.adplugin.house_ads.new"}}</span>
</LinkTo> {{/link-to}}
<LinkTo @route="adminPlugins.houseAds.index" class="btn btn-default"> {{#link-to "adminPlugins.houseAds.index" class="btn btn-default"}}
{{d-icon "gear"}} {{d-icon "cog"}}
<span>{{i18n "admin.adplugin.house_ads.settings"}}</span> <span>{{i18n "admin.adplugin.house_ads.settings"}}</span>
</LinkTo> {{/link-to}}
</div> </div>
<ul class="house-ads-list"> <ul class="house-ads-list">
{{#each this.model as |ad|}} {{#each model as |ad|}}
<li class="house-ads-list-item"> <li class="house-ads-list-item">
{{#link-to route="adminPlugins.houseAds.show" model=ad.id}} {{#link-to "adminPlugins.houseAds.show" ad.id}}
{{ad.name}} {{ad.name}}
{{/link-to}} {{/link-to}}
</li> </li>
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
#
class AdstxtController < ::ApplicationController
requires_plugin AdPlugin.plugin_name
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
def index
raise Discourse::NotFound if SiteSetting.ads_txt.blank?
render plain: SiteSetting.ads_txt
end
end
+3 -23
View File
@@ -5,23 +5,11 @@ module ::AdPlugin
requires_plugin AdPlugin.plugin_name requires_plugin AdPlugin.plugin_name
def index def index
render_json_dump( render_json_dump(house_ads: HouseAd.all.map(&:to_hash), settings: HouseAdSetting.all)
house_ads:
HouseAd.all.map do |ad|
ad.to_hash.merge!(categories: Category.secured(@guardian).where(id: ad.category_ids))
end,
settings: HouseAdSetting.all,
)
end end
def show def show
house_ad_hash = HouseAd.find(params[:id])&.to_hash render_json_dump(house_ad: HouseAd.find(params[:id])&.to_hash)
if house_ad_hash
house_ad_hash.merge!(
categories: Category.secured(@guardian).where(id: house_ad_hash[:category_ids]),
)
end
render_json_dump(house_ad: house_ad_hash)
end end
def create def create
@@ -53,15 +41,7 @@ module ::AdPlugin
@permitted ||= @permitted ||=
begin begin
permitted = permitted =
params.permit( params.permit(:id, :name, :html, :visible_to_anons, :visible_to_logged_in_users)
:id,
:name,
:html,
:visible_to_anons,
:visible_to_logged_in_users,
group_ids: [],
category_ids: [],
)
permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast( permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast(
permitted[:visible_to_logged_in_users], permitted[:visible_to_logged_in_users],
) )
+9 -40
View File
@@ -4,13 +4,7 @@ module ::AdPlugin
class HouseAd class HouseAd
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :id, attr_accessor :id, :name, :html, :visible_to_logged_in_users, :visible_to_anons
:name,
:html,
:visible_to_logged_in_users,
:visible_to_anons,
:category_ids,
:group_ids
NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i
@@ -28,8 +22,6 @@ module ::AdPlugin
@html = "<div class='house-ad'>New Ad</div>" @html = "<div class='house-ad'>New Ad</div>"
@visible_to_logged_in_users = true @visible_to_logged_in_users = true
@visible_to_anons = true @visible_to_anons = true
@group_ids = []
@category_ids = []
end end
def self.from_hash(h) def self.from_hash(h)
@@ -37,12 +29,10 @@ module ::AdPlugin
ad.name = h[:name] ad.name = h[:name]
ad.html = h[:html] ad.html = h[:html]
ad.id = h[:id].to_i if h[:id] ad.id = h[:id].to_i if h[:id]
ad.visible_to_logged_in_users = h[:visible_to_logged_in_users] if h.key?( if h.key?(:visible_to_logged_in_users)
:visible_to_logged_in_users, ad.visible_to_logged_in_users = h[:visible_to_logged_in_users]
) end
ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons) ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons)
ad.group_ids = h[:group_ids].map(&:to_i) if h.key?(:group_ids)
ad.category_ids = h[:category_ids].map(&:to_i) if h.key?(:category_ids)
ad ad
end end
@@ -82,24 +72,8 @@ module ::AdPlugin
self.all.select(&:visible_to_anons) self.all.select(&:visible_to_anons)
end end
def self.all_for_logged_in_users(scope) def self.all_for_logged_in_users
if scope.nil? self.all.select(&:visible_to_logged_in_users)
# this is for our admin page, so we don't need to filter by group
self.all.select(&:visible_to_logged_in_users)
else
# otherwise, filter by group and visible categories
self.all.select do |ad|
(
ad.group_ids.any? { |group_id| scope.user.groups.pluck(:id).include?(group_id) } ||
ad.group_ids.include?(Group::AUTO_GROUPS[:everyone]) || ad.group_ids.empty?
) && ad.visible_to_logged_in_users &&
(
ad.category_ids.any? do |category_id|
Category.secured(scope).pluck(:id).include?(category_id)
end || true
)
end
end
end end
def save def save
@@ -117,13 +91,10 @@ module ::AdPlugin
def update(attrs) def update(attrs)
self.name = attrs[:name] self.name = attrs[:name]
self.html = attrs[:html] self.html = attrs[:html]
self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users] if attrs.key?( if attrs.key?(:visible_to_logged_in_users)
:visible_to_logged_in_users, self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users]
) end
self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons) self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons)
# ensure that group_ids and category_ids can be set to an empty array
self.group_ids = attrs.key?(:group_ids) ? attrs[:group_ids].map(&:to_i) : []
self.category_ids = attrs.key?(:category_ids) ? attrs[:category_ids].map(&:to_i) : []
self.save self.save
end end
@@ -134,8 +105,6 @@ module ::AdPlugin
html: @html, html: @html,
visible_to_logged_in_users: @visible_to_logged_in_users, visible_to_logged_in_users: @visible_to_logged_in_users,
visible_to_anons: @visible_to_anons, visible_to_anons: @visible_to_anons,
group_ids: @group_ids,
category_ids: @category_ids,
} }
end end
+4 -4
View File
@@ -21,14 +21,14 @@ module ::AdPlugin
settings settings
end end
def self.settings_and_ads(for_anons: true, scope: nil) def self.settings_and_ads(for_anons: true)
settings = AdPlugin::HouseAdSetting.all settings = AdPlugin::HouseAdSetting.all
ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq
if for_anons if for_anons
ads = AdPlugin::HouseAd.all_for_anons ads = AdPlugin::HouseAd.all_for_anons
else else
ads = AdPlugin::HouseAd.all_for_logged_in_users(scope) ads = AdPlugin::HouseAd.all_for_logged_in_users
end end
ads = ads.select { |ad| ad_names.include?(ad.name) } ads = ads.select { |ad| ad_names.include?(ad.name) }
@@ -41,14 +41,14 @@ module ::AdPlugin
), ),
creatives: creatives:
ads.inject({}) do |h, ad| ads.inject({}) do |h, ad|
h[ad.name] = { html: ad.html, category_ids: ad.category_ids } h[ad.name] = ad.html
h h
end, end,
} }
end end
def self.update(setting_name, value) def self.update(setting_name, value)
raise Discourse::NotFound if DEFAULTS.keys.exclude?(setting_name.to_sym) raise Discourse::NotFound unless DEFAULTS.keys.include?(setting_name.to_sym)
ad_names = value&.split("|") || [] ad_names = value&.split("|") || []
@@ -1,36 +1,34 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { alias, or } from "@ember/object/computed"; import { alias, or } from "@ember/object/computed";
import { service } from "@ember/service"; import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { import {
isNthPost, isNthPost,
isNthTopicListItem, isNthTopicListItem,
} from "discourse/plugins/discourse-adplugin/discourse/helpers/slot-position"; } from "discourse/plugins/discourse-adplugin/discourse/helpers/slot-position";
export default class AdComponent extends Component { export default Component.extend({
@service router; router: service(),
@or( currentCategoryId: or(
"router.currentRoute.attributes.category.id", "router.currentRoute.attributes.category.id",
"router.currentRoute.parent.attributes.category_id" "router.currentRoute.parent.attributes.category_id"
) ),
currentCategoryId;
@or( currentCategorySlug: or(
"router.currentRoute.attributes.category.slug", "router.currentRoute.attributes.category.slug",
"router.currentRoute.parent.attributes.category.slug" "router.currentRoute.parent.attributes.category.slug"
) ),
currentCategorySlug;
// Server needs to compute this in case hidden tags are being used. // Server needs to compute this in case hidden tags are being used.
@alias("router.currentRoute.parent.attributes.tags_disable_ads") topicTagsDisableAds: alias(
topicTagsDisableAds; "router.currentRoute.parent.attributes.tags_disable_ads"
),
@or( isRestrictedCategory: or(
"router.currentRoute.attributes.category.read_restricted", "router.currentRoute.attributes.category.read_restricted",
"router.currentRoute.parent.attributes.category.read_restricted" "router.currentRoute.parent.attributes.category.read_restricted"
) ),
isRestrictedCategory;
@discourseComputed( @discourseComputed(
"router.currentRoute.attributes.__type", "router.currentRoute.attributes.__type",
@@ -40,21 +38,33 @@ export default class AdComponent extends Component {
if (type === "tag" && tag) { if (type === "tag" && tag) {
return tag; return tag;
} }
} },
@discourseComputed("router.currentRoute.parent.attributes.archetype") @discourseComputed("router.currentRoute.parent.attributes.archetype")
isPersonalMessage(topicType) { isPersonalMessage(topicType) {
return topicType === "private_message"; return topicType === "private_message";
} },
@discourseComputed @discourseComputed("currentUser.groups")
showToGroups() { showToGroups(groups) {
if (!this.currentUser) { const currentUser = this.currentUser;
if (
!currentUser ||
!groups ||
!this.siteSettings.no_ads_for_groups ||
this.siteSettings.no_ads_for_groups.length === 0
) {
return true; return true;
} }
return this.currentUser.show_to_groups; let noAdsGroups = this.siteSettings.no_ads_for_groups
} .split("|")
.filter(Boolean);
let currentGroups = groups.map((g) => g.id.toString());
return !currentGroups.any((g) => noAdsGroups.includes(g));
},
@discourseComputed( @discourseComputed(
"currentCategoryId", "currentCategoryId",
@@ -84,13 +94,13 @@ export default class AdComponent extends Component {
(!isRestrictedCategory || (!isRestrictedCategory ||
!this.siteSettings.no_ads_for_restricted_categories) !this.siteSettings.no_ads_for_restricted_categories)
); );
} },
isNthPost(n) { isNthPost(n) {
return isNthPost(n, this.get("postNumber")); return isNthPost(n, this.get("postNumber"));
} },
isNthTopicListItem(n) { isNthTopicListItem(n) {
return isNthTopicListItem(n, this.get("indexNumber")); return isNthTopicListItem(n, this.get("indexNumber"));
} },
} });
@@ -1,13 +1,12 @@
{{#each this.adComponents as |adComponent|}} {{#each adComponents as |adComponent|}}
{{! Trick to force full destroy/re-render of component when route changes }} {{component
{{#each (array this.router.currentRoute)}} adComponent
{{component placement=placement
adComponent refreshOnChange=refreshOnChange
placement=this.placement category=category
category=this.category listLoading=listLoading
postNumber=this.postNumber postNumber=postNumber
indexNumber=this.indexNumber indexNumber=indexNumber
tagName=this.childTagName tagName=childTagName
}} }}
{{/each}}
{{/each}} {{/each}}
@@ -1,8 +1,6 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { service } from "@ember/service";
import { isBlank } from "@ember/utils"; import { isBlank } from "@ember/utils";
import { tagName } from "@ember-decorators/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed from "discourse-common/utils/decorators";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component"; import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import { import {
isNthPost, isNthPost,
@@ -174,9 +172,9 @@ export function slotContenders(
return types; return types;
} }
@tagName("") export default AdComponent.extend({
export default class AdSlot extends AdComponent { needsUpdate: false,
@service router; tagName: "",
/** /**
* For a given ad placement and optionally a post number if in between posts, * For a given ad placement and optionally a post number if in between posts,
@@ -191,16 +189,29 @@ export default class AdSlot extends AdComponent {
indexNumber, indexNumber,
postNumber postNumber
); );
} },
/**
* When house ads are configured to alternate with other ad networks, we
* need to trigger an update of which ad component is shown after
* navigating between topic lists or topics.
*/
@observes("refreshOnChange")
changed() {
if (this.get("listLoading")) {
return;
}
// force adComponents to be recomputed
this.notifyPropertyChange("needsUpdate");
},
/** /**
* Returns a list of the names of ad components that should be rendered * Returns a list of the names of ad components that should be rendered
* in the given ad placement. It handles alternating between house ads * in the given ad placement. It handles alternating between house ads
* and other ad networks. * and other ad networks.
*
* Depends on `router.currentRoute` so that we refresh ads when navigating around.
*/ */
@discourseComputed("placement", "availableAdTypes", "router.currentRoute") @discourseComputed("placement", "availableAdTypes", "needsUpdate")
adComponents(placement, availableAdTypes) { adComponents(placement, availableAdTypes) {
if ( if (
!availableAdTypes.includes("house-ad") || !availableAdTypes.includes("house-ad") ||
@@ -239,5 +250,5 @@ export default class AdSlot extends AdComponent {
} }
return networkNames; return networkNames;
} },
} });
@@ -1,3 +1,3 @@
{{#if this.showAd}} {{#if showAd}}
<div id={{this.divId}} class={{this.className}}></div> <div id={{divId}} class={{className}}></div>
{{/if}} {{/if}}
@@ -2,7 +2,7 @@ import { scheduleOnce } from "@ember/runloop";
import RSVP from "rsvp"; import RSVP from "rsvp";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed, { observes } from "discourse-common/utils/decorators";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component"; import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
let _loaded = false, let _loaded = false,
@@ -27,9 +27,9 @@ function loadAdbutler(adserverHostname) {
return _promise; return _promise;
} }
export default class AdbutlerAd extends AdComponent { export default AdComponent.extend({
divs = null; divs: null,
publisherId = null; publisherId: null,
init() { init() {
let dimensions = [728, 90]; let dimensions = [728, 90];
@@ -70,8 +70,8 @@ export default class AdbutlerAd extends AdComponent {
dimensions, dimensions,
}); });
super.init(); this._super();
} },
_triggerAds() { _triggerAds() {
if (isTesting()) { if (isTesting()) {
@@ -104,44 +104,52 @@ export default class AdbutlerAd extends AdComponent {
} }
}.bind(this) }.bind(this)
); );
} },
didInsertElement() { didInsertElement() {
super.didInsertElement(); this._super();
scheduleOnce("afterRender", this, this._triggerAds); scheduleOnce("afterRender", this, this._triggerAds);
} },
@discourseComputed @observes("listLoading")
showAdbutlerAds() { waitForLoad() {
if (!this.currentUser) { if (this.get("adRequested")) {
return true; return;
} // already requested that this ad unit be populated
if (!this.get("listLoading")) {
scheduleOnce("afterRender", this, this._triggerAds);
} }
},
return this.currentUser.show_adbutler_ads; @discourseComputed("currentUser.trust_level")
} showToTrustLevel(trustLevel) {
return !(
trustLevel && trustLevel > this.siteSettings.adbutler_through_trust_level
);
},
@discourseComputed( @discourseComputed(
"publisherId", "publisherId",
"showAdbutlerAds", "showToTrustLevel",
"showToGroups", "showToGroups",
"showAfterPost", "showAfterPost",
"showOnCurrentPage" "showOnCurrentPage"
) )
showAd( showAd(
publisherId, publisherId,
showAdbutlerAds, showToTrustLevel,
showToGroups, showToGroups,
showAfterPost, showAfterPost,
showOnCurrentPage showOnCurrentPage
) { ) {
return ( return (
publisherId && publisherId &&
showAdbutlerAds && showToTrustLevel &&
showToGroups && showToGroups &&
showAfterPost && showAfterPost &&
showOnCurrentPage showOnCurrentPage
); );
} },
@discourseComputed("postNumber") @discourseComputed("postNumber")
showAfterPost(postNumber) { showAfterPost(postNumber) {
@@ -149,5 +157,5 @@ export default class AdbutlerAd extends AdComponent {
return true; return true;
} }
return this.isNthPost(parseInt(this.siteSettings.adbutler_nth_post, 10)); return this.isNthPost(parseInt(this.siteSettings.adbutler_nth_post, 10));
} },
} });

Some files were not shown because too many files have changed in this diff Show More