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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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,33 +1,39 @@
<div class="emoji-uploader form-horizontal">
<div class="control-group">
<span class="label">
<div class="form-kit">
<div class="form-kit__container form-kit__field form-kit__field-input-text">
<label class="form-kit__container-title">
{{i18n "admin.emoji.name"}}
</span>
<div class="input">
<Input
id="emoji-name"
name="name"
placeholder={{i18n "admin.emoji.name"}}
@value={{readonly this.name}}
{{on "input" (with-event-value (fn (mut this.name)))}}
/>
</label>
<div class="form-kit__container-content --large">
<div class="form-kit__control-input-wrapper">
<Input
id="emoji-name"
class="form-kit__control-input"
name="name"
@value={{readonly this.name}}
{{on "input" (with-event-value (fn (mut this.name)))}}
/>
</div>
</div>
</div>
<div class="control-group">
<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"}}
</span>
<div class="input">
<ComboBox
@name="group"
@id="emoji-group-selector"
@value={{this.group}}
@content={{this.newEmojiGroups}}
@onChange={{action "createEmojiGroup"}}
@valueProperty={{null}}
@nameProperty={{null}}
@options={{hash allowAny=true}}
/>
</label>
<div class="form-kit__container-content --large">
<div class="form-kit__control-input-wrapper">
<ComboBox
@name="group"
@id="emoji-group-selector"
@value={{this.group}}
@content={{this.newEmojiGroups}}
@onChange={{action "createEmojiGroup"}}
@valueProperty={{null}}
@nameProperty={{null}}
@options={{hash allowAny=true}}
/>
</div>
</div>
</div>
<div class="control-group">
@ -44,7 +50,7 @@
@translatedLabel={{this.buttonLabel}}
@action={{this.chooseFiles}}
@disabled={{this.uppyUpload.uploading}}
class="btn-default"
class="btn-primary"
/>
</div>
</div>

View File

@ -76,7 +76,7 @@ export default class EmojiUploader extends Component {
if (uploading) {
return `${I18n.t("admin.emoji.uploading")} ${uploadProgress}%`;
} 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 {
filter = "";
queryParams = ["filter"];
@action
filterChanged(filterData) {
filterChangedCallback(filterData) {
this.set("filter", filterData.filter);
}
}

View File

@ -1,3 +1,10 @@
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 I18n from "discourse-i18n";
import SiteSetting from "admin/models/site-setting";
export default class AdminEmojisSettingsRoute extends DiscourseRoute {
queryParams = {
@ -10,11 +9,4 @@ export default class AdminEmojisSettingsRoute extends DiscourseRoute {
titleToken() {
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 I18n from "discourse-i18n";
@ -7,9 +5,4 @@ export default class AdminEmojisRoute extends DiscourseRoute {
titleToken() {
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">
<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}}
<AdminConfigAreas::EmojisList />

View File

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

View File

@ -1,9 +1,4 @@
<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>
<AdminConfigAreas::EmojisSettings
@onFilterChanged={{this.filterChangedCallback}}
@initialFilter={{this.filter}}
/>

View File

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

View File

@ -5,9 +5,6 @@
#custom_emoji td.action {
text-align: right;
}
#emoji-uploader {
transition: box-shadow ease-in-out 0.25s;
}
#custom_emoji.highlighted {
background: var(--tertiary-very-low);
@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-row__content td {
vertical-align: middle;

View File

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

View File

@ -7218,10 +7218,11 @@ en:
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."
new: "Add emoji"
add: "Add New Emoji"
choose_files: "Choose Files"
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."
no_emoji: "You don't have any custom emoji yet."
add: "Add emoji"
back: "Back to emoji"
choose_files: "Choose files"
uploading: "Uploading…"
name: "Name"
group: "Group"

View File

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