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

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;
export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number];
export const fetchEmojisSortKeys = [
'id',
'updatedAt',
'name',
'host',
'uri',
'publicUrl',
'type',
'aliases',
'category',
'license',
'isSensitive',
'localOnly',
'roleIdsThatCanBeUsedThisEmojiAsReaction',
'+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',
] as const;
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()
export class CustomEmojiService implements OnApplicationShutdown {
@ -449,7 +436,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
@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>(
query: string,
builder: SelectQueryBuilder<T>,
@ -565,17 +578,19 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.id < :untilId', { untilId: params.untilId });
}
if (params?.sort && params.sort.length > 0) {
for (const sort of params.sort) {
builder.addOrderBy(`emoji.${sort.key}`, sort.direction);
if (opts?.sortKeys && opts.sortKeys.length > 0) {
for (const sortKey of opts.sortKeys) {
const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
const key = sortKey.replace(/^[+-]/, '');
builder.addOrderBy(`emoji.${key}`, direction);
}
} else {
builder.addOrderBy('emoji.id', 'DESC');
}
const limit = params?.limit ?? 10;
if (params?.page) {
builder.skip((params.page - 1) * limit);
const limit = opts?.limit ?? 10;
if (opts?.page) {
builder.skip((opts.page - 1) * limit);
}
builder.take(limit);

View file

@ -6,7 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.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 = {
tags: ['admin'],
@ -52,7 +52,11 @@ export const paramDef = {
license: { type: 'string' },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' },
hostType: {
type: 'string',
enum: fetchEmojisHostTypes,
default: 'all',
},
roleIds: {
type: 'array',
items: { type: 'string', format: 'misskey:id' },
@ -63,37 +67,12 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
page: { type: 'integer' },
sort: {
sortKeys: {
type: 'array',
default: ['-id'],
items: {
type: 'object',
properties: {
key: {
type: 'string',
enum: [
'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'],
type: 'string',
enum: fetchEmojisSortKeys,
},
},
},
@ -107,37 +86,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const params: FetchEmojisParams = {};
if (ps.query) {
params.query = {
updatedAtFrom: ps.query.updatedAtFrom,
updatedAtTo: ps.query.updatedAtTo,
name: ps.query.name,
host: ps.query.host,
uri: ps.query.uri,
publicUrl: ps.query.publicUrl,
type: ps.query.type,
aliases: ps.query.aliases,
category: ps.query.category,
license: ps.query.license,
isSensitive: ps.query.isSensitive,
localOnly: ps.query.localOnly,
hostType: ps.query.hostType,
roleIds: ps.query.roleIds,
};
}
params.sinceId = ps.sinceId;
params.untilId = ps.untilId;
params.limit = ps.limit;
params.page = ps.page;
params.sort = ps.sort?.map(it => ({
key: it.key,
direction: it.direction,
}));
const result = await this.customEmojiService.fetchEmojis(params);
const q = ps.query;
const result = await this.customEmojiService.fetchEmojis(
{
query: {
updatedAtFrom: q?.updatedAtFrom,
updatedAtTo: q?.updatedAtTo,
name: q?.name,
host: q?.host,
uri: q?.uri,
publicUrl: q?.publicUrl,
type: q?.type,
aliases: q?.aliases,
category: q?.category,
license: q?.license,
isSensitive: q?.isSensitive,
localOnly: q?.localOnly,
hostType: q?.hostType,
roleIds: q?.roleIds,
},
sinceId: ps.sinceId,
untilId: ps.untilId,
},
{
limit: ps.limit,
page: ps.page,
sortKeys: ps.sortKeys,
},
);
return {
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.sortOrderAreaTags">
<MkTagItem
v-for="order in sortOrders"
v-for="order in currentOrders"
: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'"
:content="order.key"
@click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order.key)"
@exButtonClick="onRemoveSortOrderButtonClicked(order)"
/>
</div>
<MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
@ -22,58 +22,57 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" generic="T extends string">
import { toRefs } from 'vue';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkTagItem from '@/components/MkTagItem.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 * as os from '@/os.js';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
const emit = defineEmits<{
(ev: 'update', sortOrders: GridSortOrder[]): void;
(ev: 'update', sortOrders: SortOrder<T>[]): void;
}>();
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) {
case 'ASC':
order.direction = 'DESC';
case '+':
order.direction = '-';
break;
case 'DESC':
order.direction = 'ASC';
case '-':
order.direction = '+';
break;
}
emitOrder(sortOrders.value);
emitOrder(currentOrders.value);
}
function onAddSortOrderButtonClicked(ev: MouseEvent) {
const menuItems: MenuItem[] = gridSortOrderKeys
.filter(key => !sortOrders.value.map(it => it.key).includes(key))
const menuItems: MenuItem[] = props.baseOrderKeyNames
.filter(baseKey => !currentOrders.value.map(it => it.key).includes(baseKey))
.map(it => {
return {
text: it,
action: () => {
emitOrder([...sortOrders.value, { key: it, direction: 'ASC' }]);
emitOrder([...currentOrders.value, { key: it, direction: '+' }]);
},
};
});
os.contextMenu(menuItems, ev);
}
function onRemoveSortOrderButtonClicked(key: GridSortOrderKey) {
emitOrder(sortOrders.value.filter(it => it.key !== key));
function onRemoveSortOrderButtonClicked(order: SortOrder<T>) {
emitOrder(currentOrders.value.filter(it => it.key !== order.key));
}
function emitOrder(sortOrders: GridSortOrder[]) {
function emitOrder(sortOrders: SortOrder<T>[]) {
emit('update', sortOrders);
}

View file

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

View file

@ -120,7 +120,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
<MkSortOrderEditor :sortOrders="sortOrders" @update="onSortOrderUpdate"/>
<MkSortOrderEditor
:baseOrderKeyNames="gridSortOrderKeys"
:currentOrders="sortOrders"
@update="onSortOrderUpdate"
/>
</MkFolder>
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
@ -163,7 +167,9 @@ import * as os from '@/os.js';
import {
emptyStrToEmptyArray,
emptyStrToNull,
emptyStrToUndefined, GridSortOrder,
emptyStrToUndefined,
GridSortOrderKey,
gridSortOrderKeys,
RequestLogItem,
roleIdsParser,
} 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 { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
type GridItem = {
checked: boolean;
@ -370,7 +377,7 @@ const querySensitive = ref<string | null>(null);
const queryLocalOnly = ref<string | null>(null);
const queryRoles = ref<{ id: string, name: string }[]>([]);
const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]);
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]);
const gridItems = ref<GridItem[]>([]);
@ -499,7 +506,7 @@ async function onQueryRolesEditClicked() {
queryRoles.value = result.result;
}
function onSortOrderUpdate(_sortOrders: GridSortOrder[]) {
function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
sortOrders.value = _sortOrders;
}
@ -573,7 +580,7 @@ async function refreshCustomEmojis() {
query: query,
limit: limit,
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">
<template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
<MkSortOrderEditor :sortOrders="sortOrders" @update="onSortOrderChanged"/>
<MkSortOrderEditor
:baseOrderKeyNames="gridSortOrderKeys"
:currentOrders="sortOrders"
@update="onSortOrderUpdate"
/>
</MkFolder>
<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"/>
<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>
@ -93,7 +101,12 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.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 MkFolder from '@/components/MkFolder.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 MkPagingButtons from '@/components/MkPagingButtons.vue';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
type GridItem = {
checked: boolean;
@ -163,14 +177,14 @@ const queryHost = ref<string | null>(null);
const queryUri = ref<string | null>(null);
const queryPublicUrl = ref<string | null>(null);
const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]);
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]);
const gridItems = ref<GridItem[]>([]);
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
function onSortOrderChanged(_sortOrders: GridSortOrder[]) {
function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
sortOrders.value = _sortOrders;
}
@ -272,7 +286,7 @@ async function refreshCustomEmojis() {
limit: 100,
query: query,
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 */
limit?: number;
page?: number;
sort?: ({
/**
* @default id
* @enum {string}
*/
key: 'id' | 'updatedAt' | 'name' | 'host' | 'uri' | 'publicUrl' | 'type' | 'aliases' | 'category' | 'license' | 'isSensitive' | 'localOnly' | 'roleIdsThatCanBeUsedThisEmojiAsReaction';
/**
* @default DESC
* @enum {string}
*/
direction: 'ASC' | 'DESC';
})[];
/**
* @default [
* "-id"
* ]
*/
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')[];
};
};
};