From 42b1ca8f78f56d0f60d62163e2cba77e7d03de97 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Thu, 14 Nov 2024 10:03:58 +1100 Subject: [PATCH] UX: redesign admin permalinks page (#29634) Redesign the permalinks page to follow the UX guide. In addition, the ability to edit permalinks was added. This change includes: - move to RestModel - added Validations - update endpoint and clear old values after the update - system specs and improvements for unit tests --- .../admin/addon/adapters/permalink.js | 7 + .../addon/components/admin-permalink-form.gjs | 245 ++++++++++++++++++ ...ermalinks.js => admin-permalinks-index.js} | 38 ++- .../admin/addon/models/permalink.js | 22 +- .../addon/routes/admin-permalinks-edit.js | 10 + .../admin/addon/routes/admin-route-map.js | 12 +- .../admin/addon/templates/permalinks-edit.hbs | 1 + .../addon/templates/permalinks-index.hbs | 135 ++++++++++ .../admin/addon/templates/permalinks-new.hbs | 1 + .../admin/addon/templates/permalinks.hbs | 88 ------- .../control/conditional-content/content.gjs | 16 +- .../form-kit/components/fk/control/image.gjs | 8 +- .../app/form-kit/components/fk/form.gjs | 1 + .../tests/helpers/form-kit-assertions.js | 7 +- .../form-kit/controls/image-test.gjs | 27 +- .../components/form-kit/form-test.gjs | 30 +++ .../stylesheets/common/admin/customize.scss | 63 ++--- .../admin/permalinks_controller.rb | 51 ++-- app/models/permalink.rb | 33 +++ config/locales/client.en.yml | 14 +- config/routes.rb | 6 +- spec/models/permalink_spec.rb | 55 ++++ .../admin/permalinks_controller_spec.rb | 118 ++++++--- spec/requests/application_controller_spec.rb | 32 ++- spec/requests/categories_controller_spec.rb | 7 +- spec/requests/permalinks_controller_spec.rb | 13 +- spec/system/admin_permalinks_page_spec.rb | 30 +++ .../pages/admin_permalink_form.rb | 39 +++ .../page_objects/pages/admin_permalinks.rb | 54 ++++ 29 files changed, 924 insertions(+), 239 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/adapters/permalink.js create mode 100644 app/assets/javascripts/admin/addon/components/admin-permalink-form.gjs rename app/assets/javascripts/admin/addon/controllers/{admin-permalinks.js => admin-permalinks-index.js} (65%) create mode 100644 app/assets/javascripts/admin/addon/routes/admin-permalinks-edit.js create mode 100644 app/assets/javascripts/admin/addon/templates/permalinks-edit.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/permalinks-index.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/permalinks-new.hbs delete mode 100644 app/assets/javascripts/admin/addon/templates/permalinks.hbs create mode 100644 spec/system/admin_permalinks_page_spec.rb create mode 100644 spec/system/page_objects/pages/admin_permalink_form.rb create mode 100644 spec/system/page_objects/pages/admin_permalinks.rb diff --git a/app/assets/javascripts/admin/addon/adapters/permalink.js b/app/assets/javascripts/admin/addon/adapters/permalink.js new file mode 100644 index 00000000000..44cca43b0e7 --- /dev/null +++ b/app/assets/javascripts/admin/addon/adapters/permalink.js @@ -0,0 +1,7 @@ +import RestAdapter from "discourse/adapters/rest"; + +export default class Permalink extends RestAdapter { + basePath() { + return "/admin/"; + } +} diff --git a/app/assets/javascripts/admin/addon/components/admin-permalink-form.gjs b/app/assets/javascripts/admin/addon/components/admin-permalink-form.gjs new file mode 100644 index 00000000000..5a1986ccaf1 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-permalink-form.gjs @@ -0,0 +1,245 @@ +import Component from "@glimmer/component"; +import { cached } from "@glimmer/tracking"; +import { inject as controller } from "@ember/controller"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { isEmpty } from "@ember/utils"; +import { eq } from "truth-helpers"; +import BackButton from "discourse/components/back-button"; +import Form from "discourse/components/form"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import i18n from "discourse-common/helpers/i18n"; +import { bind } from "discourse-common/utils/decorators"; +import AdminConfigAreaCard from "admin/components/admin-config-area-card"; +import Permalink from "admin/models/permalink"; + +const TYPE_TO_FIELD_MAP = { + topic: "topicId", + post: "postId", + category: "categoryId", + tag: "tagName", + user: "userId", + external_url: "externalUrl", +}; + +export default class AdminFlagsForm extends Component { + @service router; + @service store; + @controller adminPermalinks; + + get isUpdate() { + return this.args.permalink; + } + + @cached + get formData() { + if (this.isUpdate) { + let permalinkType; + let permalinkValue; + if (!isEmpty(this.args.permalink.topic_id)) { + permalinkType = "topic"; + permalinkValue = this.args.permalink.topic_id; + } else if (!isEmpty(this.args.permalink.post_id)) { + permalinkType = "post"; + permalinkValue = this.args.permalink.post_id; + } else if (!isEmpty(this.args.permalink.category_id)) { + permalinkType = "category"; + permalinkValue = this.args.permalink.category_id; + } else if (!isEmpty(this.args.permalink.tag_name)) { + permalinkType = "tag"; + permalinkValue = this.args.permalink.tag_name; + } else if (!isEmpty(this.args.permalink.external_url)) { + permalinkType = "external_url"; + permalinkValue = this.args.permalink.external_url; + } else if (!isEmpty(this.args.permalink.user_id)) { + permalinkType = "user"; + permalinkValue = this.args.permalink.user_id; + } + + return { + url: this.args.permalink.url, + [TYPE_TO_FIELD_MAP[permalinkType]]: permalinkValue, + permalinkType, + }; + } else { + return { + permalinkType: "topic", + }; + } + } + + get header() { + return this.isUpdate + ? "admin.permalink.form.edit_header" + : "admin.permalink.form.add_header"; + } + + @action + async save(data) { + this.isUpdate ? await this.update(data) : await this.create(data); + } + + @bind + async create(data) { + try { + const result = await this.store.createRecord("permalink").save({ + url: data.url, + permalink_type: data.permalinkType, + permalink_type_value: this.valueForPermalinkType(data), + }); + this.adminPermalinks.model.unshift(Permalink.create(result.payload)); + this.router.transitionTo("adminPermalinks"); + } catch (error) { + popupAjaxError(error); + } + } + + @bind + async update(data) { + try { + const result = await this.store.update( + "permalink", + this.args.permalink.id, + { + url: data.url, + permalink_type: data.permalinkType, + permalink_type_value: this.valueForPermalinkType(data), + } + ); + const index = this.adminPermalinks.model.findIndex( + (permalink) => permalink.id === this.args.permalink.id + ); + this.adminPermalinks.model[index] = Permalink.create(result.payload); + this.router.transitionTo("adminPermalinks"); + } catch (error) { + popupAjaxError(error); + } + } + + valueForPermalinkType(data) { + return data[TYPE_TO_FIELD_MAP[data.permalinkType]]; + } + + +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js b/app/assets/javascripts/admin/addon/controllers/admin-permalinks-index.js similarity index 65% rename from app/assets/javascripts/admin/addon/controllers/admin-permalinks.js rename to app/assets/javascripts/admin/addon/controllers/admin-permalinks-index.js index 854ccbe45b1..139124f758b 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-permalinks-index.js @@ -9,8 +9,9 @@ import discourseDebounce from "discourse-common/lib/debounce"; import I18n from "discourse-i18n"; import Permalink from "admin/models/permalink"; -export default class AdminPermalinksController extends Controller { +export default class AdminPermalinksIndexController extends Controller { @service dialog; + @service toasts; loading = false; filter = null; @@ -29,34 +30,29 @@ export default class AdminPermalinksController extends Controller { discourseDebounce(this, this._debouncedShow, INPUT_DELAY); } - @action - recordAdded(arg) { - this.model.unshiftObject(arg); - } - @action copyUrl(pl) { let linkElement = document.querySelector(`#admin-permalink-${pl.id}`); clipboardCopy(linkElement.textContent); + this.toasts.success({ + duration: 3000, + data: { + message: I18n.t("admin.permalink.copy_success"), + }, + }); } @action - destroyRecord(record) { - return this.dialog.yesNoConfirm({ + destroyRecord(permalink) { + this.dialog.yesNoConfirm({ message: I18n.t("admin.permalink.delete_confirm"), - didConfirm: () => { - return record.destroy().then( - (deleted) => { - if (deleted) { - this.model.removeObject(record); - } else { - this.dialog.alert(I18n.t("generic_error")); - } - }, - function () { - this.dialog.alert(I18n.t("generic_error")); - } - ); + didConfirm: async () => { + try { + await this.store.destroyRecord("permalink", permalink); + this.model.removeObject(permalink); + } catch { + this.dialog.alert(I18n.t("generic_error")); + } }, }); } diff --git a/app/assets/javascripts/admin/addon/models/permalink.js b/app/assets/javascripts/admin/addon/models/permalink.js index 21bafb83de9..af4ef5bdefd 100644 --- a/app/assets/javascripts/admin/addon/models/permalink.js +++ b/app/assets/javascripts/admin/addon/models/permalink.js @@ -1,10 +1,10 @@ -import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import DiscourseURL from "discourse/lib/url"; import Category from "discourse/models/category"; +import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; -export default class Permalink extends EmberObject { +export default class Permalink extends RestModel { static findAll(filter) { return ajax("/admin/permalinks.json", { data: { filter } }).then(function ( permalinks @@ -13,17 +13,6 @@ export default class Permalink extends EmberObject { }); } - save() { - return ajax("/admin/permalinks.json", { - type: "POST", - data: { - url: this.url, - permalink_type: this.permalink_type, - permalink_type_value: this.permalink_type_value, - }, - }); - } - @discourseComputed("category_id") category(category_id) { return Category.findById(category_id); @@ -34,9 +23,8 @@ export default class Permalink extends EmberObject { return !DiscourseURL.isInternal(external_url); } - destroy() { - return ajax("/admin/permalinks/" + this.id + ".json", { - type: "DELETE", - }); + @discourseComputed("url") + key(url) { + return url.replace("/", "_"); } } diff --git a/app/assets/javascripts/admin/addon/routes/admin-permalinks-edit.js b/app/assets/javascripts/admin/addon/routes/admin-permalinks-edit.js new file mode 100644 index 00000000000..49d77e1d42c --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-permalinks-edit.js @@ -0,0 +1,10 @@ +import { service } from "@ember/service"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default class AdminPermalinksEditRoute extends DiscourseRoute { + @service store; + + model(params) { + return this.store.find("permalink", params.permalink_id); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 6c76f9b7098..c68f82ae59f 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -81,10 +81,14 @@ export default function () { this.route("settings"); } ); - this.route("adminPermalinks", { - path: "/permalinks", - resetNamespace: true, - }); + this.route( + "adminPermalinks", + { path: "/permalinks", resetNamespace: true }, + function () { + this.route("new"); + this.route("edit", { path: "/:permalink_id" }); + } + ); this.route("adminEmbedding", { path: "/embedding", resetNamespace: true, diff --git a/app/assets/javascripts/admin/addon/templates/permalinks-edit.hbs b/app/assets/javascripts/admin/addon/templates/permalinks-edit.hbs new file mode 100644 index 00000000000..f74b91cf898 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/permalinks-edit.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/permalinks-index.hbs b/app/assets/javascripts/admin/addon/templates/permalinks-index.hbs new file mode 100644 index 00000000000..6375d393f42 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/permalinks-index.hbs @@ -0,0 +1,135 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/permalinks-new.hbs b/app/assets/javascripts/admin/addon/templates/permalinks-new.hbs new file mode 100644 index 00000000000..824a49fedef --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/permalinks-new.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/permalinks.hbs b/app/assets/javascripts/admin/addon/templates/permalinks.hbs deleted file mode 100644 index 135b614467f..00000000000 --- a/app/assets/javascripts/admin/addon/templates/permalinks.hbs +++ /dev/null @@ -1,88 +0,0 @@ -

{{i18n "admin.permalink.title"}}

- - - - - - - - - \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/conditional-content/content.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/conditional-content/content.gjs index a829796a695..eb31e5c04f2 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/conditional-content/content.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/conditional-content/content.gjs @@ -1,11 +1,15 @@ -import { eq } from "truth-helpers"; +import { notEq } from "truth-helpers"; +import concatClass from "discourse/helpers/concat-class"; const FKControlConditionalContentItem = ; export default FKControlConditionalContentItem; diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs index 94344604783..ba5a8a54a3b 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs @@ -1,17 +1,15 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; import { concat } from "@ember/helper"; import { action } from "@ember/object"; +import { isBlank } from "@ember/utils"; import UppyImageUploader from "discourse/components/uppy-image-uploader"; export default class FKControlImage extends Component { static controlType = "image"; - @tracked imageUrl = this.args.value; @action setImage(upload) { this.args.field.set(upload); - this.imageUrl = upload?.url; } @action @@ -19,6 +17,10 @@ export default class FKControlImage extends Component { this.setImage(undefined); } + get imageUrl() { + return isBlank(this.args.value) ? null : this.args.value; + } +