diff --git a/locales/index.d.ts b/locales/index.d.ts index 7c440d9119..60b44e352b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -260,6 +260,7 @@ export interface Locale { "removed": string; "removeAreYouSure": string; "deleteAreYouSure": string; + "undraftAreYouSure": string; "resetAreYouSure": string; "saved": string; "messaging": string; @@ -1023,6 +1024,7 @@ export interface Locale { "notesSearchNotAvailable": string; "license": string; "draft": string; + "undrafted": string; "unfavoriteConfirm": string; "myClips": string; "drivecleaner": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index dcf413be13..4d35a0263c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -257,6 +257,7 @@ remove: "削除" removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" +undraftAreYouSure: "「{x}」をドラフト解除しますか?" resetAreYouSure: "リセットしますか?" saved: "保存しました" messaging: "チャット" @@ -1022,6 +1023,7 @@ sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キ notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" draft: "ドラフト" +undrafted: "ドラフト解除" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" drivecleaner: "ドライブクリーナー" diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ab16d86a3d..8fba829c5e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -64,6 +64,7 @@ export const paramDef = { type: 'object', properties: { query: { type: 'string', nullable: true, default: null }, + draft: { type: 'boolean', nullable: true, default: null }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -86,6 +87,14 @@ export default class extends Endpoint { // eslint- let emojis: MiEmoji[]; + if (ps.draft !== null) { + if (ps.draft) { + q.andWhere('emoji.draft = TRUE'); + } else { + q.andWhere('emoji.draft = FALSE'); + } + } + if (ps.query) { //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index ecbdae9ed8..5dcd2009ed 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -51,6 +51,41 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ + + + +
+
@@ -89,14 +124,15 @@ import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSplit from '@/components/form/split.vue'; -import { selectFile, selectFiles } from '@/scripts/select-file.js'; +import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const emojisPaginationComponent = shallowRef>(); +const emojisDraftPaginationComponent = shallowRef>(); -const tab = ref('local'); +const tab = ref('draft'); const query = ref(null); const queryRemote = ref(null); const host = ref(null); @@ -111,6 +147,15 @@ const pagination = { })), }; +const paginationDraft = { + endpoint: 'admin/emoji/list' as const, + limit: 30, + params: computed(() => ({ + query: (query.value && query.value !== '') ? query.value : null, + draft: true, + })), +}; + const remotePagination = { endpoint: 'admin/emoji/list-remote' as const, limit: 30, @@ -166,6 +211,61 @@ const edit = (emoji) => { }, 'closed'); }; +const editDraft = (emoji) => { + os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + emoji: emoji, + isRequest: false, + }, { + done: result => { + if (result.updated) { + emojisDraftPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ + ...oldEmoji, + ...result.updated, + })); + emojisDraftPaginationComponent.value.reload(); + } else if (result.deleted) { + emojisDraftPaginationComponent.value.removeItem((item) => item.id === emoji.id); + } + }, + }, 'closed'); +}; + +async function undrafted(emoji) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('undraftAreYouSure', { x: emoji.name }), + }); + if (canceled) return; + + await os.api('admin/emoji/update', { + id: emoji.id, + name: emoji.name, + category: emoji.category, + aliases: emoji.aliases, + license: emoji.license, + draft: false, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + }); + + emojisDraftPaginationComponent.value.removeItem((item) => item.id === emoji.id); +} + +async function deleteDraft(emoji) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('removeAreYouSure', { x: emoji.name }), + }); + if (canceled) return; + + os.api('admin/emoji/delete', { + id: emoji.id, + }).then(() => { + emojisDraftPaginationComponent.value.removeItem((item) => item.id === emoji.id); + }); +} + const im = (emoji) => { os.apiWithDialog('admin/emoji/copy', { emojiId: emoji.id, @@ -308,6 +408,9 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ + key: 'draft', + title: i18n.ts.draftEmojis, +}, { key: 'local', title: i18n.ts.local, }, { @@ -374,6 +477,122 @@ definePageMetadata(computed(() => ({ } } } + > .draft { + .empty { + margin: var(--margin); + } + + .ldhfsamy { + > .emoji { + display: grid; + grid-template-rows: 40px 1fr; + grid-template-columns: 1fr 150px; + align-items: center; + padding: 11px; + text-align: left; + border: solid 1px var(--panel); + width: 100%; + margin: 10px; + + > .img { + display: grid; + grid-row: 1; + grid-column: 1/ span 2; + grid-template-columns: 50% 50%; + place-content: center; + place-items: center; + + > .imgLight { + display: grid; + grid-column: 1; + background-color: #fff; + + > img { + max-height: 30px; + max-width: 100%; + } + } + + > .imgDark { + display: grid; + grid-column: 2; + background-color: #000; + + > img { + max-height: 30px; + max-width: 100%; + } + } + } + + > .info { + display: grid; + grid-row: 2; + grid-template-rows: 30px 30px 30px; + + > .name { + grid-row: 1; + text-overflow: ellipsis; + overflow: hidden; + } + + > .category { + grid-row: 2; + text-overflow: ellipsis; + overflow: hidden; + } + + > .aliases { + grid-row: 3; + text-overflow: ellipsis; + overflow: hidden; + } + + > .license { + grid-row: 4; + text-overflow: ellipsis; + overflow: hidden; + } + } + + > .edit-button { + display: grid; + grid-row: 2; + grid-template-rows: 30px 30px 30px; + + > .edit { + grid-row: 1; + background-color: var(--buttonBg); + margin: 2px; + + &:hover { + color: var(--accent); + } + } + + > .draft { + grid-row: 2; + background-color: var(--buttonBg); + margin: 2px; + + &:hover { + color: var(--accent); + } + } + + > .delete { + background-color: var(--buttonBg); + grid-row: 3; + margin: 2px; + + &:hover { + color: var(--accent); + } + } + } + } + } + } > .remote { .empty {