support pagination

This commit is contained in:
samunohito 2024-02-05 21:30:06 +09:00
parent f8529a01b9
commit 041449e962
4 changed files with 163 additions and 84 deletions

View File

@ -455,7 +455,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.name LIKE :name', { name: `%${q.name}%` }); builder.andWhere('emoji.name LIKE :name', { name: `%${q.name}%` });
} }
if (q.hostType === 'local') { if (q.hostType === 'local') {
builder.andWhere('emoji.host LIKE :host', { host: `%${q.host}%` }); builder.andWhere('emoji.host IS NULL');
} else { } else {
if (q.host) { if (q.host) {
// noIndexScan // noIndexScan

View File

@ -0,0 +1,112 @@
<template>
<div :class="$style.root">
<MkButton primary :disabled="min === current" @click="onToPrevButtonClicked">&lt;</MkButton>
<div :class="$style.buttons">
<div v-if="prevDotVisible" :class="$style.headTailButtons">
<MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
<span class="ti ti-dots"/>
</div>
<MkButton
v-for="i in buttonRanges" :key="i"
:disabled="current === i"
@click="onNumberButtonClicked(i)"
>
{{ i }}
</MkButton>
<div v-if="nextDotVisible" :class="$style.headTailButtons">
<span class="ti ti-dots"/>
<MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
</div>
</div>
<MkButton primary :disabled="max === current" @click="onToNextButtonClicked">&gt;</MkButton>
</div>
</template>
<script setup lang="ts">
import { computed, toRefs } from 'vue';
import MkButton from '@/components/MkButton.vue';
const min = 1;
const emit = defineEmits<{
(ev: 'pageChanged', pageNumber: number): void;
}>();
const props = defineProps<{
current: number;
max: number;
buttonCount: number;
}>();
const { current, max, buttonCount } = toRefs(props);
const buttonCountHalf = computed(() => Math.floor(buttonCount.value / 2));
const buttonCountStart = computed(() => Math.min(Math.max(min, current.value - buttonCountHalf.value), max.value - buttonCount.value + 1));
const buttonRanges = computed(() => Array.from({ length: buttonCount.value }, (_, i) => buttonCountStart.value + i));
const prevDotVisible = computed(() => (current.value - 1 > buttonCountHalf.value) || (max.value < buttonCount.value));
const nextDotVisible = computed(() => (current.value < max.value - buttonCountHalf.value) || (max.value < buttonCount.value));
function onNumberButtonClicked(pageNumber: number) {
emit('pageChanged', pageNumber);
}
function onToHeadButtonClicked() {
emit('pageChanged', min);
}
function onToPrevButtonClicked() {
const newPageNumber = current.value <= min ? min : current.value - 1;
emit('pageChanged', newPageNumber);
}
function onToNextButtonClicked() {
const newPageNumber = current.value >= max.value ? max.value : current.value + 1;
emit('pageChanged', newPageNumber);
}
function onToTailButtonClicked() {
emit('pageChanged', max.value);
}
</script>
<style module lang="scss">
.root {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
button {
border-radius: 9999px;
min-width: 2.5em;
min-height: 2.5em;
max-width: 2.5em;
max-height: 2.5em;
padding: 4px;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.headTailButtons {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
span {
font-size: 0.75em;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div v-if="gridItems.length === 0" style="text-align: center"> <div v-if="false" style="text-align: center">
登録された絵文字はありません 登録された絵文字はありません
</div> </div>
<div v-else class="_gaps"> <div v-else class="_gaps">
@ -17,12 +17,9 @@
<MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/> <MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/>
</div> </div>
<div class="_gaps"> <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
<div :class="$style.pages">
<button @click="onLatestButtonClicked">&lt;</button>
<button @click="onOldestButtonClicked">&gt;</button>
</div>
<div class="_gaps">
<div :class="$style.buttons"> <div :class="$style.buttons">
<MkButton danger style="margin-right: auto" @click="onDeleteClicked">{{ i18n.ts.delete }}</MkButton> <MkButton danger style="margin-right: auto" @click="onDeleteClicked">{{ i18n.ts.delete }}</MkButton>
<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateClicked">{{ i18n.ts.update }}</MkButton> <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateClicked">{{ i18n.ts.update }}</MkButton>
@ -34,7 +31,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, toRefs, watch } from 'vue'; import { computed, onMounted, ref, toRefs, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { fromEmojiDetailedAdmin, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { fromEmojiDetailedAdmin, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
@ -56,6 +53,7 @@ import {
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import { GridSetting } from '@/components/grid/grid.js'; import { GridSetting } from '@/components/grid/grid.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
const gridSetting: GridSetting = { const gridSetting: GridSetting = {
rowNumberVisible: true, rowNumberVisible: true,
@ -76,26 +74,16 @@ const columnSettings: ColumnSetting[] = [
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 }, { bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 },
]; ];
const emit = defineEmits<{ const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
(ev: 'operation:search', query: string, sinceId?: string, untilId?: string): void;
}>();
const props = defineProps<{
customEmojis: Misskey.entities.EmojiDetailedAdmin[];
}>();
const { customEmojis } = toRefs(props);
const query = ref(''); const query = ref('');
const allPages = ref<number>(0);
const currentPage = ref<number>(0);
const previousQuery = ref<string | undefined>(undefined);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
const originGridItems = ref<GridItem[]>([]); const originGridItems = ref<GridItem[]>([]);
const updateButtonDisabled = ref<boolean>(false); const updateButtonDisabled = ref<boolean>(false);
const latest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[0]?.id : undefined);
const oldest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[customEmojis.value.length - 1]?.id : undefined);
watch(customEmojis, refreshGridItems, { immediate: true });
async function onUpdateClicked() { async function onUpdateClicked() {
const _items = gridItems.value; const _items = gridItems.value;
const _originItems = originGridItems.value; const _originItems = originGridItems.value;
@ -144,8 +132,6 @@ async function onUpdateClicked() {
() => {}, () => {},
() => {}, () => {},
); );
emit('operation:search', query.value, undefined, undefined);
} }
async function onDeleteClicked() { async function onDeleteClicked() {
@ -183,8 +169,6 @@ async function onDeleteClicked() {
() => {}, () => {},
() => {}, () => {},
); );
emit('operation:search', query.value, undefined, undefined);
} }
function onResetClicked() { function onResetClicked() {
@ -192,15 +176,11 @@ function onResetClicked() {
} }
function onSearchButtonClicked() { function onSearchButtonClicked() {
emit('operation:search', query.value, undefined, undefined);
} }
async function onLatestButtonClicked() { async function onPageChanged(pageNumber: number) {
emit('operation:search', query.value, latest.value, undefined); currentPage.value = pageNumber;
} await refreshCustomEmojis();
async function onOldestButtonClicked() {
emit('operation:search', query.value, undefined, oldest.value);
} }
function onGridEvent(event: GridEvent, currentState: GridCurrentState) { function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
@ -286,11 +266,44 @@ function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState)
optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState); optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState);
} }
async function refreshCustomEmojis() {
const limit = 10;
const query: Misskey.entities.AdminEmojiV2ListRequest['query'] = {
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,
}),
() => {},
() => {},
);
customEmojis.value = result.emojis;
allPages.value = result.allPages;
previousQuery.value = JSON.stringify(query);
refreshGridItems();
}
function refreshGridItems() { function refreshGridItems() {
gridItems.value = customEmojis.value.map(it => fromEmojiDetailedAdmin(it)); gridItems.value = customEmojis.value.map(it => fromEmojiDetailedAdmin(it));
originGridItems.value = JSON.parse(JSON.stringify(gridItems.value)); originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
} }
onMounted(async () => {
await refreshCustomEmojis();
});
</script> </script>
<style module lang="scss"> <style module lang="scss">
@ -309,20 +322,6 @@ function refreshGridItems() {
resize: vertical; resize: vertical;
} }
.pages {
display: flex;
justify-content: center;
align-items: center;
button {
background-color: var(--buttonBg);
border-radius: 9999px;
border: none;
margin: 0 4px;
padding: 8px;
}
}
.buttons { .buttons {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;

View File

@ -6,53 +6,21 @@
</MkTab> </MkTab>
<div> <div>
<XListComponent <XListComponent v-if="modeTab === 'list'"/>
v-if="modeTab === 'list'" <XRegisterComponent v-else/>
:customEmojis="customEmojis"
@operation:search="onOperationSearch"
/>
<XRegisterComponent
v-else
@operation:registered="onOperationRegistered"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkTab from '@/components/MkTab.vue'; import MkTab from '@/components/MkTab.vue';
import XListComponent from '@/pages/admin/custom-emojis-grid.local.list.vue'; import XListComponent from '@/pages/admin/custom-emojis-grid.local.list.vue';
import XRegisterComponent from '@/pages/admin/custom-emojis-grid.local.register.vue'; import XRegisterComponent from '@/pages/admin/custom-emojis-grid.local.register.vue';
type PageMode = 'list' | 'register'; type PageMode = 'list' | 'register';
const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const modeTab = ref<PageMode>('list'); const modeTab = ref<PageMode>('list');
const query = ref<string>();
async function refreshCustomEmojis(query?: string, sinceId?: string, untilId?: string) {
const emojis = await misskeyApi('admin/emoji/v2/list', {
limit: 100,
}).then(it => it.emojis);
customEmojis.value = emojis;
}
async function onOperationSearch(q: string, sinceId?: string, untilId?: string) {
query.value = q;
await refreshCustomEmojis(q, sinceId, untilId);
}
async function onOperationRegistered() {
await refreshCustomEmojis(query.value);
}
onMounted(async () => {
await refreshCustomEmojis();
});
</script> </script>
<style lang="scss"> <style lang="scss">