refine remote page

This commit is contained in:
samunohito 2024-02-18 13:34:01 +09:00
parent 1cdf1bf4c9
commit ac943195cf
3 changed files with 227 additions and 75 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div v-if="false" style="text-align: center"> <div v-if="gridItems.value === 0" style="text-align: center">
登録された絵文字はありません 登録された絵文字はありません
</div> </div>
<div v-else class="_gaps"> <div v-else class="_gaps">
@ -474,7 +474,6 @@ async function onQueryRolesEditClicked() {
} }
function onToggleSortOrderButtonClicked(order: GridSortOrder) { function onToggleSortOrderButtonClicked(order: GridSortOrder) {
console.log(order);
switch (order.direction) { switch (order.direction) {
case 'ASC': case 'ASC':
order.direction = 'DESC'; order.direction = 'DESC';

View File

@ -23,19 +23,6 @@ type PageMode = 'list' | 'register';
const modeTab = ref<PageMode>('list'); const modeTab = ref<PageMode>('list');
</script> </script>
<style lang="scss">
.emoji-grid-row-edited {
background-color: var(--ag-advanced-filter-column-pill-color);
}
.emoji-grid-item-image {
width: auto;
height: 26px;
max-width: 100%;
max-height: 100%;
}
</style>
<style module lang="scss"> <style module lang="scss">
.root { .root {
padding: 16px; padding: 16px;

View File

@ -1,18 +1,60 @@
<template> <template>
<div class="_gaps" :class="$style.root"> <div :class="$style.root">
<div :class="$style.searchArea"> <div class="_gaps">
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off" style="flex: 1"> <MkFolder>
<template #prefix><i class="ti ti-search"></i></template> <template #icon><i class="ti ti-search"></i></template>
<template #label>検索設定</template>
<template #caption>
検索条件を詳細に設定します
</template>
<div class="_gaps">
<div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]">
<MkInput v-model="queryName" :debounce="true" type="search" autocapitalize="off" class="col1 row1">
<template #label>name</template> <template #label>name</template>
</MkInput> </MkInput>
<MkInput v-model="host" :debounce="true" type="search" autocapitalize="off" style="flex: 1"> <MkInput v-model="queryHost" :debounce="true" type="search" autocapitalize="off" class="col2 row1">
<template #prefix><i class="ti ti-cloud-network"></i></template>
<template #label>host</template> <template #label>host</template>
</MkInput> </MkInput>
<MkButton primary style="margin-left: auto;" @click="onSearchButtonClicked"> <MkInput v-model="queryUri" :debounce="true" type="search" autocapitalize="off" class="col1 row2">
{{ i18n.ts.search }} <template #label>uri</template>
</MkInput>
<MkInput v-model="queryPublicUrl" :debounce="true" type="search" autocapitalize="off" class="col2 row2">
<template #label>publicUrl</template>
</MkInput>
</div>
<MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>ソート順</template>
<div :class="$style.sortOrderArea">
<div :class="$style.sortOrderAreaTags">
<MkTagItem
v-for="order in sortOrders"
:key="order.key"
:iconClass="order.direction === 'ASC' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
:exButtonIconClass="'ti ti-x'"
:content="order.key"
@click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order.key)"
/>
</div>
<MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
<span class="ti ti-plus"/>
</MkButton> </MkButton>
</div> </div>
</MkFolder>
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
<MkButton primary @click="onSearchButtonClicked">
{{ i18n.ts.search }}
</MkButton>
<MkButton @click="onQueryResetButtonClicked">
リセット
</MkButton>
</div>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-notes"></i></template> <template #icon><i class="ti ti-notes"></i></template>
@ -24,22 +66,16 @@
<XRegisterLogs :logs="requestLogs"/> <XRegisterLogs :logs="requestLogs"/>
</MkFolder> </MkFolder>
<div v-if="gridItems.length > 0"> <div v-if="gridItems.length > 0" :class="$style.gridArea">
<div :class="$style.gridArea">
<MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/> <MkGrid :data="gridItems" :settings="setupGrid()" @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="$style.buttons"> <div v-if="gridItems.length > 0" class="_gaps" :class="$style.buttons">
<MkButton primary @click="onImportClicked">チェックがついた絵文字をインポート</MkButton> <MkButton primary @click="onImportClicked">チェックがついた絵文字をインポート</MkButton>
</div> </div>
</div> </div>
</div>
</div> </div>
</template> </template>
@ -51,13 +87,18 @@ 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 { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import { emptyStrToUndefined, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import { GridCellValueChangeEvent, GridContext, GridEvent, GridKeyDownEvent } from '@/components/grid/grid-event.js'; import { GridCellValueChangeEvent, GridContext, GridEvent, GridKeyDownEvent } from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.local.logs.vue'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.local.logs.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { GridSetting } from '@/components/grid/grid.js'; import { GridSetting } from '@/components/grid/grid.js';
import MkTagItem from '@/components/MkTagItem.vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { MenuItem } from '@/types/menu.js';
import MkTab from '@/components/MkTab.vue';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -67,6 +108,19 @@ type GridItem = {
host: string; host: string;
} }
const gridSortOrderKeys = [
'name',
'host',
'uri',
'publicUrl',
];
type GridSortOrderKey = typeof gridSortOrderKeys[number];
type GridSortOrder = {
key: GridSortOrderKey;
direction: 'ASC' | 'DESC';
}
function setupGrid(): GridSetting { function setupGrid(): GridSetting {
return { return {
row: { row: {
@ -89,6 +143,8 @@ function setupGrid(): GridSetting {
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' }, { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' }, { bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' },
], ],
cells: { cells: {
contextMenuFactory: (col, row, value, context) => { contextMenuFactory: (col, row, value, context) => {
@ -108,26 +164,68 @@ function setupGrid(): GridSetting {
}; };
} }
const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const allPages = ref<number>(0);
const currentPage = ref<number>(0);
const queryName = ref<string | null>(null);
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 requestLogs = ref<RequestLogItem[]>([]); const requestLogs = ref<RequestLogItem[]>([]);
const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
const query = ref<string>('');
const host = ref<string>('');
const latest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[0]?.id : undefined); const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
const oldest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[customEmojis.value.length - 1]?.id : undefined);
function onToggleSortOrderButtonClicked(order: GridSortOrder) {
switch (order.direction) {
case 'ASC':
order.direction = 'DESC';
break;
case 'DESC':
order.direction = 'ASC';
break;
}
}
function onRemoveSortOrderButtonClicked(key: GridSortOrderKey) {
sortOrders.value = sortOrders.value.filter(it => it.key !== key);
}
function onAddSortOrderButtonClicked(ev: MouseEvent) {
const menuItems: MenuItem[] = gridSortOrderKeys
.filter(key => !sortOrders.value.map(it => it.key).includes(key))
.map(it => {
return {
text: it,
action: () => {
sortOrders.value.push({ key: it, direction: 'ASC' });
},
};
});
os.contextMenu(menuItems, ev);
}
async function onSearchButtonClicked() { async function onSearchButtonClicked() {
await refreshCustomEmojis(query.value, host.value); await refreshCustomEmojis();
}
function onQueryResetButtonClicked() {
queryName.value = null;
queryHost.value = null;
queryUri.value = null;
queryPublicUrl.value = null;
} }
async function onLatestButtonClicked() { async function onLatestButtonClicked() {
await refreshCustomEmojis(query.value, host.value, undefined, latest.value); await refreshCustomEmojis();
} }
async function onOldestButtonClicked() { async function onOldestButtonClicked() {
await refreshCustomEmojis(query.value, host.value, oldest.value, undefined); await refreshCustomEmojis();
} }
async function onImportClicked() { async function onImportClicked() {
@ -201,19 +299,29 @@ async function importEmojis(targets: GridItem[]) {
await refreshCustomEmojis(); await refreshCustomEmojis();
} }
async function refreshCustomEmojis(query?: string, host?: string, sinceId?: string, untilId?: string) { async function refreshCustomEmojis() {
const emojis = await misskeyApi('admin/emoji/v2/list', { const query: Misskey.entities.AdminEmojiV2ListRequest['query'] = {
limit: 100, name: emptyStrToUndefined(queryName.value),
query: { host: emptyStrToUndefined(queryHost.value),
name: query, uri: emptyStrToUndefined(queryUri.value),
host: host, publicUrl: emptyStrToUndefined(queryPublicUrl.value),
sinceId: sinceId,
untilId: untilId,
hostType: 'remote', hostType: 'remote',
}, };
if (JSON.stringify(query) !== previousQuery.value) {
currentPage.value = 1;
}
const result = await misskeyApi('admin/emoji/v2/list', {
limit: 100,
query: query,
page: currentPage.value,
sort: sortOrders.value.map(({ key, direction }) => ({ key: key as any, direction })),
}); });
customEmojis.value = emojis.emojis; customEmojis.value = result.emojis;
allPages.value = result.allPages;
previousQuery.value = JSON.stringify(query);
gridItems.value = customEmojis.value.map(it => ({ gridItems.value = customEmojis.value.map(it => ({
checked: false, checked: false,
id: it.id, id: it.id,
@ -229,32 +337,90 @@ onMounted(async () => {
</script> </script>
<style lang="scss"> <style lang="scss">
.emoji-grid-row-edited { .row1 {
background-color: var(--ag-advanced-filter-column-pill-color); grid-row: 1 / 2;
} }
.emoji-grid-item-image { .row2 {
width: auto; grid-row: 2 / 3;
height: 26px;
max-width: 100%;
max-height: 100%;
} }
.col1 {
grid-column: 1 / 2;
}
.col2 {
grid-column: 2 / 3;
}
</style> </style>
<style module lang="scss"> <style module lang="scss">
.root { .root {
--stickyTop: 0px;
padding: 16px; padding: 16px;
overflow: scroll; overflow: scroll;
} }
.searchArea { .searchArea {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.searchButtons {
display: flex;
justify-content: flex-end;
align-items: flex-end;
gap: 8px;
}
.searchButtonsSp {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.searchAreaSp {
display: flex;
flex-direction: column;
gap: 8px;
}
.sortOrderArea {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-end; align-items: flex-start;
justify-content: stretch; justify-content: flex-start;
}
.sortOrderAreaTags {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
gap: 8px; gap: 8px;
} }
.sortOrderAddButton {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
min-width: 2.0em;
min-height: 2.0em;
max-width: 2.0em;
max-height: 2.0em;
padding: 8px;
margin-left: auto;
border-radius: 9999px;
background-color: var(--buttonBg);
}
.gridArea { .gridArea {
overflow: scroll; overflow: scroll;
padding-top: 8px; padding-top: 8px;