feat: multiple emojis editing

This commit is contained in:
syuilo 2022-01-13 00:47:05 +09:00
parent b17726c9da
commit 1f2dab0a83
10 changed files with 428 additions and 139 deletions

View File

@ -0,0 +1,39 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection, In } from 'typeorm';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
ids: {
validator: $.arr($.type(ID)),
},
aliases: {
validator: $.arr($.str),
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps) => {
const emojis = await Emojis.find({
id: In(ps.ids),
});
for (const emoji of emojis) {
await Emojis.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
});
}
await getConnection().queryResultCache!.remove(['meta_emojis']);
});

View File

@ -0,0 +1,37 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection, In } from 'typeorm';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
ids: {
validator: $.arr($.type(ID)),
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps, me) => {
const emojis = await Emojis.find({
id: In(ps.ids),
});
for (const emoji of emojis) {
await Emojis.delete(emoji.id);
await getConnection().queryResultCache!.remove(['meta_emojis']);
insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
}
});

View File

@ -37,7 +37,7 @@ export default define(meta, async (ps, me) => {
await getConnection().queryResultCache!.remove(['meta_emojis']); await getConnection().queryResultCache!.remove(['meta_emojis']);
insertModerationLog(me, 'removeEmoji', { insertModerationLog(me, 'deleteEmoji', {
emoji: emoji, emoji: emoji,
}); });
}); });

View File

@ -0,0 +1,21 @@
import $ from 'cafy';
import define from '../../../define';
import { createImportCustomEmojisJob } from '@/queue/index';
import ms from 'ms';
import { ID } from '@/misc/cafy-id';
export const meta = {
secure: true,
requireCredential: true as const,
requireModerator: true,
params: {
fileId: {
validator: $.type(ID),
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps, user) => {
createImportCustomEmojisJob(user, ps.fileId);
});

View File

@ -0,0 +1,39 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection, In } from 'typeorm';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
ids: {
validator: $.arr($.type(ID)),
},
aliases: {
validator: $.arr($.str),
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps) => {
const emojis = await Emojis.find({
id: In(ps.ids),
});
for (const emoji of emojis) {
await Emojis.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
});
}
await getConnection().queryResultCache!.remove(['meta_emojis']);
});

View File

@ -0,0 +1,35 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection, In } from 'typeorm';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
ids: {
validator: $.arr($.type(ID)),
},
aliases: {
validator: $.arr($.str),
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps) => {
await Emojis.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
aliases: ps.aliases,
});
await getConnection().queryResultCache!.remove(['meta_emojis']);
});

View File

@ -0,0 +1,35 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection, In } from 'typeorm';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
ids: {
validator: $.arr($.type(ID)),
},
category: {
validator: $.optional.nullable.str,
},
},
};
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps) => {
await Emojis.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
category: ps.category,
});
await getConnection().queryResultCache!.remove(['meta_emojis']);
});

View File

@ -95,7 +95,7 @@ export default defineComponent({
}); });
if (canceled) return; if (canceled) return;
os.api('admin/emoji/remove', { os.api('admin/emoji/delete', {
id: this.emoji.id id: this.emoji.id
}).then(() => { }).then(() => {
this.$emit('done', { this.$emit('done', {

View File

@ -6,11 +6,22 @@
<template #prefix><i class="fas fa-search"></i></template> <template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template> <template #label>{{ $ts.search }}</template>
</MkInput> </MkInput>
<MkPagination ref="emojis" :pagination="pagination"> <MkSwitch v-model="selectMode" style="margin: 8px 0;">
<template #label>Select mode</template>
</MkSwitch>
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton inline @click="selectAll">Select all</MkButton>
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
<MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}"> <template v-slot="{items}">
<div class="ldhfsamy"> <div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)"> <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/> <img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body"> <div class="body">
<div class="name _monospace">{{ emoji.name }}</div> <div class="name _monospace">{{ emoji.name }}</div>
@ -32,7 +43,7 @@
<template #label>{{ $ts.host }}</template> <template #label>{{ $ts.host }}</template>
</MkInput> </MkInput>
</FormSplit> </FormSplit>
<MkPagination ref="remoteEmojis" :pagination="remotePagination"> <MkPagination :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}"> <template v-slot="{items}">
<div class="ldhfsamy"> <div class="ldhfsamy">
@ -51,137 +62,118 @@
</MkSpacer> </MkSpacer>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, toRef } from 'vue'; import { computed, defineComponent, ref, toRef } from 'vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue'; import MkTab from '@/components/tab.vue';
import MkSwitch from '@/components/form/switch.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import { selectFiles } from '@/scripts/select-file'; import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({ const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
components: {
MkTab,
MkButton,
MkInput,
MkPagination,
FormSplit,
},
emits: ['info'], const tab = ref('local');
const query = ref(null);
const queryRemote = ref(null);
const host = ref(null);
const selectMode = ref(false);
const selectedEmojis = ref<string[]>([]);
data() { const pagination = {
return {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.addEmoji,
handler: this.add,
}, {
icon: 'fas fa-ellipsis-h',
handler: this.menu,
}],
tabs: [{
active: this.tab === 'local',
title: this.$ts.local,
onClick: () => { this.tab = 'local'; },
}, {
active: this.tab === 'remote',
title: this.$ts.remote,
onClick: () => { this.tab = 'remote'; },
},]
})),
tab: 'local',
query: null,
queryRemote: null,
host: '',
pagination: {
endpoint: 'admin/emoji/list', endpoint: 'admin/emoji/list',
limit: 30, limit: 30,
params: computed(() => ({ params: computed(() => ({
query: (this.query && this.query !== '') ? this.query : null query: (query.value && query.value !== '') ? query.value : null,
})) })),
}, };
remotePagination: {
const remotePagination = {
endpoint: 'admin/emoji/list-remote', endpoint: 'admin/emoji/list-remote',
limit: 30, limit: 30,
params: computed(() => ({ params: computed(() => ({
query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null, query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
host: (this.host && this.host !== '') ? this.host : null host: (host.value && host.value !== '') ? host.value : null,
})) })),
}, };
const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
} }
}, };
async mounted() { const toggleSelect = (emoji) => {
this.$emit('info', toRef(this, symbols.PAGE_INFO)); if (selectedEmojis.value.includes(emoji.id)) {
}, selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
} else {
selectedEmojis.value.push(emoji.id);
}
};
methods: { const add = async (ev: MouseEvent) => {
async add(e) { const files = await selectFiles(ev.currentTarget || ev.target, null);
const files = await selectFiles(e.currentTarget || e.target, null);
const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
fileId: file.id, fileId: file.id,
}))); })));
promise.then(() => { promise.then(() => {
this.$refs.emojis.reload(); emojisPaginationComponent.value.reload();
}); });
os.promiseDialog(promise); os.promiseDialog(promise);
}, };
edit(emoji) { const edit = (emoji) => {
os.popup(import('./emoji-edit-dialog.vue'), { os.popup(import('./emoji-edit-dialog.vue'), {
emoji: emoji emoji: emoji
}, { }, {
done: result => { done: result => {
if (result.updated) { if (result.updated) {
this.$refs.emojis.replaceItem(item => item.id === emoji.id, { emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
...emoji, ...emoji,
...result.updated ...result.updated
}); });
} else if (result.deleted) { } else if (result.deleted) {
this.$refs.emojis.removeItem(item => item.id === emoji.id); emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
} }
}, },
}, 'closed'); }, 'closed');
}, };
im(emoji) { const im = (emoji) => {
os.apiWithDialog('admin/emoji/copy', { os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id, emojiId: emoji.id,
}); });
}, };
remoteMenu(emoji, ev) { const remoteMenu = (emoji, ev: MouseEvent) => {
os.popupMenu([{ os.popupMenu([{
type: 'label', type: 'label',
text: ':' + emoji.name + ':', text: ':' + emoji.name + ':',
}, { }, {
text: this.$ts.import, text: i18n.locale.import,
icon: 'fas fa-plus', icon: 'fas fa-plus',
action: () => { this.im(emoji) } action: () => { im(emoji) }
}], ev.currentTarget || ev.target); }], ev.currentTarget || ev.target);
}, };
menu(ev) { const menu = (ev: MouseEvent) => {
os.popupMenu([{ os.popupMenu([{
icon: 'fas fa-download', icon: 'fas fa-download',
text: this.$ts.export, text: i18n.locale.export,
action: async () => { action: async () => {
os.api('export-custom-emojis', { os.api('export-custom-emojis', {
}) })
.then(() => { .then(() => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: this.$ts.exportRequested, text: i18n.locale.exportRequested,
}); });
}).catch((e) => { }).catch((e) => {
os.alert({ os.alert({
@ -191,8 +183,92 @@ export default defineComponent({
}); });
} }
}], ev.currentTarget || ev.target); }], ev.currentTarget || ev.target);
} };
}
const setCategoryBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Category',
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/set-category-bulk', {
ids: selectedEmojis.value,
category: result,
});
emojisPaginationComponent.value.reload();
};
const addTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value.reload();
};
const removeTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value.reload();
};
const setTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: 'Tag',
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value.reload();
};
const delBulk = async () => {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.locale.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog('admin/emoji/delete-bulk', {
ids: selectedEmojis.value,
});
emojisPaginationComponent.value.reload();
};
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.locale.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.locale.addEmoji,
handler: add,
}, {
icon: 'fas fa-ellipsis-h',
handler: menu,
}],
tabs: [{
active: tab.value === 'local',
title: i18n.locale.local,
onClick: () => { tab.value = 'local'; },
}, {
active: tab.value === 'remote',
title: i18n.locale.remote,
onClick: () => { tab.value = 'remote'; },
},]
})),
}); });
</script> </script>
@ -212,11 +288,16 @@ export default defineComponent({
> .emoji { > .emoji {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px; padding: 11px;
text-align: left; text-align: left;
border: solid 1px var(--panel);
&:hover { &:hover {
color: var(--accent); border-color: var(--inputBorderHover);
}
&.selected {
border-color: var(--accent);
} }
> .img { > .img {

View File

@ -19,7 +19,7 @@
<div class="main"> <div class="main">
<MkStickyContainer> <MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/> <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
</MkStickyContainer> </MkStickyContainer>
</div> </div>
</div> </div>
@ -66,7 +66,9 @@ export default defineComponent({
const narrow = ref(false); const narrow = ref(false);
const view = ref(null); const view = ref(null);
const el = ref(null); const el = ref(null);
const onInfo = (viewInfo) => { const pageChanged = (page) => {
if (page == null) return;
const viewInfo = page[symbols.PAGE_INFO];
if (isRef(viewInfo)) { if (isRef(viewInfo)) {
watch(viewInfo, () => { watch(viewInfo, () => {
childInfo.value = viewInfo.value; childInfo.value = viewInfo.value;
@ -311,7 +313,7 @@ export default defineComponent({
narrow, narrow,
view, view,
el, el,
onInfo, pageChanged,
childInfo, childInfo,
pageProps, pageProps,
component, component,