misskey/packages/frontend/src/pages/admin/custom-emojis-manager.local...

804 lines
21 KiB
Vue
Raw Normal View History

2024-01-27 03:02:50 +00:00
<template>
2024-02-04 09:42:03 +00:00
<div>
2024-02-22 23:59:00 +00:00
<div v-if="gridItems.length === 0" style="text-align: center">
2024-02-04 09:42:03 +00:00
登録された絵文字はありません
2024-01-27 03:02:50 +00:00
</div>
2024-02-04 09:42:03 +00:00
<div v-else class="_gaps">
2024-02-06 14:43:12 +00:00
<MkFolder>
<template #icon><i class="ti ti-search"></i></template>
<template #label>検索設定</template>
<template #caption>
検索条件を詳細に設定します
</template>
2024-02-06 22:57:25 +00:00
<div class="_gaps">
<div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]">
2024-02-18 04:53:12 +00:00
<MkInput
v-model="queryName"
:debounce="true"
type="search"
autocapitalize="off"
class="col1 row1"
@enter="onSearchRequest"
>
2024-02-06 22:57:25 +00:00
<template #label>name</template>
</MkInput>
<MkInput
2024-02-18 04:53:12 +00:00
v-model="queryCategory"
:debounce="true"
type="search"
autocapitalize="off"
class="col2 row1"
@enter="onSearchRequest"
2024-02-06 22:57:25 +00:00
>
<template #label>category</template>
</MkInput>
<MkInput
2024-02-18 04:53:12 +00:00
v-model="queryAlias"
:debounce="true"
type="search"
autocapitalize="off"
class="col3 row1"
@enter="onSearchRequest"
2024-02-06 22:57:25 +00:00
>
<template #label>alias</template>
</MkInput>
<MkInput
2024-02-18 04:53:12 +00:00
v-model="queryType"
:debounce="true"
type="search"
autocapitalize="off"
class="col1 row2"
@enter="onSearchRequest"
2024-02-06 22:57:25 +00:00
>
<template #label>type</template>
</MkInput>
<MkInput
2024-02-18 04:53:12 +00:00
v-model="queryLicense"
:debounce="true"
type="search"
autocapitalize="off"
class="col2 row2"
@enter="onSearchRequest"
2024-02-06 22:57:25 +00:00
>
<template #label>license</template>
</MkInput>
2024-02-18 04:53:12 +00:00
<MkSelect
v-model="querySensitive"
class="col3 row2"
>
2024-02-06 22:57:25 +00:00
<template #label>sensitive</template>
<option :value="null">-</option>
<option :value="true">true</option>
<option :value="false">false</option>
</MkSelect>
2024-02-18 04:53:12 +00:00
<MkSelect
v-model="queryLocalOnly"
class="col1 row3"
>
2024-02-06 22:57:25 +00:00
<template #label>localOnly</template>
<option :value="null">-</option>
<option :value="true">true</option>
<option :value="false">false</option>
</MkSelect>
2024-02-18 04:53:12 +00:00
<MkInput
v-model="queryUpdatedAtFrom"
:debounce="true"
type="date"
autocapitalize="off"
class="col2 row3"
@enter="onSearchRequest"
>
2024-02-06 22:57:25 +00:00
<template #label>updatedAt(from)</template>
</MkInput>
2024-02-18 04:53:12 +00:00
<MkInput
v-model="queryUpdatedAtTo"
:debounce="true"
type="date"
autocapitalize="off"
class="col3 row3"
@enter="onSearchRequest"
>
2024-02-06 22:57:25 +00:00
<template #label>updatedAt(to)</template>
</MkInput>
2024-02-18 04:53:12 +00:00
2024-02-17 10:51:44 +00:00
<MkInput
v-model="queryRolesText"
:debounce="true"
type="text"
readonly
autocapitalize="off"
class="col1 row4"
@click="onQueryRolesEditClicked"
>
<template #label>role</template>
<template #suffix><span class="ti ti-pencil"/></template>
</MkInput>
2024-02-06 22:57:25 +00:00
</div>
2024-02-12 03:04:27 +00:00
2024-02-12 11:10:49 +00:00
<MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>ソート順</template>
<div :class="$style.sortOrderArea">
<div :class="$style.sortOrderAreaTags">
<MkTagItem
v-for="order in sortOrders"
2024-02-14 13:10:42 +00:00
:key="order.key"
2024-02-12 11:10:49 +00:00
:iconClass="order.direction === 'ASC' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
:exButtonIconClass="'ti ti-x'"
2024-02-14 13:10:42 +00:00
:content="order.key"
@click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order.key)"
2024-02-12 11:10:49 +00:00
/>
</div>
2024-02-14 13:10:42 +00:00
<MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
2024-02-12 11:10:49 +00:00
<span class="ti ti-plus"/>
</MkButton>
</div>
</MkFolder>
2024-02-12 03:04:27 +00:00
2024-02-06 22:57:25 +00:00
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
2024-02-18 04:53:12 +00:00
<MkButton primary @click="onSearchRequest">
2024-02-06 14:43:12 +00:00
{{ i18n.ts.search }}
</MkButton>
<MkButton @click="onQueryResetButtonClicked">
リセット
</MkButton>
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-notes"></i></template>
<template #label>登録ログ</template>
<template #caption>
絵文字更新削除時のログが表示されます更新削除操作を行ったりページをリロードすると消えます
</template>
<XRegisterLogs :logs="requestLogs"/>
</MkFolder>
2024-01-27 03:02:50 +00:00
2024-02-04 23:31:42 +00:00
<div :class="$style.gridArea">
2024-02-10 13:31:04 +00:00
<MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
2024-01-29 00:35:26 +00:00
</div>
2024-01-27 03:02:50 +00:00
2024-02-05 12:30:06 +00:00
<MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
2024-02-04 09:42:03 +00:00
2024-02-05 12:30:06 +00:00
<div class="_gaps">
2024-02-04 09:42:03 +00:00
<div :class="$style.buttons">
2024-02-06 14:43:12 +00:00
<MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">{{ i18n.ts.delete }}</MkButton>
2024-02-06 22:57:25 +00:00
<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
{{
i18n.ts.update
}}
</MkButton>
2024-02-06 14:43:12 +00:00
<MkButton @click="onGridResetButtonClicked">リセット</MkButton>
2024-02-04 09:42:03 +00:00
</div>
2024-01-29 00:35:26 +00:00
</div>
2024-01-27 03:02:50 +00:00
</div>
</div>
</template>
<script setup lang="ts">
2024-02-06 22:57:25 +00:00
import { computed, onMounted, ref } from 'vue';
2024-01-27 03:02:50 +00:00
import * as Misskey from 'misskey-js';
2024-02-04 04:44:58 +00:00
import * as os from '@/os.js';
2024-02-07 23:12:04 +00:00
import {
emptyStrToEmptyArray,
emptyStrToNull,
emptyStrToUndefined,
2024-02-25 23:12:37 +00:00
RequestLogItem, roleIdsParser,
2024-02-18 01:04:39 +00:00
} from '@/pages/admin/custom-emojis-manager.impl.js';
2024-01-27 03:02:50 +00:00
import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
2024-01-29 00:35:26 +00:00
import MkButton from '@/components/MkButton.vue';
2024-02-25 23:12:37 +00:00
import { GridCellValidator, validators } from '@/components/grid/cell-validators.js';
2024-02-20 11:25:42 +00:00
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
2024-02-04 04:44:58 +00:00
import { misskeyApi } from '@/scripts/misskey-api.js';
2024-02-05 12:30:06 +00:00
import MkPagingButtons from '@/components/MkPagingButtons.vue';
2024-02-20 11:25:42 +00:00
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
2024-02-06 14:43:12 +00:00
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
2024-02-06 22:57:25 +00:00
import { deviceKind } from '@/scripts/device-kind.js';
2024-02-10 13:31:04 +00:00
import { GridSetting } from '@/components/grid/grid.js';
2024-02-12 11:10:49 +00:00
import MkTagItem from '@/components/MkTagItem.vue';
2024-02-14 13:10:42 +00:00
import { MenuItem } from '@/types/menu.js';
2024-02-17 10:51:44 +00:00
import { selectFile } from '@/scripts/select-file.js';
2024-02-20 11:25:42 +00:00
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
2024-02-04 04:44:58 +00:00
2024-02-07 23:12:04 +00:00
type GridItem = {
checked: boolean;
id: string;
url: string;
name: string;
host: string;
category: string;
aliases: string;
license: string;
isSensitive: boolean;
localOnly: boolean;
2024-02-17 10:51:44 +00:00
roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
2024-02-10 02:52:34 +00:00
fileId?: string;
2024-02-10 13:31:04 +00:00
updatedAt: string | null;
2024-02-12 03:04:27 +00:00
publicUrl?: string | null;
originalUrl?: string | null;
2024-02-07 23:12:04 +00:00
}
2024-02-06 11:44:20 +00:00
2024-02-14 13:10:42 +00:00
const gridSortOrderKeys = [
'name',
'category',
'aliases',
'type',
'license',
'isSensitive',
'localOnly',
'updatedAt',
];
type GridSortOrderKey = typeof gridSortOrderKeys[number];
2024-02-12 11:10:49 +00:00
type GridSortOrder = {
2024-02-14 13:10:42 +00:00
key: GridSortOrderKey;
2024-02-12 11:10:49 +00:00
direction: 'ASC' | 'DESC';
}
2024-02-10 13:31:04 +00:00
function setupGrid(): GridSetting {
const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
2024-02-25 23:12:37 +00:00
const unique = validators.unique();
2024-02-10 13:31:04 +00:00
return {
row: {
showNumber: true,
selectable: true,
2024-02-25 23:29:35 +00:00
// グリッドの行数をあらかじめ100行確保する
2024-02-10 13:31:04 +00:00
minimumDefinitionCount: 100,
styleRules: [
{
2024-02-25 23:29:35 +00:00
// 初期値から変わっていたら背景色を変更
2024-02-10 13:31:04 +00:00
condition: ({ row }) => JSON.stringify(gridItems.value[row.index]) !== JSON.stringify(originGridItems.value[row.index]),
applyStyle: { className: 'changedRow' },
},
{
2024-02-25 23:29:35 +00:00
// バリデーションに引っかかっていたら背景色を変更
2024-02-10 13:31:04 +00:00
condition: ({ cells }) => cells.some(it => !it.violation.valid),
applyStyle: { className: 'violationRow' },
},
],
2024-02-25 23:29:35 +00:00
// 行のコンテキストメニュー設定
2024-02-15 22:41:29 +00:00
contextMenuFactory: (row, context) => {
return [
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
2024-02-20 11:25:42 +00:00
action: () => copyGridDataToClipboard(gridItems, context),
2024-02-15 22:41:29 +00:00
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
2024-02-20 11:25:42 +00:00
for (const rangedRow of context.rangedRows) {
gridItems.value[rangedRow.index].checked = true;
2024-02-15 22:41:29 +00:00
}
},
},
];
},
2024-02-20 11:25:42 +00:00
events: {
delete(rows) {
2024-02-25 23:29:35 +00:00
// 行削除時は元データの行を消さず、削除対象としてマークするのみにする
2024-02-20 11:25:42 +00:00
for (const row of rows) {
gridItems.value[row.index].checked = true;
}
},
},
2024-02-10 13:31:04 +00:00
},
cols: [
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
2024-02-17 10:51:44 +00:00
{
bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
2024-02-20 11:25:42 +00:00
async customValueEditor(row, col, value, cellElement) {
2024-02-17 10:51:44 +00:00
const file = await selectFile(cellElement);
2024-02-18 06:14:52 +00:00
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
2024-02-17 10:51:44 +00:00
2024-02-18 06:14:52 +00:00
return file.url;
2024-02-17 10:51:44 +00:00
},
},
2024-02-25 23:12:37 +00:00
{
bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140,
validators: [required, regex, unique],
},
2024-02-10 13:31:04 +00:00
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
2024-02-17 10:51:44 +00:00
{
bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
2024-02-20 11:25:42 +00:00
valueTransformer(row) {
2024-02-17 10:51:44 +00:00
// バックエンドからからはIDと名前のペア配列で受け取るが、表示にIDがあると煩雑なので名前だけにする
2024-02-18 06:24:22 +00:00
return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
2024-02-18 06:14:52 +00:00
.map((it) => it.name)
2024-02-17 10:51:44 +00:00
.join(',');
},
2024-02-20 11:25:42 +00:00
async customValueEditor(row) {
2024-02-17 10:51:44 +00:00
// ID直記入は体験的に最悪なのでモーダルを使って入力する
2024-02-18 00:33:10 +00:00
const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction;
2024-02-17 10:51:44 +00:00
const result = await os.selectRole({
2024-02-18 00:33:10 +00:00
initialRoleIds: current.map(it => it.id),
2024-02-17 10:51:44 +00:00
title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction,
infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription,
publicOnly: true,
});
if (result.canceled) {
return current;
}
const transform = result.result.map(it => ({ id: it.id, name: it.name }));
gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform;
return transform;
},
2024-02-20 11:25:42 +00:00
events: {
2024-02-25 23:12:37 +00:00
paste: roleIdsParser,
2024-02-20 11:25:42 +00:00
delete(cell) {
// デフォルトはundefinedになるが、このプロパティは空配列にしたい
gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];
},
},
2024-02-17 10:51:44 +00:00
},
2024-02-12 03:04:27 +00:00
{ bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', type: 'text', editable: false, width: 180 },
{ bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
2024-02-10 13:31:04 +00:00
],
2024-02-15 22:41:29 +00:00
cells: {
2024-02-25 23:29:35 +00:00
// セルのコンテキストメニュー設定
2024-02-20 11:25:42 +00:00
contextMenuFactory(col, row, value, context) {
2024-02-15 22:41:29 +00:00
return [
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
2024-02-17 10:51:44 +00:00
action: () => {
2024-02-20 11:25:42 +00:00
return copyGridDataToClipboard(gridItems, context);
2024-02-17 10:51:44 +00:00
},
2024-02-15 22:41:29 +00:00
},
{
type: 'button',
text: '選択範囲を削除',
icon: 'ti ti-trash',
2024-02-20 11:25:42 +00:00
action: () => {
removeDataFromGrid(context, (cell) => {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
});
},
2024-02-15 22:41:29 +00:00
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const rowIdx of [...new Set(context.rangedCells.map(it => it.row.index)).values()]) {
gridItems.value[rowIdx].checked = true;
}
},
},
];
},
},
2024-02-10 13:31:04 +00:00
};
}
2024-01-27 03:02:50 +00:00
2024-02-05 12:30:06 +00:00
const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const allPages = ref<number>(0);
const currentPage = ref<number>(0);
2024-02-06 14:43:12 +00:00
const queryName = ref<string | null>(null);
const queryCategory = ref<string | null>(null);
const queryAlias = ref<string | null>(null);
const queryType = ref<string | null>(null);
const queryLicense = ref<string | null>(null);
const queryUpdatedAtFrom = ref<string | null>(null);
const queryUpdatedAtTo = ref<string | null>(null);
const querySensitive = ref<string | null>(null);
const queryLocalOnly = ref<string | null>(null);
2024-02-17 10:51:44 +00:00
const queryRoles = ref<{ id: string, name: string }[]>([]);
2024-02-05 12:30:06 +00:00
const previousQuery = ref<string | undefined>(undefined);
2024-02-12 11:10:49 +00:00
const sortOrders = ref<GridSortOrder[]>([]);
2024-02-06 14:43:12 +00:00
const requestLogs = ref<RequestLogItem[]>([]);
2024-01-27 03:02:50 +00:00
const gridItems = ref<GridItem[]>([]);
2024-02-04 04:44:58 +00:00
const originGridItems = ref<GridItem[]>([]);
const updateButtonDisabled = ref<boolean>(false);
2024-01-27 03:02:50 +00:00
2024-02-06 22:57:25 +00:00
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
2024-02-17 10:51:44 +00:00
const queryRolesText = computed(() => queryRoles.value.map(it => it.name).join(','));
2024-02-06 22:57:25 +00:00
2024-02-06 14:43:12 +00:00
async function onUpdateButtonClicked() {
2024-02-04 04:44:58 +00:00
const _items = gridItems.value;
const _originItems = originGridItems.value;
if (_items.length !== _originItems.length) {
throw new Error('The number of items has been changed. Please refresh the page and try again.');
}
2024-02-04 09:42:03 +00:00
const confirm = await os.confirm({
type: 'info',
title: '確認',
text: '絵文字の変更を保存します。よろしいですか?',
});
if (confirm.canceled) {
return;
}
const updatedItems = _items.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(_originItems[idx]));
if (updatedItems.length === 0) {
await os.alert({
type: 'info',
text: '変更された絵文字はありません。',
});
return;
}
2024-02-04 04:44:58 +00:00
2024-02-06 08:21:47 +00:00
const action = () => {
return updatedItems.map(item =>
2024-02-06 14:43:12 +00:00
misskeyApi(
'admin/emoji/update',
{
// eslint-disable-next-line
id: item.id!,
name: item.name,
category: emptyStrToNull(item.category),
aliases: emptyStrToEmptyArray(item.aliases),
license: emptyStrToNull(item.license),
isSensitive: item.isSensitive,
localOnly: item.localOnly,
2024-02-17 10:51:44 +00:00
roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
2024-02-10 13:31:04 +00:00
fileId: item.fileId,
2024-02-06 14:43:12 +00:00
})
.then(() => ({ item, success: true, err: undefined }))
.catch(err => ({ item, success: false, err })),
2024-02-06 08:21:47 +00:00
);
};
2024-02-04 04:44:58 +00:00
2024-02-06 14:43:12 +00:00
const result = await os.promiseDialog(Promise.all(action()));
const failedItems = result.filter(it => !it.success);
if (failedItems.length > 0) {
await os.alert({
type: 'error',
title: 'エラー',
text: '絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。',
});
}
requestLogs.value = result.map(it => ({
failed: !it.success,
url: it.item.url,
name: it.item.name,
error: it.err ? JSON.stringify(it.err) : undefined,
}));
await refreshCustomEmojis();
2024-02-04 09:42:03 +00:00
}
2024-02-06 14:43:12 +00:00
async function onDeleteButtonClicked() {
2024-02-04 09:42:03 +00:00
const _items = gridItems.value;
const _originItems = originGridItems.value;
if (_items.length !== _originItems.length) {
throw new Error('The number of items has been changed. Please refresh the page and try again.');
}
const confirm = await os.confirm({
type: 'info',
title: '確認',
text: 'チェックをつけられた絵文字を削除します。よろしいですか?',
});
if (confirm.canceled) {
return;
}
const deleteItems = _items.filter((it) => it.checked);
if (deleteItems.length === 0) {
await os.alert({
type: 'info',
text: '削除対象の絵文字はありません。',
});
return;
}
async function action() {
2024-02-04 04:44:58 +00:00
const deleteIds = deleteItems.map(it => it.id!);
await misskeyApi('admin/emoji/delete-bulk', { ids: deleteIds });
}
await os.promiseDialog(
action(),
);
}
2024-02-06 14:43:12 +00:00
function onGridResetButtonClicked() {
2024-02-04 04:44:58 +00:00
refreshGridItems();
}
2024-01-27 03:02:50 +00:00
2024-02-17 10:51:44 +00:00
async function onQueryRolesEditClicked() {
const result = await os.selectRole({
initialRoleIds: queryRoles.value.map(it => it.id),
title: '絵文字に設定されたロールで検索',
publicOnly: true,
});
if (result.canceled) {
return;
}
queryRoles.value = result.result;
}
2024-02-14 13:10:42 +00:00
function onToggleSortOrderButtonClicked(order: GridSortOrder) {
switch (order.direction) {
case 'ASC':
order.direction = 'DESC';
break;
case 'DESC':
order.direction = 'ASC';
break;
}
}
function onRemoveSortOrderButtonClicked(key: GridSortOrderKey) {
sortOrders.value = sortOrders.value.filter(it => it.key !== key);
}
function onAddSortOrderButtonClicked(ev: MouseEvent) {
const menuItems: MenuItem[] = gridSortOrderKeys
.filter(key => !sortOrders.value.map(it => it.key).includes(key))
.map(it => {
return {
text: it,
action: () => {
sortOrders.value.push({ key: it, direction: 'ASC' });
},
};
});
os.contextMenu(menuItems, ev);
}
2024-02-18 04:53:12 +00:00
async function onSearchRequest() {
2024-02-06 11:44:20 +00:00
await refreshCustomEmojis();
2024-02-04 00:24:10 +00:00
}
2024-02-06 14:43:12 +00:00
function onQueryResetButtonClicked() {
queryName.value = null;
queryCategory.value = null;
queryAlias.value = null;
queryType.value = null;
queryLicense.value = null;
queryUpdatedAtFrom.value = null;
queryUpdatedAtTo.value = null;
querySensitive.value = null;
queryLocalOnly.value = null;
2024-02-17 10:51:44 +00:00
queryRoles.value = [];
2024-02-06 14:43:12 +00:00
}
2024-02-05 12:30:06 +00:00
async function onPageChanged(pageNumber: number) {
currentPage.value = pageNumber;
await refreshCustomEmojis();
2024-02-04 00:24:10 +00:00
}
2024-02-20 11:25:42 +00:00
function onGridEvent(event: GridEvent) {
2024-02-04 04:44:58 +00:00
switch (event.type) {
case 'cell-validation':
onGridCellValidation(event);
break;
case 'cell-value-change':
2024-02-07 23:12:04 +00:00
onGridCellValueChange(event);
2024-02-04 04:44:58 +00:00
break;
}
}
function onGridCellValidation(event: GridCellValidationEvent) {
updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
}
2024-02-07 23:12:04 +00:00
function onGridCellValueChange(event: GridCellValueChangeEvent) {
2024-02-04 04:44:58 +00:00
const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
2024-02-17 10:51:44 +00:00
gridItems.value[row.index][column.setting.bindTo] = newValue;
2024-02-04 04:44:58 +00:00
}
}
2024-02-05 12:30:06 +00:00
async function refreshCustomEmojis() {
2024-02-06 08:43:37 +00:00
const limit = 100;
2024-02-05 12:30:06 +00:00
const query: Misskey.entities.AdminEmojiV2ListRequest['query'] = {
2024-02-06 14:43:12 +00:00
name: emptyStrToUndefined(queryName.value),
type: emptyStrToUndefined(queryType.value),
aliases: emptyStrToUndefined(queryAlias.value),
category: emptyStrToUndefined(queryCategory.value),
license: emptyStrToUndefined(queryLicense.value),
isSensitive: querySensitive.value ? Boolean(querySensitive.value).valueOf() : undefined,
localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined,
updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value),
updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value),
2024-02-17 10:51:44 +00:00
roleIds: queryRoles.value.map(it => it.id),
2024-02-05 12:30:06 +00:00
hostType: 'local',
};
if (JSON.stringify(query) !== previousQuery.value) {
currentPage.value = 1;
}
const result = await os.promiseDialog(
misskeyApi('admin/emoji/v2/list', {
query: query,
limit: limit,
page: currentPage.value,
2024-02-14 13:10:42 +00:00
sort: sortOrders.value.map(({ key, direction }) => ({ key: key as any, direction })),
2024-02-05 12:30:06 +00:00
}),
2024-02-06 22:57:25 +00:00
() => {
},
() => {
},
2024-02-05 12:30:06 +00:00
);
customEmojis.value = result.emojis;
allPages.value = result.allPages;
previousQuery.value = JSON.stringify(query);
refreshGridItems();
}
2024-01-27 03:02:50 +00:00
function refreshGridItems() {
2024-02-07 23:12:04 +00:00
gridItems.value = customEmojis.value.map(it => ({
checked: false,
id: it.id,
fileId: undefined,
url: it.publicUrl,
name: it.name,
host: it.host ?? '',
category: it.category ?? '',
2024-02-12 03:04:27 +00:00
aliases: it.aliases.join(','),
2024-02-07 23:12:04 +00:00
license: it.license ?? '',
isSensitive: it.isSensitive,
localOnly: it.localOnly,
2024-02-17 10:51:44 +00:00
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction,
2024-02-10 13:31:04 +00:00
updatedAt: it.updatedAt,
2024-02-12 03:04:27 +00:00
publicUrl: it.publicUrl,
originalUrl: it.originalUrl,
2024-02-07 23:12:04 +00:00
}));
2024-02-04 04:44:58 +00:00
originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
2024-01-27 03:02:50 +00:00
}
2024-02-05 12:30:06 +00:00
onMounted(async () => {
await refreshCustomEmojis();
});
2024-01-27 03:02:50 +00:00
</script>
2024-02-10 13:31:04 +00:00
<style lang="scss">
.violationRow {
background-color: var(--infoWarnBg);
}
.changedRow {
background-color: var(--infoBg);
}
</style>
2024-02-06 22:57:25 +00:00
<style lang="scss">
2024-02-10 02:52:34 +00:00
.editedRow {
background-color: var(--infoBg);
}
2024-02-06 22:57:25 +00:00
.row1 {
grid-row: 1 / 2;
}
.row2 {
grid-row: 2 / 3;
}
.row3 {
grid-row: 3 / 4;
}
.row4 {
grid-row: 4 / 5;
}
.col1 {
grid-column: 1 / 2;
}
.col2 {
grid-column: 2 / 3;
}
.col3 {
grid-column: 3 / 4;
}
</style>
2024-01-27 03:02:50 +00:00
<style module lang="scss">
2024-01-29 00:35:26 +00:00
.searchArea {
2024-02-06 14:43:12 +00:00
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
2024-01-29 00:35:26 +00:00
}
2024-01-27 03:02:50 +00:00
2024-02-06 22:57:25 +00:00
.searchAreaSp {
display: flex;
flex-direction: column;
gap: 8px;
}
.searchButtons {
display: flex;
justify-content: flex-end;
align-items: flex-end;
gap: 8px;
}
.searchButtonsSp {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
2024-02-12 11:10:49 +00:00
.sortOrderArea {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
}
.sortOrderAreaTags {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
2024-02-14 13:10:42 +00:00
flex-wrap: wrap;
2024-02-12 11:10:49 +00:00
gap: 8px;
}
.sortOrderAddButton {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
min-width: 2.0em;
min-height: 2.0em;
max-width: 2.0em;
max-height: 2.0em;
padding: 8px;
margin-left: auto;
border-radius: 9999px;
background-color: var(--buttonBg);
}
2024-02-04 23:31:42 +00:00
.gridArea {
overflow: scroll;
padding-top: 8px;
padding-bottom: 8px;
resize: vertical;
}
2024-01-29 00:35:26 +00:00
.buttons {
2024-02-04 09:42:03 +00:00
display: flex;
align-items: flex-end;
justify-content: center;
2024-01-29 00:35:26 +00:00
gap: 8px;
flex-wrap: wrap;
}
2024-02-12 03:04:27 +00:00
.divider {
margin: 8px 0;
border-top: solid 0.5px var(--divider);
}
2024-01-27 03:02:50 +00:00
</style>