enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善 (#15349)
* enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善
* fix
* 🎨
* 表示件数をメニューから変更するように
* 確認ダイアログ
* fix i18n
* needWideArea: trueならwidgetの開閉ボタンを表示しないように
* fix: 検索ウィンドウは一つしか開けないように
			
			
This commit is contained in:
		@@ -22,7 +22,8 @@ export const gridSortOrderKeys = [
 | 
			
		||||
	'isSensitive',
 | 
			
		||||
	'localOnly',
 | 
			
		||||
	'updatedAt',
 | 
			
		||||
];
 | 
			
		||||
] as const satisfies string[];
 | 
			
		||||
 | 
			
		||||
export type GridSortOrderKey = typeof gridSortOrderKeys[number];
 | 
			
		||||
 | 
			
		||||
export function emptyStrToUndefined(value: string | null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkWindow
 | 
			
		||||
	ref="uiWindow"
 | 
			
		||||
	:initialWidth="400"
 | 
			
		||||
	:initialHeight="500"
 | 
			
		||||
	:canResize="true"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}
 | 
			
		||||
	</template>
 | 
			
		||||
	<MkSpacer>
 | 
			
		||||
		<XRegisterLogs :logs="logs"/>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 | 
			
		||||
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
import type { RequestLogItem } from './custom-emojis-manager.impl.js';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
	logs: RequestLogItem[];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,213 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkWindow
 | 
			
		||||
	ref="uiWindow"
 | 
			
		||||
	:initialWidth="400"
 | 
			
		||||
	:initialHeight="500"
 | 
			
		||||
	:canResize="true"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }}
 | 
			
		||||
	</template>
 | 
			
		||||
	<div :class="$style.root">
 | 
			
		||||
		<MkSpacer>
 | 
			
		||||
			<div class="_gaps">
 | 
			
		||||
				<div class="_gaps_s">
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.name"
 | 
			
		||||
						type="search"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>name</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.category"
 | 
			
		||||
						type="search"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>category</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.aliases"
 | 
			
		||||
						type="search"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>aliases</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.type"
 | 
			
		||||
						type="search"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>type</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.license"
 | 
			
		||||
						type="search"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>license</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkSelect
 | 
			
		||||
						v-model="model.sensitive"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>sensitive</template>
 | 
			
		||||
						<option :value="null">-</option>
 | 
			
		||||
						<option :value="true">true</option>
 | 
			
		||||
						<option :value="false">false</option>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
 | 
			
		||||
					<MkSelect
 | 
			
		||||
						v-model="model.localOnly"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>localOnly</template>
 | 
			
		||||
						<option :value="null">-</option>
 | 
			
		||||
						<option :value="true">true</option>
 | 
			
		||||
						<option :value="false">false</option>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.updatedAtFrom"
 | 
			
		||||
						type="date"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>updatedAt(from)</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="model.updatedAtTo"
 | 
			
		||||
						type="date"
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>updatedAt(to)</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="queryRolesText"
 | 
			
		||||
						type="text"
 | 
			
		||||
						readonly
 | 
			
		||||
						autocapitalize="off"
 | 
			
		||||
						@click="onQueryRolesEditClicked"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>role</template>
 | 
			
		||||
						<template #suffix><i class="ti ti-pencil"></i></template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
				</div>
 | 
			
		||||
				<MkFolder :spacerMax="8" :spacerMin="8">
 | 
			
		||||
					<template #icon><i class="ti ti-arrows-sort"></i></template>
 | 
			
		||||
					<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
 | 
			
		||||
					<MkSortOrderEditor
 | 
			
		||||
						:baseOrderKeyNames="gridSortOrderKeys"
 | 
			
		||||
						:currentOrders="sortOrders"
 | 
			
		||||
						@update="onSortOrderUpdate"
 | 
			
		||||
					/>
 | 
			
		||||
				</MkFolder>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkSpacer>
 | 
			
		||||
		<div :class="$style.footerActions">
 | 
			
		||||
			<MkButton primary @click="onSearchRequest">
 | 
			
		||||
				{{ i18n.ts.search }}
 | 
			
		||||
			</MkButton>
 | 
			
		||||
			<MkButton @click="onQueryResetButtonClicked">
 | 
			
		||||
				{{ i18n.ts.reset }}
 | 
			
		||||
			</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
	gridSortOrderKeys,
 | 
			
		||||
} from './custom-emojis-manager.impl.js';
 | 
			
		||||
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
 | 
			
		||||
import type { EmojiSearchQuery } from './custom-emojis-manager.local.list.vue';
 | 
			
		||||
import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
 | 
			
		||||
import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	query: EmojiSearchQuery;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
	(ev: 'queryUpdated', query: EmojiSearchQuery): void;
 | 
			
		||||
	(ev: 'sortOrderUpdated', orders: SortOrder<GridSortOrderKey>[]): void;
 | 
			
		||||
	(ev: 'search'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const model = ref<EmojiSearchQuery>(props.query);
 | 
			
		||||
const queryRolesText = computed(() => model.value.roles.map(it => it.name).join(','));
 | 
			
		||||
 | 
			
		||||
watch(model, () => {
 | 
			
		||||
	emit('queryUpdated', model.value);
 | 
			
		||||
}, { deep: true });
 | 
			
		||||
 | 
			
		||||
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
 | 
			
		||||
 | 
			
		||||
function onSortOrderUpdate(orders: SortOrder<GridSortOrderKey>[]) {
 | 
			
		||||
	sortOrders.value = orders;
 | 
			
		||||
	emit('sortOrderUpdated', orders);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onSearchRequest() {
 | 
			
		||||
	emit('search');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onQueryResetButtonClicked() {
 | 
			
		||||
	model.value.name = '';
 | 
			
		||||
	model.value.category = '';
 | 
			
		||||
	model.value.aliases = '';
 | 
			
		||||
	model.value.type = '';
 | 
			
		||||
	model.value.license = '';
 | 
			
		||||
	model.value.sensitive = null;
 | 
			
		||||
	model.value.localOnly = null;
 | 
			
		||||
	model.value.updatedAtFrom = '';
 | 
			
		||||
	model.value.updatedAtTo = '';
 | 
			
		||||
	sortOrders.value = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onQueryRolesEditClicked() {
 | 
			
		||||
	const result = await os.selectRole({
 | 
			
		||||
		initialRoleIds: model.value.roles.map(it => it.id),
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle,
 | 
			
		||||
		publicOnly: true,
 | 
			
		||||
	});
 | 
			
		||||
	if (result.canceled) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	model.value.roles = result.result;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style module>
 | 
			
		||||
.root {
 | 
			
		||||
	position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footerActions {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	padding: var(--MI-margin);
 | 
			
		||||
	background-color: var(--MI_THEME-bg);
 | 
			
		||||
	display: flex;
 | 
			
		||||
	gap: 8px;
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -5,137 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkStickyContainer>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<MkPageHeader :overridePageMetadata="headerPageMetadata" :actions="headerActions"/>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template #default>
 | 
			
		||||
		<div class="_gaps">
 | 
			
		||||
			<MkFolder>
 | 
			
		||||
				<template #icon><i class="ti ti-search"></i></template>
 | 
			
		||||
				<template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}</template>
 | 
			
		||||
				<template #caption>
 | 
			
		||||
					{{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }}
 | 
			
		||||
				</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_gaps">
 | 
			
		||||
					<div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]">
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryName"
 | 
			
		||||
							type="search"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col1, $style.row1]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>name</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryCategory"
 | 
			
		||||
							type="search"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col2, $style.row1]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>category</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryAliases"
 | 
			
		||||
							type="search"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col3, $style.row1]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>aliases</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryType"
 | 
			
		||||
							type="search"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col1, $style.row2]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>type</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryLicense"
 | 
			
		||||
							type="search"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col2, $style.row2]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>license</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
						<MkSelect
 | 
			
		||||
							v-model="querySensitive"
 | 
			
		||||
							:class="[$style.col3, $style.row2]"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>sensitive</template>
 | 
			
		||||
							<option :value="null">-</option>
 | 
			
		||||
							<option :value="true">true</option>
 | 
			
		||||
							<option :value="false">false</option>
 | 
			
		||||
						</MkSelect>
 | 
			
		||||
 | 
			
		||||
						<MkSelect
 | 
			
		||||
							v-model="queryLocalOnly"
 | 
			
		||||
							:class="[$style.col1, $style.row3]"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>localOnly</template>
 | 
			
		||||
							<option :value="null">-</option>
 | 
			
		||||
							<option :value="true">true</option>
 | 
			
		||||
							<option :value="false">false</option>
 | 
			
		||||
						</MkSelect>
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryUpdatedAtFrom"
 | 
			
		||||
							type="date"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col2, $style.row3]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>updatedAt(from)</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryUpdatedAtTo"
 | 
			
		||||
							type="date"
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col3, $style.row3]"
 | 
			
		||||
							@enter="onSearchRequest"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>updatedAt(to)</template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
 | 
			
		||||
						<MkInput
 | 
			
		||||
							v-model="queryRolesText"
 | 
			
		||||
							type="text"
 | 
			
		||||
							readonly
 | 
			
		||||
							autocapitalize="off"
 | 
			
		||||
							:class="[$style.col1, $style.row4]"
 | 
			
		||||
							@click="onQueryRolesEditClicked"
 | 
			
		||||
						>
 | 
			
		||||
							<template #label>role</template>
 | 
			
		||||
							<template #suffix><span class="ti ti-pencil"/></template>
 | 
			
		||||
						</MkInput>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<MkFolder :spacerMax="8" :spacerMin="8">
 | 
			
		||||
						<template #icon><i class="ti ti-arrows-sort"></i></template>
 | 
			
		||||
						<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
 | 
			
		||||
						<MkSortOrderEditor
 | 
			
		||||
							:baseOrderKeyNames="gridSortOrderKeys"
 | 
			
		||||
							:currentOrders="sortOrders"
 | 
			
		||||
							@update="onSortOrderUpdate"
 | 
			
		||||
						/>
 | 
			
		||||
					</MkFolder>
 | 
			
		||||
 | 
			
		||||
					<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
 | 
			
		||||
						<MkButton primary @click="onSearchRequest">
 | 
			
		||||
							{{ i18n.ts.search }}
 | 
			
		||||
						</MkButton>
 | 
			
		||||
						<MkButton @click="onQueryResetButtonClicked">
 | 
			
		||||
							{{ i18n.ts.reset }}
 | 
			
		||||
						</MkButton>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<XRegisterLogsFolder :logs="requestLogs"/>
 | 
			
		||||
 | 
			
		||||
		<div class="_gaps" :class="$style.main">
 | 
			
		||||
			<component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
 | 
			
		||||
			<template v-else>
 | 
			
		||||
				<div v-if="gridItems.length === 0" style="text-align: center">
 | 
			
		||||
@@ -143,65 +17,78 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<template v-else>
 | 
			
		||||
					<div :class="$style.gridArea">
 | 
			
		||||
					<div :class="$style.grid">
 | 
			
		||||
						<MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div :class="$style.footer">
 | 
			
		||||
						<div :class="$style.left">
 | 
			
		||||
							<MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">
 | 
			
		||||
								{{ i18n.ts.delete }} ({{ deleteItemsCount }})
 | 
			
		||||
							</MkButton>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div :class="$style.center">
 | 
			
		||||
							<MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div :class="$style.right">
 | 
			
		||||
							<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
 | 
			
		||||
								{{ i18n.ts.update }} ({{ updatedItemsCount }})
 | 
			
		||||
							</MkButton>
 | 
			
		||||
							<MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<template #footer>
 | 
			
		||||
		<div v-if="gridItems.length > 0" :class="$style.footer">
 | 
			
		||||
			<div :class="$style.left">
 | 
			
		||||
				<MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">
 | 
			
		||||
					{{ i18n.ts.delete }} ({{ deleteItemsCount }})
 | 
			
		||||
				</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div :class="$style.center">
 | 
			
		||||
				<MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div :class="$style.right">
 | 
			
		||||
				<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
 | 
			
		||||
					{{ i18n.ts.update }} ({{ updatedItemsCount }})
 | 
			
		||||
				</MkButton>
 | 
			
		||||
				<MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
 | 
			
		||||
import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
 | 
			
		||||
 | 
			
		||||
export type EmojiSearchQuery = {
 | 
			
		||||
	name: string | null;
 | 
			
		||||
	category: string | null;
 | 
			
		||||
	aliases: string | null;
 | 
			
		||||
	type: string | null;
 | 
			
		||||
	license: string | null;
 | 
			
		||||
	updatedAtFrom: string | null;
 | 
			
		||||
	updatedAtTo: string | null;
 | 
			
		||||
	sensitive: string | null;
 | 
			
		||||
	localOnly: string | null;
 | 
			
		||||
	roles: { id: string, name: string }[];
 | 
			
		||||
	sortOrders: SortOrder<GridSortOrderKey>[];
 | 
			
		||||
	limit: number;
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, onMounted, ref, useCssModule } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import {
 | 
			
		||||
	emptyStrToEmptyArray,
 | 
			
		||||
	emptyStrToNull,
 | 
			
		||||
	emptyStrToUndefined,
 | 
			
		||||
	GridSortOrderKey,
 | 
			
		||||
	gridSortOrderKeys,
 | 
			
		||||
	RequestLogItem,
 | 
			
		||||
	roleIdsParser,
 | 
			
		||||
} from '@/pages/admin/custom-emojis-manager.impl.js';
 | 
			
		||||
import MkGrid from '@/components/grid/MkGrid.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { validators } from '@/components/grid/cell-validators.js';
 | 
			
		||||
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import MkPagingButtons from '@/components/MkPagingButtons.vue';
 | 
			
		||||
import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import { deviceKind } from '@/scripts/device-kind.js';
 | 
			
		||||
import { GridSetting } from '@/components/grid/grid.js';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file.js';
 | 
			
		||||
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
 | 
			
		||||
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
 | 
			
		||||
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
 | 
			
		||||
import { useLoading } from "@/components/hook/useLoading.js";
 | 
			
		||||
 | 
			
		||||
type GridItem = {
 | 
			
		||||
@@ -230,6 +117,11 @@ function setupGrid(): GridSetting {
 | 
			
		||||
	const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
 | 
			
		||||
	const unique = validators.unique();
 | 
			
		||||
	return {
 | 
			
		||||
		root: {
 | 
			
		||||
			noOverflowStyle: true,
 | 
			
		||||
			rounded: false,
 | 
			
		||||
			outerBorder: false,
 | 
			
		||||
		},
 | 
			
		||||
		row: {
 | 
			
		||||
			showNumber: true,
 | 
			
		||||
			selectable: true,
 | 
			
		||||
@@ -381,16 +273,22 @@ const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
 | 
			
		||||
const allPages = ref<number>(0);
 | 
			
		||||
const currentPage = ref<number>(0);
 | 
			
		||||
 | 
			
		||||
const queryName = ref<string | null>(null);
 | 
			
		||||
const queryCategory = ref<string | null>(null);
 | 
			
		||||
const queryAliases = 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);
 | 
			
		||||
const queryRoles = ref<{ id: string, name: string }[]>([]);
 | 
			
		||||
const searchQuery = ref<EmojiSearchQuery>({
 | 
			
		||||
	name: null,
 | 
			
		||||
	category: null,
 | 
			
		||||
	aliases: null,
 | 
			
		||||
	type: null,
 | 
			
		||||
	license: null,
 | 
			
		||||
	updatedAtFrom: null,
 | 
			
		||||
	updatedAtTo: null,
 | 
			
		||||
	sensitive: null,
 | 
			
		||||
	localOnly: null,
 | 
			
		||||
	roles: [],
 | 
			
		||||
	sortOrders: [],
 | 
			
		||||
	limit: 25,
 | 
			
		||||
});
 | 
			
		||||
let searchWindowOpening = false;
 | 
			
		||||
 | 
			
		||||
const previousQuery = ref<string | undefined>(undefined);
 | 
			
		||||
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
 | 
			
		||||
const requestLogs = ref<RequestLogItem[]>([]);
 | 
			
		||||
@@ -399,8 +297,6 @@ const gridItems = ref<GridItem[]>([]);
 | 
			
		||||
const originGridItems = ref<GridItem[]>([]);
 | 
			
		||||
const updateButtonDisabled = ref<boolean>(false);
 | 
			
		||||
 | 
			
		||||
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
 | 
			
		||||
const queryRolesText = computed(() => queryRoles.value.map(it => it.name).join(','));
 | 
			
		||||
const updatedItemsCount = computed(() => {
 | 
			
		||||
	return gridItems.value.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(originGridItems.value[idx])).length;
 | 
			
		||||
});
 | 
			
		||||
@@ -422,12 +318,11 @@ async function onUpdateButtonClicked() {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const confirm = await os.confirm({
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'info',
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._list.confirmUpdateEmojisTitle,
 | 
			
		||||
		text: i18n.tsx._customEmojisManager._local._list.confirmUpdateEmojisDescription({ count: updatedItems.length }),
 | 
			
		||||
	});
 | 
			
		||||
	if (confirm.canceled) {
 | 
			
		||||
	if (canceled) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -458,7 +353,7 @@ async function onUpdateButtonClicked() {
 | 
			
		||||
	if (failedItems.length > 0) {
 | 
			
		||||
		await os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
 | 
			
		||||
			title: i18n.ts.somethingHappened,
 | 
			
		||||
			text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
@@ -489,12 +384,11 @@ async function onDeleteButtonClicked() {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const confirm = await os.confirm({
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'info',
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._list.confirmDeleteEmojisTitle,
 | 
			
		||||
		text: i18n.tsx._customEmojisManager._local._list.confirmDeleteEmojisDescription({ count: deleteItems.length }),
 | 
			
		||||
	});
 | 
			
		||||
	if (confirm.canceled) {
 | 
			
		||||
	if (canceled) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -508,47 +402,35 @@ async function onDeleteButtonClicked() {
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onGridResetButtonClicked() {
 | 
			
		||||
	refreshGridItems();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onQueryRolesEditClicked() {
 | 
			
		||||
	const result = await os.selectRole({
 | 
			
		||||
		initialRoleIds: queryRoles.value.map(it => it.id),
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle,
 | 
			
		||||
		publicOnly: true,
 | 
			
		||||
async function onGridResetButtonClicked() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		title: i18n.ts.resetAreYouSure,
 | 
			
		||||
		text: i18n.ts._customEmojisManager._local._list.confirmResetDescription,
 | 
			
		||||
	});
 | 
			
		||||
	if (result.canceled) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	queryRoles.value = result.result;
 | 
			
		||||
}
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
 | 
			
		||||
	sortOrders.value = _sortOrders;
 | 
			
		||||
	refreshGridItems();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onSearchRequest() {
 | 
			
		||||
	await refreshCustomEmojis();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onQueryResetButtonClicked() {
 | 
			
		||||
	queryName.value = null;
 | 
			
		||||
	queryCategory.value = null;
 | 
			
		||||
	queryAliases.value = null;
 | 
			
		||||
	queryType.value = null;
 | 
			
		||||
	queryLicense.value = null;
 | 
			
		||||
	queryUpdatedAtFrom.value = null;
 | 
			
		||||
	queryUpdatedAtTo.value = null;
 | 
			
		||||
	querySensitive.value = null;
 | 
			
		||||
	queryLocalOnly.value = null;
 | 
			
		||||
	queryRoles.value = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onPageChanged(pageNumber: number) {
 | 
			
		||||
	if (updatedItemsCount.value > 0) {
 | 
			
		||||
		const { canceled } = await os.confirm({
 | 
			
		||||
			type: 'warning',
 | 
			
		||||
			title: i18n.ts._customEmojisManager._local._list.confirmMovePage,
 | 
			
		||||
			text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
 | 
			
		||||
		});
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	currentPage.value = pageNumber;
 | 
			
		||||
	await refreshCustomEmojis();
 | 
			
		||||
	await nextTick();
 | 
			
		||||
	refreshCustomEmojis();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onGridEvent(event: GridEvent) {
 | 
			
		||||
@@ -574,19 +456,19 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function refreshCustomEmojis() {
 | 
			
		||||
	const limit = 100;
 | 
			
		||||
	const limit = searchQuery.value.limit;
 | 
			
		||||
 | 
			
		||||
	const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
 | 
			
		||||
		name: emptyStrToUndefined(queryName.value),
 | 
			
		||||
		type: emptyStrToUndefined(queryType.value),
 | 
			
		||||
		aliases: emptyStrToUndefined(queryAliases.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),
 | 
			
		||||
		roleIds: queryRoles.value.map(it => it.id),
 | 
			
		||||
		name: emptyStrToUndefined(searchQuery.value.name),
 | 
			
		||||
		type: emptyStrToUndefined(searchQuery.value.type),
 | 
			
		||||
		aliases: emptyStrToUndefined(searchQuery.value.aliases),
 | 
			
		||||
		category: emptyStrToUndefined(searchQuery.value.category),
 | 
			
		||||
		license: emptyStrToUndefined(searchQuery.value.license),
 | 
			
		||||
		isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined,
 | 
			
		||||
		localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined,
 | 
			
		||||
		updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom),
 | 
			
		||||
		updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo),
 | 
			
		||||
		roleIds: searchQuery.value.roles.map(it => it.id),
 | 
			
		||||
		hostType: 'local',
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
@@ -635,6 +517,83 @@ onMounted(async () => {
 | 
			
		||||
	await refreshCustomEmojis();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const headerPageMetadata = computed(() => ({
 | 
			
		||||
	title: i18n.ts._customEmojisManager._local.tabTitleList,
 | 
			
		||||
	icon: 'ti ti-icons',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const headerActions = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-search',
 | 
			
		||||
	text: i18n.ts.search,
 | 
			
		||||
	handler: () => {
 | 
			
		||||
		if (searchWindowOpening) return;
 | 
			
		||||
		searchWindowOpening = true;
 | 
			
		||||
		const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.search.vue')), {
 | 
			
		||||
			query: searchQuery.value,
 | 
			
		||||
		}, {
 | 
			
		||||
			queryUpdated: (query: EmojiSearchQuery) => {
 | 
			
		||||
				searchQuery.value = query;
 | 
			
		||||
			},
 | 
			
		||||
			sortOrderUpdated: (orders: SortOrder<GridSortOrderKey>[]) => {
 | 
			
		||||
				sortOrders.value = orders;
 | 
			
		||||
			},
 | 
			
		||||
			search: () => {
 | 
			
		||||
				onSearchRequest();
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
				searchWindowOpening = false;
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
}, {
 | 
			
		||||
	icon: 'ti ti-list-numbers',
 | 
			
		||||
	text: i18n.ts._customEmojisManager._gridCommon.searchLimit,
 | 
			
		||||
	handler: (ev: MouseEvent) => {
 | 
			
		||||
		async function changeSearchLimit(to: number) {
 | 
			
		||||
			if (updatedItemsCount.value > 0) {
 | 
			
		||||
				const { canceled } = await os.confirm({
 | 
			
		||||
					type: 'warning',
 | 
			
		||||
					title: i18n.ts._customEmojisManager._local._list.confirmChangeView,
 | 
			
		||||
					text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
 | 
			
		||||
				});
 | 
			
		||||
				if (canceled) return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			searchQuery.value.limit = to;
 | 
			
		||||
			refreshCustomEmojis();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		os.popupMenu([{
 | 
			
		||||
			type: 'radioOption',
 | 
			
		||||
			text: '25',
 | 
			
		||||
			active: computed(() => searchQuery.value.limit === 25),
 | 
			
		||||
			action: () => changeSearchLimit(25),
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'radioOption',
 | 
			
		||||
			text: '50',
 | 
			
		||||
			active: computed(() => searchQuery.value.limit === 50),
 | 
			
		||||
			action: () => changeSearchLimit(50),
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'radioOption',
 | 
			
		||||
			text: '100',
 | 
			
		||||
			active: computed(() => searchQuery.value.limit === 100),
 | 
			
		||||
			action: () => changeSearchLimit(100),
 | 
			
		||||
		}], ev.currentTarget ?? ev.target);
 | 
			
		||||
	},
 | 
			
		||||
}, {
 | 
			
		||||
	icon: 'ti ti-notes',
 | 
			
		||||
	text: i18n.ts._customEmojisManager._gridCommon.registrationLogs,
 | 
			
		||||
	handler: () => {
 | 
			
		||||
		const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.logs.vue')), {
 | 
			
		||||
			logs: requestLogs.value,
 | 
			
		||||
		}, {
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}]);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style module lang="scss">
 | 
			
		||||
@@ -650,77 +609,21 @@ onMounted(async () => {
 | 
			
		||||
	background-color: var(--MI_THEME-infoBg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.row1 {
 | 
			
		||||
	grid-row: 1 / 2;
 | 
			
		||||
.main {
 | 
			
		||||
	height: calc(100vh - var(--MI-stickyTop) - var(--MI-stickyBottom));
 | 
			
		||||
	overflow: scroll;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.searchArea {
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
	gap: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gridArea {
 | 
			
		||||
	padding-top: 8px;
 | 
			
		||||
	padding-bottom: 8px;
 | 
			
		||||
.grid {
 | 
			
		||||
	width: max-content;
 | 
			
		||||
	border-bottom: 1px solid var(--MI_THEME-divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footer {
 | 
			
		||||
	background-color: var(--MI_THEME-bg);
 | 
			
		||||
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	left:0;
 | 
			
		||||
	bottom:0;
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
	// stickyで追従させる都合上、フッター自身でpaddingを持つ必要があるため、親要素で画一的に指定している分をネガティブマージンで相殺している
 | 
			
		||||
	margin-top: calc(var(--MI-margin) * -1);
 | 
			
		||||
	margin-bottom: calc(var(--MI-margin) * -1);
 | 
			
		||||
	padding-top: var(--MI-margin);
 | 
			
		||||
	padding-bottom: var(--MI-margin);
 | 
			
		||||
	padding: var(--MI-margin);
 | 
			
		||||
	border-top: 1px solid var(--MI_THEME-divider);
 | 
			
		||||
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
 | 
			
		||||
	<XRegisterLogsFolder :logs="requestLogs"/>
 | 
			
		||||
	<MkFolder>
 | 
			
		||||
		<template #icon><i class="ti ti-notes"></i></template>
 | 
			
		||||
		<template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
 | 
			
		||||
		<template #caption>
 | 
			
		||||
			{{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
 | 
			
		||||
		</template>
 | 
			
		||||
		<XRegisterLogs :logs="requestLogs"/>
 | 
			
		||||
	</MkFolder>
 | 
			
		||||
 | 
			
		||||
	<div
 | 
			
		||||
		:class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]"
 | 
			
		||||
@@ -91,7 +98,7 @@ import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js'
 | 
			
		||||
import { uploadFile } from '@/scripts/upload.js';
 | 
			
		||||
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
 | 
			
		||||
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
 | 
			
		||||
import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
 | 
			
		||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 | 
			
		||||
import { GridSetting } from '@/components/grid/grid.js';
 | 
			
		||||
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
 | 
			
		||||
import { GridRow } from '@/components/grid/row.js';
 | 
			
		||||
@@ -245,7 +252,6 @@ const isDragOver = ref<boolean>(false);
 | 
			
		||||
async function onRegistryClicked() {
 | 
			
		||||
	const dialogSelection = await os.confirm({
 | 
			
		||||
		type: 'info',
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._register.confirmRegisterEmojisTitle,
 | 
			
		||||
		text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -279,7 +285,7 @@ async function onRegistryClicked() {
 | 
			
		||||
	if (failedItems.length > 0) {
 | 
			
		||||
		await os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
 | 
			
		||||
			title: i18n.ts.somethingHappened,
 | 
			
		||||
			text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
@@ -299,7 +305,6 @@ async function onRegistryClicked() {
 | 
			
		||||
async function onClearClicked() {
 | 
			
		||||
	const result = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._register.confirmClearEmojisTitle,
 | 
			
		||||
		text: i18n.ts._customEmojisManager._local._register.confirmClearEmojisDescription,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -314,7 +319,6 @@ async function onDrop(ev: DragEvent) {
 | 
			
		||||
	const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it));
 | 
			
		||||
	const confirm = await os.confirm({
 | 
			
		||||
		type: 'info',
 | 
			
		||||
		title: i18n.ts._customEmojisManager._local._register.confirmUploadEmojisTitle,
 | 
			
		||||
		text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }),
 | 
			
		||||
	});
 | 
			
		||||
	if (confirm.canceled) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,33 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_gaps" :class="$style.root">
 | 
			
		||||
	<MkTab v-model="modeTab" style="margin-bottom: var(--margin);">
 | 
			
		||||
		<option value="list">{{ i18n.ts._customEmojisManager._local.tabTitleList }}</option>
 | 
			
		||||
		<option value="register">{{ i18n.ts._customEmojisManager._local.tabTitleRegister }}</option>
 | 
			
		||||
	</MkTab>
 | 
			
		||||
 | 
			
		||||
	<div>
 | 
			
		||||
		<XListComponent v-if="modeTab === 'list'"/>
 | 
			
		||||
		<XRegisterComponent v-else/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<MkStickyContainer>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<MkPageHeader v-model:tab="headerTab" :tabs="headerTabs" hideTitle thin/>
 | 
			
		||||
	</template>
 | 
			
		||||
	<XListComponent v-if="headerTab === 'list'" key="localList"/>
 | 
			
		||||
	<MkSpacer v-else key="localRegister">
 | 
			
		||||
		<XRegisterComponent/>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkTab from '@/components/MkTab.vue';
 | 
			
		||||
import XListComponent from '@/pages/admin/custom-emojis-manager.local.list.vue';
 | 
			
		||||
import XRegisterComponent from '@/pages/admin/custom-emojis-manager.local.register.vue';
 | 
			
		||||
 | 
			
		||||
type PageMode = 'list' | 'register';
 | 
			
		||||
 | 
			
		||||
const modeTab = ref<PageMode>('list');
 | 
			
		||||
</script>
 | 
			
		||||
const headerTab = ref<PageMode>('list');
 | 
			
		||||
 | 
			
		||||
<style module lang="scss">
 | 
			
		||||
.root {
 | 
			
		||||
	padding: var(--MI-margin);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
const headerTabs = computed(() => [{
 | 
			
		||||
	key: 'list',
 | 
			
		||||
	title: i18n.ts._customEmojisManager._local.tabTitleList,
 | 
			
		||||
}, {
 | 
			
		||||
	key: 'register',
 | 
			
		||||
	title: i18n.ts._customEmojisManager._local.tabTitleRegister,
 | 
			
		||||
}]);
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,47 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkFolder>
 | 
			
		||||
	<template #icon><i class="ti ti-notes"></i></template>
 | 
			
		||||
	<template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
 | 
			
		||||
	<template #caption>
 | 
			
		||||
		{{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div>
 | 
			
		||||
		<div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;">
 | 
			
		||||
			<MkSwitch v-model="showingSuccessLogs">
 | 
			
		||||
				<template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template>
 | 
			
		||||
			</MkSwitch>
 | 
			
		||||
			<div>
 | 
			
		||||
				<div v-if="filteredLogs.length > 0">
 | 
			
		||||
					<MkGrid
 | 
			
		||||
						:data="filteredLogs"
 | 
			
		||||
						:settings="setupGrid()"
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-else>
 | 
			
		||||
					{{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
 | 
			
		||||
				</div>
 | 
			
		||||
<div>
 | 
			
		||||
	<div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;">
 | 
			
		||||
		<MkSwitch v-model="showingSuccessLogs">
 | 
			
		||||
			<template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template>
 | 
			
		||||
		</MkSwitch>
 | 
			
		||||
		<div>
 | 
			
		||||
			<div v-if="filteredLogs.length > 0">
 | 
			
		||||
				<MkGrid
 | 
			
		||||
					:data="filteredLogs"
 | 
			
		||||
					:settings="setupGrid()"
 | 
			
		||||
				/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-else>
 | 
			
		||||
				{{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-else>
 | 
			
		||||
			{{ i18n.ts._customEmojisManager._logs.logNothing }}
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkFolder>
 | 
			
		||||
	<div v-else>
 | 
			
		||||
		{{ i18n.ts._customEmojisManager._logs.logNothing }}
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 | 
			
		||||
import { computed, ref, toRefs } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
 | 
			
		||||
import MkGrid from '@/components/grid/MkGrid.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import { GridSetting } from '@/components/grid/grid.js';
 | 
			
		||||
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
 | 
			
		||||
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
 | 
			
		||||
import type { GridSetting } from '@/components/grid/grid.js';
 | 
			
		||||
 | 
			
		||||
function setupGrid(): GridSetting {
 | 
			
		||||
	return {
 | 
			
		||||
@@ -94,9 +85,4 @@ const filteredLogs = computed(() => {
 | 
			
		||||
	const forceShowing = showingSuccessLogs.value;
 | 
			
		||||
	return logs.value.filter((log) => forceShowing || log.failed);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style module lang="scss">
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
						</MkInput>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<hr>
 | 
			
		||||
 | 
			
		||||
					<MkFolder :spacerMax="8" :spacerMin="8">
 | 
			
		||||
						<template #icon><i class="ti ti-arrows-sort"></i></template>
 | 
			
		||||
						<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
 | 
			
		||||
@@ -74,6 +76,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
						/>
 | 
			
		||||
					</MkFolder>
 | 
			
		||||
 | 
			
		||||
					<MkInput
 | 
			
		||||
						v-model="queryLimit"
 | 
			
		||||
						type="number"
 | 
			
		||||
						:max="100"
 | 
			
		||||
					>
 | 
			
		||||
						<template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchLimit }}</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
					<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
 | 
			
		||||
						<MkButton primary @click="onSearchRequest">
 | 
			
		||||
							{{ i18n.ts.search }}
 | 
			
		||||
@@ -85,7 +95,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<XRegisterLogsFolder :logs="requestLogs"/>
 | 
			
		||||
			<MkFolder>
 | 
			
		||||
				<template #icon><i class="ti ti-notes"></i></template>
 | 
			
		||||
				<template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
 | 
			
		||||
				<template #caption>
 | 
			
		||||
					{{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
 | 
			
		||||
				</template>
 | 
			
		||||
				<XRegisterLogs :logs="requestLogs"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
 | 
			
		||||
			<template v-else>
 | 
			
		||||
@@ -139,7 +156,7 @@ import {
 | 
			
		||||
} from '@/pages/admin/custom-emojis-manager.impl.js';
 | 
			
		||||
import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
 | 
			
		||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { GridSetting } from '@/components/grid/grid.js';
 | 
			
		||||
import { deviceKind } from '@/scripts/device-kind.js';
 | 
			
		||||
@@ -246,6 +263,7 @@ const queryHost = ref<string | null>(null);
 | 
			
		||||
const queryLicense = ref<string | null>(null);
 | 
			
		||||
const queryUri = ref<string | null>(null);
 | 
			
		||||
const queryPublicUrl = ref<string | null>(null);
 | 
			
		||||
const queryLimit = ref<number>(25);
 | 
			
		||||
const previousQuery = ref<string | undefined>(undefined);
 | 
			
		||||
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
 | 
			
		||||
const requestLogs = ref<RequestLogItem[]>([]);
 | 
			
		||||
@@ -325,7 +343,7 @@ async function importEmojis(targets: GridItem[]) {
 | 
			
		||||
	if (failedItems.length > 0) {
 | 
			
		||||
		await os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
 | 
			
		||||
			title: i18n.ts.somethingHappened,
 | 
			
		||||
			text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
@@ -355,7 +373,7 @@ async function refreshCustomEmojis() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', {
 | 
			
		||||
		limit: 100,
 | 
			
		||||
		limit: queryLimit.value,
 | 
			
		||||
		query: query,
 | 
			
		||||
		page: currentPage.value,
 | 
			
		||||
		sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`) as never[],
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<!-- コンテナが入れ子になるのでz-indexが被らないよう大きめの数値を設定する-->
 | 
			
		||||
	<MkStickyContainer :headerZIndex="2000">
 | 
			
		||||
	<MkStickyContainer>
 | 
			
		||||
		<template #header>
 | 
			
		||||
			<MkPageHeader v-model:tab="headerTab" :tabs="headerTabs"/>
 | 
			
		||||
		</template>
 | 
			
		||||
		<XGridLocalComponent v-if="headerTab === 'local'"/>
 | 
			
		||||
		<XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/>
 | 
			
		||||
		<XGridRemoteComponent v-else/>
 | 
			
		||||
	</MkStickyContainer>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -40,5 +39,13 @@ const headerTabs = computed(() => [{
 | 
			
		||||
definePageMetadata(computed(() => ({
 | 
			
		||||
	title: i18n.ts.customEmojis,
 | 
			
		||||
	icon: 'ti ti-icons',
 | 
			
		||||
	needWideArea: true,
 | 
			
		||||
})));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="css" module>
 | 
			
		||||
.local {
 | 
			
		||||
	height: calc(100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom));
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
 | 
			
		||||
import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 | 
			
		||||
import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { lookup } from '@/scripts/lookup.js';
 | 
			
		||||
@@ -55,7 +56,7 @@ const indexInfo = {
 | 
			
		||||
 | 
			
		||||
provide('shouldOmitHeaderTitle', false);
 | 
			
		||||
 | 
			
		||||
const INFO = ref(indexInfo);
 | 
			
		||||
const INFO = ref<PageMetadata>(indexInfo);
 | 
			
		||||
const childInfo = ref<null | PageMetadata>(null);
 | 
			
		||||
const narrow = ref(false);
 | 
			
		||||
const view = ref(null);
 | 
			
		||||
@@ -81,7 +82,7 @@ const ro = new ResizeObserver((entries, observer) => {
 | 
			
		||||
	narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const menuDef = computed(() => [{
 | 
			
		||||
const menuDef = computed<SuperMenuDef[]>(() => [{
 | 
			
		||||
	title: i18n.ts.quickAction,
 | 
			
		||||
	items: [{
 | 
			
		||||
		type: 'button',
 | 
			
		||||
@@ -89,7 +90,7 @@ const menuDef = computed(() => [{
 | 
			
		||||
		text: i18n.ts.lookup,
 | 
			
		||||
		action: adminLookup,
 | 
			
		||||
	}, ...(instance.disableRegistration ? [{
 | 
			
		||||
		type: 'button',
 | 
			
		||||
		type: 'button' as const,
 | 
			
		||||
		icon: 'ti ti-user-plus',
 | 
			
		||||
		text: i18n.ts.createInviteCode,
 | 
			
		||||
		action: invite,
 | 
			
		||||
@@ -333,12 +334,14 @@ defineExpose({
 | 
			
		||||
		height: 100%;
 | 
			
		||||
 | 
			
		||||
		> .nav {
 | 
			
		||||
			position: sticky;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			width: 32%;
 | 
			
		||||
			max-width: 280px;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			border-right: solid 0.5px var(--MI_THEME-divider);
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			height: 100dvh;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .main {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user