ソートキーの指定方法を他と合わせた

This commit is contained in:
samunohito 2024-07-27 10:19:02 +09:00
parent fdf20a6605
commit ca2da9d296
8 changed files with 169 additions and 158 deletions

View File

@ -27,47 +27,34 @@ export const fetchEmojisHostTypes = [
] as const; ] as const;
export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number]; export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number];
export const fetchEmojisSortKeys = [ export const fetchEmojisSortKeys = [
'id', '+id',
'updatedAt', '-id',
'name', '+updatedAt',
'host', '-updatedAt',
'uri', '+name',
'publicUrl', '-name',
'type', '+host',
'aliases', '-host',
'category', '+uri',
'license', '-uri',
'isSensitive', '+publicUrl',
'localOnly', '-publicUrl',
'roleIdsThatCanBeUsedThisEmojiAsReaction', '+type',
'-type',
'+aliases',
'-aliases',
'+category',
'-category',
'+license',
'-license',
'+isSensitive',
'-isSensitive',
'+localOnly',
'-localOnly',
'+roleIdsThatCanBeUsedThisEmojiAsReaction',
'-roleIdsThatCanBeUsedThisEmojiAsReaction',
] as const; ] as const;
export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number]; export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number];
export type FetchEmojisParams = {
query?: {
updatedAtFrom?: string;
updatedAtTo?: string;
name?: string;
host?: string;
uri?: string;
publicUrl?: string;
type?: string;
aliases?: string;
category?: string;
license?: string;
isSensitive?: boolean;
localOnly?: boolean;
hostType?: FetchEmojisHostTypes;
roleIds?: string[];
},
sinceId?: string;
untilId?: string;
limit?: number;
page?: number;
sort?: {
key: FetchEmojisSortKeys;
direction: 'ASC' | 'DESC';
}[]
}
@Injectable() @Injectable()
export class CustomEmojiService implements OnApplicationShutdown { export class CustomEmojiService implements OnApplicationShutdown {
@ -449,7 +436,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async fetchEmojis(params?: FetchEmojisParams) { public async fetchEmojis(
params?: {
query?: {
updatedAtFrom?: string;
updatedAtTo?: string;
name?: string;
host?: string;
uri?: string;
publicUrl?: string;
type?: string;
aliases?: string;
category?: string;
license?: string;
isSensitive?: boolean;
localOnly?: boolean;
hostType?: FetchEmojisHostTypes;
roleIds?: string[];
},
sinceId?: string;
untilId?: string;
},
opts?: {
limit?: number;
page?: number;
sortKeys?: FetchEmojisSortKeys[]
},
) {
function multipleWordsToQuery<T extends ObjectLiteral>( function multipleWordsToQuery<T extends ObjectLiteral>(
query: string, query: string,
builder: SelectQueryBuilder<T>, builder: SelectQueryBuilder<T>,
@ -565,17 +578,19 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.id < :untilId', { untilId: params.untilId }); builder.andWhere('emoji.id < :untilId', { untilId: params.untilId });
} }
if (params?.sort && params.sort.length > 0) { if (opts?.sortKeys && opts.sortKeys.length > 0) {
for (const sort of params.sort) { for (const sortKey of opts.sortKeys) {
builder.addOrderBy(`emoji.${sort.key}`, sort.direction); const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
const key = sortKey.replace(/^[+-]/, '');
builder.addOrderBy(`emoji.${key}`, direction);
} }
} else { } else {
builder.addOrderBy('emoji.id', 'DESC'); builder.addOrderBy('emoji.id', 'DESC');
} }
const limit = params?.limit ?? 10; const limit = opts?.limit ?? 10;
if (params?.page) { if (opts?.page) {
builder.skip((params.page - 1) * limit); builder.skip((opts.page - 1) * limit);
} }
builder.take(limit); builder.take(limit);

View File

@ -6,7 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { CustomEmojiService, FetchEmojisParams } from '@/core/CustomEmojiService.js'; import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -52,7 +52,11 @@ export const paramDef = {
license: { type: 'string' }, license: { type: 'string' },
isSensitive: { type: 'boolean' }, isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' }, localOnly: { type: 'boolean' },
hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' }, hostType: {
type: 'string',
enum: fetchEmojisHostTypes,
default: 'all',
},
roleIds: { roleIds: {
type: 'array', type: 'array',
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
@ -63,37 +67,12 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
page: { type: 'integer' }, page: { type: 'integer' },
sort: { sortKeys: {
type: 'array', type: 'array',
default: ['-id'],
items: { items: {
type: 'object',
properties: {
key: {
type: 'string', type: 'string',
enum: [ enum: fetchEmojisSortKeys,
'id',
'updatedAt',
'name',
'host',
'uri',
'publicUrl',
'type',
'aliases',
'category',
'license',
'isSensitive',
'localOnly',
'roleIdsThatCanBeUsedThisEmojiAsReaction',
],
default: 'id',
},
direction: {
type: 'string',
enum: ['ASC', 'DESC'],
default: 'DESC',
},
},
required: ['key', 'direction'],
}, },
}, },
}, },
@ -107,37 +86,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const params: FetchEmojisParams = {}; const q = ps.query;
const result = await this.customEmojiService.fetchEmojis(
if (ps.query) { {
params.query = { query: {
updatedAtFrom: ps.query.updatedAtFrom, updatedAtFrom: q?.updatedAtFrom,
updatedAtTo: ps.query.updatedAtTo, updatedAtTo: q?.updatedAtTo,
name: ps.query.name, name: q?.name,
host: ps.query.host, host: q?.host,
uri: ps.query.uri, uri: q?.uri,
publicUrl: ps.query.publicUrl, publicUrl: q?.publicUrl,
type: ps.query.type, type: q?.type,
aliases: ps.query.aliases, aliases: q?.aliases,
category: ps.query.category, category: q?.category,
license: ps.query.license, license: q?.license,
isSensitive: ps.query.isSensitive, isSensitive: q?.isSensitive,
localOnly: ps.query.localOnly, localOnly: q?.localOnly,
hostType: ps.query.hostType, hostType: q?.hostType,
roleIds: ps.query.roleIds, roleIds: q?.roleIds,
}; },
} sinceId: ps.sinceId,
untilId: ps.untilId,
params.sinceId = ps.sinceId; },
params.untilId = ps.untilId; {
params.limit = ps.limit; limit: ps.limit,
params.page = ps.page; page: ps.page,
params.sort = ps.sort?.map(it => ({ sortKeys: ps.sortKeys,
key: it.key, },
direction: it.direction, );
}));
const result = await this.customEmojiService.fetchEmojis(params);
return { return {
emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis), emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),

View File

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type SortOrderDirection = '+' | '-'
export type SortOrder<T extends string> = {
key: T;
direction: SortOrderDirection;
}

View File

@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.sortOrderArea"> <div :class="$style.sortOrderArea">
<div :class="$style.sortOrderAreaTags"> <div :class="$style.sortOrderAreaTags">
<MkTagItem <MkTagItem
v-for="order in sortOrders" v-for="order in currentOrders"
:key="order.key" :key="order.key"
:iconClass="order.direction === 'ASC' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'" :iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
:exButtonIconClass="'ti ti-x'" :exButtonIconClass="'ti ti-x'"
:content="order.key" :content="order.key"
@click="onToggleSortOrderButtonClicked(order)" @click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order.key)" @exButtonClick="onRemoveSortOrderButtonClicked(order)"
/> />
</div> </div>
<MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked"> <MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
@ -22,58 +22,57 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" generic="T extends string">
import { toRefs } from 'vue'; import { toRefs } from 'vue';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkTagItem from '@/components/MkTagItem.vue'; import MkTagItem from '@/components/MkTagItem.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { GridSortOrder, GridSortOrderKey, gridSortOrderKeys } from '@/pages/admin/custom-emojis-manager.impl.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update', sortOrders: GridSortOrder[]): void; (ev: 'update', sortOrders: SortOrder<T>[]): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
sortOrders: GridSortOrder[]; baseOrderKeyNames: T[];
currentOrders: SortOrder<T>[];
}>(); }>();
const { sortOrders } = toRefs(props); const { currentOrders } = toRefs(props);
function onToggleSortOrderButtonClicked(order: GridSortOrder) { function onToggleSortOrderButtonClicked(order: SortOrder<T>) {
switch (order.direction) { switch (order.direction) {
case 'ASC': case '+':
order.direction = 'DESC'; order.direction = '-';
break; break;
case 'DESC': case '-':
order.direction = 'ASC'; order.direction = '+';
break; break;
} }
emitOrder(sortOrders.value); emitOrder(currentOrders.value);
} }
function onAddSortOrderButtonClicked(ev: MouseEvent) { function onAddSortOrderButtonClicked(ev: MouseEvent) {
const menuItems: MenuItem[] = gridSortOrderKeys const menuItems: MenuItem[] = props.baseOrderKeyNames
.filter(key => !sortOrders.value.map(it => it.key).includes(key)) .filter(baseKey => !currentOrders.value.map(it => it.key).includes(baseKey))
.map(it => { .map(it => {
return { return {
text: it, text: it,
action: () => { action: () => {
emitOrder([...sortOrders.value, { key: it, direction: 'ASC' }]); emitOrder([...currentOrders.value, { key: it, direction: '+' }]);
}, },
}; };
}); });
os.contextMenu(menuItems, ev); os.contextMenu(menuItems, ev);
} }
function onRemoveSortOrderButtonClicked(key: GridSortOrderKey) { function onRemoveSortOrderButtonClicked(order: SortOrder<T>) {
emitOrder(sortOrders.value.filter(it => it.key !== key)); emitOrder(currentOrders.value.filter(it => it.key !== order.key));
} }
function emitOrder(sortOrders: GridSortOrder[]) { function emitOrder(sortOrders: SortOrder<T>[]) {
emit('update', sortOrders); emit('update', sortOrders);
} }

View File

@ -25,11 +25,6 @@ export const gridSortOrderKeys = [
]; ];
export type GridSortOrderKey = typeof gridSortOrderKeys[number]; export type GridSortOrderKey = typeof gridSortOrderKeys[number];
export type GridSortOrder = {
key: GridSortOrderKey;
direction: 'ASC' | 'DESC';
}
export function emptyStrToUndefined(value: string | null) { export function emptyStrToUndefined(value: string | null) {
return value ? value : undefined; return value ? value : undefined;
} }

View File

@ -120,7 +120,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :spacerMax="8" :spacerMin="8"> <MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template> <template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
<MkSortOrderEditor :sortOrders="sortOrders" @update="onSortOrderUpdate"/> <MkSortOrderEditor
:baseOrderKeyNames="gridSortOrderKeys"
:currentOrders="sortOrders"
@update="onSortOrderUpdate"
/>
</MkFolder> </MkFolder>
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]"> <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
@ -163,7 +167,9 @@ import * as os from '@/os.js';
import { import {
emptyStrToEmptyArray, emptyStrToEmptyArray,
emptyStrToNull, emptyStrToNull,
emptyStrToUndefined, GridSortOrder, emptyStrToUndefined,
GridSortOrderKey,
gridSortOrderKeys,
RequestLogItem, RequestLogItem,
roleIdsParser, roleIdsParser,
} from '@/pages/admin/custom-emojis-manager.impl.js'; } from '@/pages/admin/custom-emojis-manager.impl.js';
@ -183,6 +189,7 @@ import { GridSetting } from '@/components/grid/grid.js';
import { selectFile } from '@/scripts/select-file.js'; import { selectFile } from '@/scripts/select-file.js';
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js'; import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -370,7 +377,7 @@ const querySensitive = ref<string | null>(null);
const queryLocalOnly = ref<string | null>(null); const queryLocalOnly = ref<string | null>(null);
const queryRoles = ref<{ id: string, name: string }[]>([]); const queryRoles = ref<{ id: string, name: string }[]>([]);
const previousQuery = ref<string | undefined>(undefined); const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]); const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]); const requestLogs = ref<RequestLogItem[]>([]);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
@ -499,7 +506,7 @@ async function onQueryRolesEditClicked() {
queryRoles.value = result.result; queryRoles.value = result.result;
} }
function onSortOrderUpdate(_sortOrders: GridSortOrder[]) { function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
sortOrders.value = _sortOrders; sortOrders.value = _sortOrders;
} }
@ -573,7 +580,7 @@ async function refreshCustomEmojis() {
query: query, query: query,
limit: limit, limit: limit,
page: currentPage.value, page: currentPage.value,
sort: sortOrders.value.map(({ key, direction }) => ({ key: key as any, direction })), sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`),
}), }),
() => { () => {
}, },

View File

@ -56,7 +56,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :spacerMax="8" :spacerMin="8"> <MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template> <template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
<MkSortOrderEditor :sortOrders="sortOrders" @update="onSortOrderChanged"/> <MkSortOrderEditor
:baseOrderKeyNames="gridSortOrderKeys"
:currentOrders="sortOrders"
@update="onSortOrderUpdate"
/>
</MkFolder> </MkFolder>
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]"> <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
@ -79,7 +83,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
<div v-if="gridItems.length > 0" class="_gaps" :class="$style.buttons"> <div v-if="gridItems.length > 0" class="_gaps" :class="$style.buttons">
<MkButton primary @click="onImportClicked">{{ i18n.ts._customEmojisManager._remote.importEmojisButton }}</MkButton> <MkButton primary @click="onImportClicked">
{{
i18n.ts._customEmojisManager._remote.importEmojisButton
}}
</MkButton>
</div> </div>
</div> </div>
</div> </div>
@ -93,7 +101,12 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { emptyStrToUndefined, GridSortOrder, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import {
emptyStrToUndefined,
GridSortOrderKey,
gridSortOrderKeys,
RequestLogItem,
} from '@/pages/admin/custom-emojis-manager.impl.js';
import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue'; import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
@ -102,6 +115,7 @@ import { GridSetting } from '@/components/grid/grid.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue'; import MkPagingButtons from '@/components/MkPagingButtons.vue';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -163,14 +177,14 @@ const queryHost = ref<string | null>(null);
const queryUri = ref<string | null>(null); const queryUri = ref<string | null>(null);
const queryPublicUrl = ref<string | null>(null); const queryPublicUrl = ref<string | null>(null);
const previousQuery = ref<string | undefined>(undefined); const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]); const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]); const requestLogs = ref<RequestLogItem[]>([]);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind)); const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
function onSortOrderChanged(_sortOrders: GridSortOrder[]) { function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
sortOrders.value = _sortOrders; sortOrders.value = _sortOrders;
} }
@ -272,7 +286,7 @@ async function refreshCustomEmojis() {
limit: 100, limit: 100,
query: query, query: query,
page: currentPage.value, page: currentPage.value,
sort: sortOrders.value.map(({ key, direction }) => ({ key: key as any, direction })), sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`),
}), }),
() => { () => {
}, },

View File

@ -7681,18 +7681,12 @@ export type operations = {
/** @default 10 */ /** @default 10 */
limit?: number; limit?: number;
page?: number; page?: number;
sort?: ({
/** /**
* @default id * @default [
* @enum {string} * "-id"
* ]
*/ */
key: 'id' | 'updatedAt' | 'name' | 'host' | 'uri' | 'publicUrl' | 'type' | 'aliases' | 'category' | 'license' | 'isSensitive' | 'localOnly' | 'roleIdsThatCanBeUsedThisEmojiAsReaction'; sortKeys?: ('+id' | '-id' | '+updatedAt' | '-updatedAt' | '+name' | '-name' | '+host' | '-host' | '+uri' | '-uri' | '+publicUrl' | '-publicUrl' | '+type' | '-type' | '+aliases' | '-aliases' | '+category' | '-category' | '+license' | '-license' | '+isSensitive' | '-isSensitive' | '+localOnly' | '-localOnly' | '+roleIdsThatCanBeUsedThisEmojiAsReaction' | '-roleIdsThatCanBeUsedThisEmojiAsReaction')[];
/**
* @default DESC
* @enum {string}
*/
direction: 'ASC' | 'DESC';
})[];
}; };
}; };
}; };