From 21bee2ffde1dc0e2512c9cb8179060caf7747d8d Mon Sep 17 00:00:00 2001
From: tar_bin <tar.bin.master@gmail.com>
Date: Fri, 11 Aug 2023 23:02:54 +0900
Subject: [PATCH] =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0=E7=B5=B5?=
 =?UTF-8?q?=E6=96=87=E5=AD=97=E3=81=AE=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9?=
 =?UTF-8?q?=E3=83=88=E6=89=BF=E8=AA=8D=E7=94=A8=E7=94=BB=E9=9D=A2=E3=81=AE?=
 =?UTF-8?q?=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

(cherry picked from commit 94451c380458b459791fc2bb6864a3255aca03c4)
---
 locales/index.d.ts                            |   2 +
 locales/ja-JP.yml                             |   2 +
 .../server/api/endpoints/admin/emoji/list.ts  |   9 +
 .../src/pages/custom-emojis-manager.vue       | 223 +++++++++++++++++-
 4 files changed, 234 insertions(+), 2 deletions(-)

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<typeof meta, typeof paramDef> { // 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
 					</MkPagination>
 				</div>
 
+				<div v-if="tab === 'draft'" class="draft">
+					<MkPagination ref="emojisDraftPaginationComponent" :pagination="paginationDraft">
+						<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
+						<template #default="{items}">
+							<div class="ldhfsamy">
+								<template v-for="emoji in items" :key="emoji.id">
+									<div class="emoji _panel">
+										<div class="img">
+											<div class="imgLight"><img :src="emoji.url" :alt="emoji.name"/></div>
+											<div class="imgDark"><img :src="emoji.url" :alt="emoji.name"/></div>
+										</div>
+										<div class="info">
+											<div class="name _monospace">{{ i18n.ts.name }}: {{ emoji.name }}</div>
+											<div class="category">{{ i18n.ts.category }}:{{ emoji.category }}</div>
+											<div class="aliases">{{ i18n.ts.tags }}:{{ emoji.aliases.join(' ') }}</div>
+											<div class="license">{{ i18n.ts.license }}:{{ emoji.license }}</div>
+										</div>
+										<div class="edit-button">
+											<button class="edit _button" @click="editDraft(emoji)">
+												{{ i18n.ts.edit }}
+											</button>
+											<button class="draft _button" @click="undrafted(emoji)">
+												{{ i18n.ts.undrafted }}
+											</button>
+											<button class="delete _button" @click="deleteDraft(emoji)">
+												{{ i18n.ts.delete }}
+											</button>
+										</div>
+									</div>
+								</template>
+							</div>
+						</template>
+					</MkPagination>
+				</div>
+
 				<div v-else-if="tab === 'remote'" class="remote">
 					<FormSplit>
 						<MkInput v-model="queryRemote" :debounce="true" type="search">
@@ -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<InstanceType<typeof MkPagination>>();
+const emojisDraftPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
-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 {