DEV: Modernize admin emoji JavaScript (#29714)

app/assets/javascripts/admin/addon/templates/emojis.hbs
This commit is contained in:
Ted Johansson
2024-11-19 15:44:34 +08:00
committed by GitHub
parent 01a160d8af
commit d96b8d1001
20 changed files with 338 additions and 257 deletions

View File

@ -0,0 +1,98 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import i18n from "discourse-common/helpers/i18n";
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import ComboBox from "select-kit/components/combo-box";
export default class AdminConfigAreasEmojisList extends Component {
@service dialog;
@service adminEmojis;
get emojis() {
return this.adminEmojis.emojis;
}
get sortedEmojis() {
return this.adminEmojis.sortedEmojis;
}
get filteringGroups() {
return this.adminEmojis.filteringGroups;
}
<template>
<div class="form-horizontal">
<div class="inline-form">
<ComboBox
@value={{this.adminEmojis.filter}}
@content={{this.filteringGroups}}
@nameProperty={{null}}
@valueProperty={{null}}
/>
</div>
</div>
{{#if this.emojis}}
<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>
{{#if this.sortedEmojis}}
<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.adminEmojis.destroyEmoji emoji}}
@icon="trash-can"
class="btn-small"
/>
</td>
</tr>
{{/each}}
</tbody>
{{/if}}
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="admin.emoji.add"
@ctaRoute="adminEmojis.new"
@ctaClass="admin-emojis__add-emoji"
@emptyLabel="admin.emoji.no_emoji"
/>
{{/if}}
</template>
}

View File

@ -0,0 +1,44 @@
import Component from "@glimmer/component";
import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import BackButton from "discourse/components/back-button";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
import EmojiUploader from "admin/components/emoji-uploader";
export default class AdminConfigAreasEmojisNew extends Component {
@service router;
@service currentUser;
@service adminEmojis;
get emojiGroups() {
return this.adminEmojis.emojiGroups;
}
@action
emojiUploaded(emoji, group) {
emoji.url += "?t=" + new Date().getTime();
emoji.group = group;
emoji.created_by = this.currentUser.username;
this.adminEmojis.emojis = [
...this.adminEmojis.emojis,
EmberObject.create(emoji),
];
this.router.transitionTo("adminEmojis.index");
}
<template>
<BackButton @route="adminEmojis.index" @label="admin.emoji.back" />
<div class="admin-config-area">
<div class="admin-config-area__primary-content admin-emojis-form">
<AdminConfigAreaCard @heading="admin.emoji.add">
<:content>
<EmojiUploader
@emojiGroups={{this.emojiGroups}}
@done={{this.emojiUploaded}}
/>
</:content>
</AdminConfigAreaCard>
</div>
</div>
</template>
}

View File

@ -0,0 +1,54 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import { ajax } from "discourse/lib/ajax";
import i18n from "discourse-common/helpers/i18n";
import { bind } from "discourse-common/utils/decorators";
import AdminFilteredSiteSettings from "admin/components/admin-filtered-site-settings";
import SiteSetting from "admin/models/site-setting";
export default class AdminConfigAreasEmojisSettings extends Component {
@service siteSettings;
@tracked settings;
@bind
loadSettings() {
ajax("/admin/config/site_settings.json", {
data: {
filter_area: "emojis",
},
}).then((result) => {
this.settings = [
{
name: "All",
nameKey: "all_results",
siteSettings: result.site_settings.map((setting) =>
SiteSetting.create(setting)
),
},
];
});
}
<template>
<DBreadcrumbsItem
@path="/admin/config/emojis/settings"
@label={{i18n "settings"}}
/>
<div
class="content-body admin-config-area__settings admin-detail pull-left"
{{didInsert this.loadSettings}}
>
{{#if this.settings}}
<AdminFilteredSiteSettings
@initialFilter={{@initialFilter}}
@onFilterChanged={{@onFilterChanged}}
@settings={{this.settings}}
/>
{{/if}}
</div>
</template>
}

View File

@ -1,23 +1,28 @@
<div class="emoji-uploader form-horizontal"> <div class="form-kit">
<div class="control-group"> <div class="form-kit__container form-kit__field form-kit__field-input-text">
<span class="label"> <label class="form-kit__container-title">
{{i18n "admin.emoji.name"}} {{i18n "admin.emoji.name"}}
</span> </label>
<div class="input"> <div class="form-kit__container-content --large">
<div class="form-kit__control-input-wrapper">
<Input <Input
id="emoji-name" id="emoji-name"
class="form-kit__control-input"
name="name" name="name"
placeholder={{i18n "admin.emoji.name"}}
@value={{readonly this.name}} @value={{readonly this.name}}
{{on "input" (with-event-value (fn (mut this.name)))}} {{on "input" (with-event-value (fn (mut this.name)))}}
/> />
</div> </div>
</div> </div>
<div class="control-group"> </div>
<span class="label"> <div
class="form-kit__container form-kit__field form-kit__field-input-combo-box"
>
<label class="form-kit__container-title">
{{i18n "admin.emoji.group"}} {{i18n "admin.emoji.group"}}
</span> </label>
<div class="input"> <div class="form-kit__container-content --large">
<div class="form-kit__control-input-wrapper">
<ComboBox <ComboBox
@name="group" @name="group"
@id="emoji-group-selector" @id="emoji-group-selector"
@ -30,6 +35,7 @@
/> />
</div> </div>
</div> </div>
</div>
<div class="control-group"> <div class="control-group">
<div class="input"> <div class="input">
<input <input
@ -44,7 +50,7 @@
@translatedLabel={{this.buttonLabel}} @translatedLabel={{this.buttonLabel}}
@action={{this.chooseFiles}} @action={{this.chooseFiles}}
@disabled={{this.uppyUpload.uploading}} @disabled={{this.uppyUpload.uploading}}
class="btn-default" class="btn-primary"
/> />
</div> </div>
</div> </div>

View File

@ -76,7 +76,7 @@ export default class EmojiUploader extends Component {
if (uploading) { if (uploading) {
return `${I18n.t("admin.emoji.uploading")} ${uploadProgress}%`; return `${I18n.t("admin.emoji.uploading")} ${uploadProgress}%`;
} else { } else {
return I18n.t("admin.emoji.add"); return I18n.t("admin.emoji.choose_files");
} }
} }

View File

@ -1,72 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -3,9 +3,10 @@ import { action } from "@ember/object";
export default class AdminEmojisSettingsController extends Controller { export default class AdminEmojisSettingsController extends Controller {
filter = ""; filter = "";
queryParams = ["filter"];
@action @action
filterChanged(filterData) { filterChangedCallback(filterData) {
this.set("filter", filterData.filter); this.set("filter", filterData.filter);
} }
} }

View File

@ -1,3 +1,10 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { service } from "@ember/service";
export default class AdminEmojisController extends Controller {} export default class AdminEmojisController extends Controller {
@service router;
get hideTabs() {
return ["adminEmojis.new"].includes(this.router.currentRouteName);
}
}

View File

@ -1,6 +1,5 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import SiteSetting from "admin/models/site-setting";
export default class AdminEmojisSettingsRoute extends DiscourseRoute { export default class AdminEmojisSettingsRoute extends DiscourseRoute {
queryParams = { queryParams = {
@ -10,11 +9,4 @@ export default class AdminEmojisSettingsRoute extends DiscourseRoute {
titleToken() { titleToken() {
return I18n.t("settings"); return I18n.t("settings");
} }
async model() {
return {
settings: await SiteSetting.findAll(),
initialFilter: "emoji",
};
}
} }

View File

@ -1,5 +1,3 @@
import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
@ -7,9 +5,4 @@ export default class AdminEmojisRoute extends DiscourseRoute {
titleToken() { titleToken() {
return I18n.t("admin.emoji.title"); return I18n.t("admin.emoji.title");
} }
async model() {
const emojis = await ajax("/admin/customize/emojis.json");
return emojis.map((emoji) => EmberObject.create(emoji));
}
} }

View File

@ -0,0 +1,74 @@
import { tracked } from "@glimmer/tracking";
import EmberObject, { action } from "@ember/object";
import Service, { 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";
const DEFAULT_GROUP = "default";
export default class AdminEmojis extends Service {
@service dialog;
@tracked emojis = [];
@tracked filter = ALL_FILTER;
@tracked sorting = ["group", "name"];
constructor() {
super(...arguments);
this.#fetchEmojis();
}
get filteredEmojis() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.emojis;
} else {
return this.emojis.filter((e) => e.group === this.filter);
}
}
get sortedEmojis() {
return this.filteredEmojis.sort((a, b) => a.name.localeCompare(b.name));
}
get emojiGroups() {
return [DEFAULT_GROUP].concat(this.emojis.map((e) => e.group)).uniq();
}
get filteringGroups() {
return [ALL_FILTER].concat(this.emojiGroups);
}
async #fetchEmojis() {
try {
const data = await ajax("/admin/customize/emojis.json");
this.emojis = data.map((emoji) => EmberObject.create(emoji));
} catch (err) {
popupAjaxError(err);
}
}
@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.emojis.removeObject(emoji);
} catch (err) {
popupAjaxError(err);
}
}
}

View File

@ -1,63 +1 @@
<div class="form-horizontal"> <AdminConfigAreas::EmojisList />
<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

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

View File

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

View File

@ -2,6 +2,7 @@
<AdminPageHeader <AdminPageHeader
@titleLabel="admin.emoji.title" @titleLabel="admin.emoji.title"
@descriptionLabel="admin.emoji.description" @descriptionLabel="admin.emoji.description"
@hideTabs={{this.hideTabs}}
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem <DBreadcrumbsItem
@ -10,7 +11,7 @@
/> />
</:breadcrumbs> </:breadcrumbs>
<:actions as |actions|> <:actions as |actions|>
<actions.Primary @route="adminEmojis.new" @label="admin.emoji.new" /> <actions.Primary @route="adminEmojis.new" @label="admin.emoji.add" />
</:actions> </:actions>
<:tabs> <:tabs>
<NavItem <NavItem

View File

@ -5,9 +5,6 @@
#custom_emoji td.action { #custom_emoji td.action {
text-align: right; text-align: right;
} }
#emoji-uploader {
transition: box-shadow ease-in-out 0.25s;
}
#custom_emoji.highlighted { #custom_emoji.highlighted {
background: var(--tertiary-very-low); background: var(--tertiary-very-low);
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
@ -15,31 +12,6 @@
} }
} }
.emoji-uploader.form-horizontal {
padding: var(--space-3);
margin-top: var(--space-2);
background: var(--primary-very-low);
display: flex;
gap: var(--space-3);
flex-direction: row;
align-items: end;
@include breakpoint("tablet") {
flex-direction: column;
align-items: normal;
}
.control-group {
margin-bottom: 0;
}
.label {
font-weight: bold;
margin-right: var(--space-2);
color: var(--primary-high);
}
}
.d-admin-table { .d-admin-table {
.d-admin-row__content td { .d-admin-row__content td {
vertical-align: middle; vertical-align: middle;

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class SiteSetting < ActiveRecord::Base class SiteSetting < ActiveRecord::Base
VALID_AREAS = %w[flags about] VALID_AREAS = %w[flags about emojis]
extend GlobalPath extend GlobalPath
extend SiteSettingExtension extend SiteSettingExtension

View File

@ -7218,10 +7218,11 @@ en:
emoji: emoji:
title: "Emoji" title: "Emoji"
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." description: "Add new emoji that will be available to everyone. Select multiple files to create emojis using their file names. The selected group will be used for all files that are added at the same time."
new: "Add emoji" no_emoji: "You don't have any custom emoji yet."
add: "Add New Emoji" add: "Add emoji"
choose_files: "Choose Files" back: "Back to emoji"
choose_files: "Choose files"
uploading: "Uploading…" uploading: "Uploading…"
name: "Name" name: "Name"
group: "Group" group: "Group"

View File

@ -909,7 +909,9 @@ posting:
client: true client: true
default: 2 default: 2
min: 1 min: 1
max_emojis_in_title: 1 max_emojis_in_title:
default: 1
area: "emojis"
allow_uncategorized_topics: allow_uncategorized_topics:
client: true client: true
default: false default: false
@ -1097,18 +1099,22 @@ posting:
enable_emoji: enable_emoji:
default: true default: true
client: true client: true
area: "emojis"
enable_emoji_shortcuts: enable_emoji_shortcuts:
default: true default: true
client: true client: true
area: "emojis"
emoji_set: emoji_set:
default: "twitter" default: "twitter"
client: true client: true
enum: "EmojiSetSiteSetting" enum: "EmojiSetSiteSetting"
area: "emojis"
emoji_autocomplete_min_chars: emoji_autocomplete_min_chars:
client: true client: true
default: 0 default: 0
locale_default: locale_default:
fr: 1 fr: 1
area: "emojis"
enable_inline_emoji_translation: enable_inline_emoji_translation:
client: true client: true
default: false default: false
@ -1117,11 +1123,13 @@ posting:
zh_TW: true zh_TW: true
ja: true ja: true
ko: true ko: true
area: "emojis"
emoji_deny_list: emoji_deny_list:
type: emoji_list type: emoji_list
default: "" default: ""
client: true client: true
refresh: true refresh: true
area: "emojis"
approve_post_count: approve_post_count:
default: 0 default: 0
approve_unless_trust_level: approve_unless_trust_level:
@ -1674,6 +1682,7 @@ files:
external_emoji_url: external_emoji_url:
default: "" default: ""
client: true client: true
area: "emojis"
restrict_letter_avatar_colors: restrict_letter_avatar_colors:
default: "" default: ""
type: list type: list