DEV: Convert admin emojis UI to new layout (#29615)

This PR converts the custom emoji UI in the admin pages to follow the new admin UI guidelines.
This commit is contained in:
Ted Johansson 2024-11-11 15:04:15 +08:00 committed by GitHub
parent 4ad83a98a1
commit 189d98f3ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 339 additions and 169 deletions

View File

@ -0,0 +1,72 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import { sort } from "@ember/object/computed";
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "discourse-i18n";
const ALL_FILTER = "all";
export default class AdminEmojisIndexController extends Controller {
@service dialog;
filter = null;
sorting = null;
@sort("filteredEmojis.[]", "sorting") sortedEmojis;
init() {
super.init(...arguments);
this.setProperties({
filter: ALL_FILTER,
sorting: ["group", "name"],
});
}
@computed("model.[]", "filter")
get filteredEmojis() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
}
@computed("model.[]")
get emojiGroups() {
return this.model.mapBy("group").uniq();
}
@computed("emojiGroups.[]")
get sortingGroups() {
return [ALL_FILTER].concat(this.emojiGroups);
}
@action
filterGroups(value) {
this.set("filter", value);
}
@action
destroyEmoji(emoji) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.emoji.delete_confirm", {
name: emoji.get("name"),
}),
didConfirm: () => this.#destroyEmoji(emoji),
});
}
async #destroyEmoji(emoji) {
try {
await ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE",
});
this.model.removeObject(emoji);
} catch (err) {
popupAjaxError(err);
}
}
}

View File

@ -0,0 +1,29 @@
import Controller from "@ember/controller";
import EmberObject, { action, computed } from "@ember/object";
import { service } from "@ember/service";
const ALL_FILTER = "all";
export default class AdminEmojisNewController extends Controller {
@service router;
@service currentUser;
@computed("model")
get emojiGroups() {
return this.model.mapBy("group").uniq();
}
@computed("emojiGroups.[]")
get sortingGroups() {
return [ALL_FILTER].concat(this.emojiGroups);
}
@action
emojiUploaded(emoji, group) {
emoji.url += "?t=" + new Date().getTime();
emoji.group = group;
emoji.created_by = this.currentUser.username;
this.model.pushObject(EmberObject.create(emoji));
this.router.transitionTo("adminEmojis.index");
}
}

View File

@ -0,0 +1,11 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
export default class AdminEmojisSettingsController extends Controller {
filter = "";
@action
filterChanged(filterData) {
this.set("filter", filterData.filter);
}
}

View File

@ -1,86 +1,3 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import EmberObject, { action, computed } from "@ember/object";
import { sort } from "@ember/object/computed";
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import I18n from "discourse-i18n";
const ALL_FILTER = "all"; export default class AdminEmojisController extends Controller {}
export default class AdminEmojisController extends Controller {
@service dialog;
filter = null;
sorting = null;
@sort("filteredEmojis.[]", "sorting") sortedEmojis;
init() {
super.init(...arguments);
this.setProperties({
filter: ALL_FILTER,
sorting: ["group", "name"],
});
}
@computed("model")
get emojiGroups() {
return this.model.mapBy("group").uniq();
}
@computed("emojiGroups.[]")
get sortingGroups() {
return [ALL_FILTER].concat(this.emojiGroups);
}
@computed("model.[]", "filter")
get filteredEmojis() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
}
_highlightEmojiList() {
const customEmojiListEl = document.querySelector("#custom_emoji");
if (
customEmojiListEl &&
!customEmojiListEl.classList.contains("highlighted")
) {
customEmojiListEl.classList.add("highlighted");
customEmojiListEl.addEventListener("animationend", () => {
customEmojiListEl.classList.remove("highlighted");
});
}
}
@action
filterGroups(value) {
this.set("filter", value);
}
@action
emojiUploaded(emoji, group) {
emoji.url += "?t=" + new Date().getTime();
emoji.group = group;
this.model.pushObject(EmberObject.create(emoji));
this._highlightEmojiList();
}
@action
destroyEmoji(emoji) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.emoji.delete_confirm", {
name: emoji.get("name"),
}),
didConfirm: () => {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE",
}).then(() => {
this.model.removeObject(emoji);
});
},
});
}
}

View File

@ -0,0 +1,20 @@
import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n";
import SiteSetting from "admin/models/site-setting";
export default class AdminEmojisSettingsRoute extends DiscourseRoute {
queryParams = {
filter: { replace: true },
};
titleToken() {
return I18n.t("settings");
}
async model() {
return {
settings: await SiteSetting.findAll(),
initialFilter: "emoji",
};
}
}

View File

@ -1,8 +1,13 @@
import EmberObject from "@ember/object"; 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";
import I18n from "discourse-i18n";
export default class AdminEmojisRoute extends DiscourseRoute { export default class AdminEmojisRoute extends DiscourseRoute {
titleToken() {
return I18n.t("admin.emoji.title");
}
async model() { async model() {
const emojis = await ajax("/admin/customize/emojis.json"); const emojis = await ajax("/admin/customize/emojis.json");
return emojis.map((emoji) => EmberObject.create(emoji)); return emojis.map((emoji) => EmberObject.create(emoji));

View File

@ -72,7 +72,15 @@ export default function () {
path: "/user_fields", path: "/user_fields",
resetNamespace: true, resetNamespace: true,
}); });
this.route("adminEmojis", { path: "/emojis", resetNamespace: true }); this.route(
"adminEmojis",
{ path: "/emojis", resetNamespace: true },
function () {
this.route("new");
this.route("index", { path: "/" });
this.route("settings");
}
);
this.route("adminPermalinks", { this.route("adminPermalinks", {
path: "/permalinks", path: "/permalinks",
resetNamespace: true, resetNamespace: true,

View File

@ -0,0 +1,63 @@
<div class="form-horizontal">
<div class="inline-form">
<ComboBox
@value={{this.filter}}
@content={{this.sortingGroups}}
@nameProperty={{null}}
@valueProperty={{null}}
@onChange={{action "filterGroups"}}
/>
</div>
</div>
{{#if this.sortedEmojis}}
<table id="custom_emoji" class="d-admin-table">
<thead>
<tr>
<th>{{i18n "admin.emoji.image"}}</th>
<th>{{i18n "admin.emoji.name"}}</th>
<th>{{i18n "admin.emoji.group"}}</th>
<th colspan="3">{{i18n "admin.emoji.created_by"}}</th>
</tr>
</thead>
<tbody>
{{#each this.sortedEmojis as |emoji|}}
<tr class="d-admin-row__content">
<td class="d-admin-row__overview">
<img
class="emoji emoji-custom"
src={{emoji.url}}
title={{emoji.name}}
alt={{i18n "admin.emoji.alt"}}
/>
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.name"}}
</div>
:{{emoji.name}}:
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.group"}}
</div>
{{emoji.group}}
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.created_by"}}
</div>
{{emoji.created_by}}
</td>
<td class="d-admin-row__controls action">
<DButton
@action={{fn this.destroyEmoji emoji}}
@icon="trash-can"
class="btn-small"
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}}

View File

@ -0,0 +1,4 @@
<EmojiUploader
@emojiGroups={{this.emojiGroups}}
@done={{action "emojiUploaded"}}
/>

View File

@ -0,0 +1,9 @@
<DBreadcrumbsItem @path="/admin/emojis/settings" @label={{i18n "settings"}} />
<div class="content-body admin-config-area__settings admin-detail pull-left">
<AdminFilteredSiteSettings
@initialFilter={{@model.initialFilter}}
@settings={{@model.settings}}
@onFilterChanged={{this.filterChanged}}
/>
</div>

View File

@ -1,86 +1,32 @@
<div class="admin-emojis"> <div class="admin-emojis admin-config-page">
<div class="admin-emojis__header"> <AdminPageHeader
<h1>{{i18n "admin.emoji.title"}}</h1> @titleLabel="admin.emoji.title"
<LinkTo @descriptionLabel="admin.emoji.description"
@route="adminSiteSettingsCategory"
@model="all_results"
@query={{hash filter="emoji"}}
> >
{{i18n "admin.emoji.settings"}} <:breadcrumbs>
</LinkTo> <DBreadcrumbsItem
</div> @path="/admin/customize/emojis"
@label={{i18n "admin.emoji.title"}}
<p class="desc">{{i18n "admin.emoji.help"}}</p>
<EmojiUploader
@emojiGroups={{this.emojiGroups}}
@done={{action "emojiUploaded"}}
/> />
</:breadcrumbs>
<hr /> <:actions as |actions|>
<actions.Primary @route="adminEmojis.new" @label="admin.emoji.new" />
<div class="form-horizontal"> </:actions>
<div class="inline-form"> <:tabs>
<label class="label">Show</label> <NavItem
<ComboBox @route="adminEmojis.settings"
@value={{this.filter}} @label="settings"
@content={{this.sortingGroups}} class="admin-emojis-tabs__settings"
@nameProperty={{null}}
@valueProperty={{null}}
@onChange={{action "filterGroups"}}
/> />
</div> <NavItem
</div> @route="adminEmojis.index"
@label="admin.emoji.title"
class="admin-emojis-tabs__emoji"
/>
</:tabs>
</AdminPageHeader>
{{#if this.sortedEmojis}} <div class="admin-container admin-config-page__main-area">
<table id="custom_emoji" class="d-admin-table"> {{outlet}}
<thead>
<tr>
<th>{{i18n "admin.emoji.image"}}</th>
<th>{{i18n "admin.emoji.name"}}</th>
<th>{{i18n "admin.emoji.group"}}</th>
<th colspan="3">{{i18n "admin.emoji.created_by"}}</th>
</tr>
</thead>
<tbody>
{{#each this.sortedEmojis as |e|}}
<tr class="d-admin-row__content">
<td class="d-admin-row__overview">
<img
class="emoji emoji-custom"
src={{e.url}}
title={{e.name}}
alt={{i18n "admin.emoji.alt"}}
/>
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.name"}}
</div> </div>
:{{e.name}}:
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.group"}}
</div>
{{e.group}}
</td>
<td class="d-admin-row__detail">
<div class="d-admin-row__mobile-label">
{{i18n "admin.emoji.created_by"}}
</div>
{{e.created_by}}
</td>
<td class="d-admin-row__controls action">
<DButton
@action={{fn this.destroyEmoji e}}
@icon="trash-can"
class="btn-danger"
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
</div> </div>

View File

@ -7204,7 +7204,8 @@ en:
emoji: emoji:
title: "Emoji" title: "Emoji"
help: "Add new emoji that will be available to everyone. Drag and drop multiple files at once without entering a name to create emojis using their file names. The selected group will be used for all files that are added at the same time. You can also click 'Add New Emoji' to open the file picker." description: "Add new emoji that will be available to everyone. Drag and drop multiple files at once without entering a name to create emojis using their file names. The selected group will be used for all files that are added at the same time. You can also click 'Add New Emoji' to open the file picker."
new: "Add"
add: "Add New Emoji" add: "Add New Emoji"
choose_files: "Choose Files" choose_files: "Choose Files"
uploading: "Uploading…" uploading: "Uploading…"

View File

@ -246,6 +246,8 @@ Discourse::Application.routes.draw do
only: %i[index create update destroy], only: %i[index create update destroy],
constraints: AdminConstraint.new constraints: AdminConstraint.new
resources :emojis, only: %i[index create destroy], constraints: AdminConstraint.new resources :emojis, only: %i[index create destroy], constraints: AdminConstraint.new
get "emojis/new" => "emojis#index"
get "emojis/settings" => "emojis#index"
resources :form_templates, constraints: AdminConstraint.new, path: "/form-templates" do resources :form_templates, constraints: AdminConstraint.new, path: "/form-templates" do
collection { get "preview" => "form_templates#preview" } collection { get "preview" => "form_templates#preview" }
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:custom_emoji) do
upload { Fabricate(:image_upload) }
name { "joffrey_facepalm" }
end

View File

@ -0,0 +1,33 @@
#frozen_string_literal: true
describe "Admin Customize Emoji Page", type: :system do
fab!(:current_user) { Fabricate(:admin) }
let(:emojis_page) { PageObjects::Pages::AdminEmojis.new }
let(:dialog) { PageObjects::Components::Dialog.new }
let(:settings_page) { PageObjects::Pages::AdminSiteSettings.new }
before do
Fabricate(:custom_emoji)
sign_in(current_user)
end
it "shows a list of custom emojis" do
emojis_page.visit_page
expect(emojis_page).to have_emoji_listed("joffrey_facepalm")
end
it "can delete a custom emoji" do
emojis_page.visit_page
emojis_page.delete_emoji("joffrey_facepalm")
dialog.click_yes
expect(emojis_page).to have_no_emoji_listed("joffrey_facepalm")
end
it "can see emoji site settings" do
emojis_page.visit_page
emojis_page.click_tab("settings")
expect(settings_page).to have_setting("enable_emoji")
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminEmojis < PageObjects::Pages::Base
def visit_page
page.visit "/admin/customize/emojis"
self
end
def click_tab(tab_name)
case tab_name
when "settings"
find(".admin-emojis-tabs__settings").click
when "index"
find(".admin-emojis-tabs__emoji").click
end
end
def has_emoji_listed?(name)
page.has_css?(emoji_table_selector, text: name)
end
def has_no_emoji_listed?(name)
page.has_no_css?(emoji_table_selector, text: name)
end
def delete_emoji(name)
find(".d-admin-row__content", text: name).find(delete_button_selector).click
end
private
def emoji_table_selector
"#custom_emoji"
end
def delete_button_selector
".d-icon-trash-can"
end
end
end
end