diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-10-08 13:18:08 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-08 13:18:08 +0000 |
| commit | 56cc89b521e8ca0d302230d123c3924e4461556d (patch) | |
| tree | 242411d50ffd1ed7096f95ecdafe91b482628a46 /packages/frontend/src/pages/admin | |
| parent | Merge pull request #16521 from misskey-dev/develop (diff) | |
| parent | Release: 2025.10.0 (diff) | |
| download | misskey-56cc89b521e8ca0d302230d123c3924e4461556d.tar.gz misskey-56cc89b521e8ca0d302230d123c3924e4461556d.tar.bz2 misskey-56cc89b521e8ca0d302230d123c3924e4461556d.zip | |
Merge pull request #16591 from misskey-dev/develop
Release: 2025.10.0
Diffstat (limited to 'packages/frontend/src/pages/admin')
27 files changed, 450 insertions, 238 deletions
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index 89ecc155b2..9d9db9158d 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -6,26 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div :class="$style.header"> - <MkSelect v-model="type" :class="$style.typeSelect"> - <option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option> - <option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option> - <option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option> - <option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option> - <option value="isBot">{{ i18n.ts._role._condition.isBot }}</option> - <option value="isCat">{{ i18n.ts._role._condition.isCat }}</option> - <option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option> - <option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option> - <option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option> - <option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option> - <option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option> - <option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option> - <option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option> - <option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option> - <option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option> - <option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option> - <option value="and">{{ i18n.ts._role._condition.and }}</option> - <option value="or">{{ i18n.ts._role._condition.or }}</option> - <option value="not">{{ i18n.ts._role._condition.not }}</option> + <MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect"> </MkSelect> <button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle"> <i class="ti ti-menu-2"></i> @@ -58,8 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number"> </MkInput> - <MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId"> - <option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option> + <MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId" :items="assignedToDef"> </MkSelect> </div> </template> @@ -69,6 +49,7 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue'; import { genId } from '@/utility/id.js'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; +import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/utility/clone.js'; @@ -99,7 +80,29 @@ watch(v, () => { emit('update:modelValue', v.value); }, { deep: true }); -const type = computed({ +const typeDef = [ + { label: i18n.ts._role._condition.isLocal, value: 'isLocal' }, + { label: i18n.ts._role._condition.isRemote, value: 'isRemote' }, + { label: i18n.ts._role._condition.isSuspended, value: 'isSuspended' }, + { label: i18n.ts._role._condition.isLocked, value: 'isLocked' }, + { label: i18n.ts._role._condition.isBot, value: 'isBot' }, + { label: i18n.ts._role._condition.isCat, value: 'isCat' }, + { label: i18n.ts._role._condition.isExplorable, value: 'isExplorable' }, + { label: i18n.ts._role._condition.roleAssignedTo, value: 'roleAssignedTo' }, + { label: i18n.ts._role._condition.createdLessThan, value: 'createdLessThan' }, + { label: i18n.ts._role._condition.createdMoreThan, value: 'createdMoreThan' }, + { label: i18n.ts._role._condition.followersLessThanOrEq, value: 'followersLessThanOrEq' }, + { label: i18n.ts._role._condition.followersMoreThanOrEq, value: 'followersMoreThanOrEq' }, + { label: i18n.ts._role._condition.followingLessThanOrEq, value: 'followingLessThanOrEq' }, + { label: i18n.ts._role._condition.followingMoreThanOrEq, value: 'followingMoreThanOrEq' }, + { label: i18n.ts._role._condition.notesLessThanOrEq, value: 'notesLessThanOrEq' }, + { label: i18n.ts._role._condition.notesMoreThanOrEq, value: 'notesMoreThanOrEq' }, + { label: i18n.ts._role._condition.and, value: 'and' }, + { label: i18n.ts._role._condition.or, value: 'or' }, + { label: i18n.ts._role._condition.not, value: 'not' }, +] as const satisfies MkSelectItem[]; + +const type = computed<GetMkSelectValueTypesFromDef<typeof typeDef>>({ get: () => v.value.type, set: (t) => { if (t === 'and') v.value.values = []; @@ -118,6 +121,8 @@ const type = computed({ }, }); +const assignedToDef = computed(() => roles.filter(r => r.target === 'manual').map(r => ({ label: r.name, value: r.id })) satisfies MkSelectItem[]); + function addValue() { v.value.values.push({ id: genId(), type: 'isRemote' }); } diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue index b69c818b48..7c3f736506 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue @@ -22,27 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="title"> <template #label>{{ i18n.ts.title }}</template> </MkInput> - <MkSelect v-model="method"> + <MkSelect v-model="method" :items="methodDef"> <template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template> - <option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option> - <option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option> <template #caption> {{ methodCaption }} </template> </MkSelect> <div> - <MkSelect v-if="method === 'email'" v-model="userId"> + <MkSelect v-if="method === 'email'" v-model="userId" :items="userIdDef"> <template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template> - <option v-for="user in moderators" :key="user.id" :value="user.id"> - {{ user.name ? `${user.name}(${user.username})` : user.username }} - </option> </MkSelect> <div v-else-if="method === 'webhook'" :class="$style.systemWebhook"> - <MkSelect v-model="systemWebhookId" style="flex: 1"> + <MkSelect v-model="systemWebhookId" :items="systemWebhookIdDef" style="flex: 1"> <template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template> - <option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id"> - {{ webhook.name }} - </option> </MkSelect> <MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked"> <span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/> @@ -79,14 +71,13 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; import MkInput from '@/components/MkInput.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import MkSelect from '@/components/MkSelect.vue'; import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkDivider from '@/components/MkDivider.vue'; import * as os from '@/os.js'; -type NotificationRecipientMethod = 'email' | 'webhook'; - const emit = defineEmits<{ (ev: 'submitted'): void; (ev: 'canceled'): void; @@ -105,9 +96,28 @@ const dialogEl = useTemplateRef('dialogEl'); const loading = ref<number>(0); const title = ref<string>(''); -const method = ref<NotificationRecipientMethod>('email'); -const userId = ref<string | null>(null); -const systemWebhookId = ref<string | null>(null); +const { + model: method, + def: methodDef, +} = useMkSelect({ + items: [ + { label: i18n.ts._abuseReport._notificationRecipient._recipientType.mail, value: 'email' }, + { label: i18n.ts._abuseReport._notificationRecipient._recipientType.webhook, value: 'webhook' }, + ], + initialValue: 'email', +}); +const { + model: userId, + def: userIdDef, +} = useMkSelect({ + items: computed(() => moderators.value.map(u => ({ label: u.name ? `${u.name}(${u.username})` : u.username, value: u.id as string | null }))), +}); +const { + model: systemWebhookId, + def: systemWebhookIdDef, +} = useMkSelect({ + items: computed(() => systemWebhooks.value.map(w => ({ label: w.name, value: w.id }))), +}); const isActive = ref<boolean>(true); const moderators = ref<entities.User[]>([]); diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue index f5e77cbe4e..893bd8d6d3 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue @@ -13,11 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkButton> </div> <div :class="$style.subMenus" class="_gaps_s"> - <MkSelect v-model="filterMethod" style="flex: 1"> + <MkSelect v-model="filterMethod" :items="filterMethodDef" style="flex: 1"> <template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template> - <option :value="null">-</option> - <option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option> - <option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option> </MkSelect> <MkInput v-model="filterText" type="search" style="flex: 1"> <template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template> @@ -51,10 +48,21 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import MkDivider from '@/components/MkDivider.vue'; import { i18n } from '@/i18n.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]); -const filterMethod = ref<string | null>(null); +const { + model: filterMethod, + def: filterMethodDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: null }, + { label: i18n.ts._abuseReport._notificationRecipient._recipientType.mail, value: 'email' }, + { label: i18n.ts._abuseReport._notificationRecipient._recipientType.webhook, value: 'webhook' }, + ], + initialValue: null, +}); const filterText = ref<string>(''); const filteredRecipients = computed(() => { diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index ab462229a7..76bf20b409 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -16,23 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkTip> <div :class="$style.inputs" class="_gaps"> - <MkSelect v-model="state" style="margin: 0; flex: 1;"> + <MkSelect v-model="state" :items="stateDef" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="unresolved">{{ i18n.ts.unresolved }}</option> - <option value="resolved">{{ i18n.ts.resolved }}</option> </MkSelect> - <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> + <MkSelect v-model="targetUserOrigin" :items="targetUserOriginDef" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.reporteeOrigin }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> </MkSelect> - <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> + <MkSelect v-model="reporterOrigin" :items="reporterOriginDef" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.reporterOrigin }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> </MkSelect> </div> @@ -64,13 +55,44 @@ import MkPagination from '@/components/MkPagination.vue'; import XAbuseReport from '@/components/MkAbuseReport.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import MkButton from '@/components/MkButton.vue'; import { store } from '@/store.js'; import { Paginator } from '@/utility/paginator.js'; -const state = ref('unresolved'); -const reporterOrigin = ref('combined'); -const targetUserOrigin = ref('combined'); +const { + model: state, + def: stateDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'all' }, + { label: i18n.ts.unresolved, value: 'unresolved' }, + { label: i18n.ts.resolved, value: 'resolved' }, + ], + initialValue: 'unresolved', +}); +const { + model: reporterOrigin, + def: reporterOriginDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'combined' }, + { label: i18n.ts.local, value: 'local' }, + { label: i18n.ts.remote, value: 'remote' }, + ], + initialValue: 'combined', +}); +const { + model: targetUserOrigin, + def: targetUserOriginDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'combined' }, + { label: i18n.ts.local, value: 'local' }, + { label: i18n.ts.remote, value: 'remote' }, + ], + initialValue: 'combined', +}); const searchUsername = ref(''); const searchHost = ref(''); diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 06a28db088..94940a84ae 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -6,27 +6,29 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 900px;"> - <MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems"> + <MkSelect v-model="filterType" :items="filterTypeDef" :class="$style.input" @update:modelValue="filterItems"> <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="publishing">{{ i18n.ts.publishing }}</option> - <option value="expired">{{ i18n.ts.expired }}</option> </MkSelect> + <div> <div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad"> <MkAd v-if="ad.url" :key="ad.id" :specify="ad"/> + <MkInput v-model="ad.url" type="url"> <template #label>URL</template> </MkInput> + <MkInput v-model="ad.imageUrl" type="url"> <template #label>{{ i18n.ts.imageUrl }}</template> </MkInput> + <MkRadios v-model="ad.place"> <template #label>Form</template> <option value="square">square</option> <option value="horizontal">horizontal</option> <option value="horizontal-big">horizontal-big</option> </MkRadios> + <!-- <div style="margin: 32px 0;"> {{ i18n.ts.priority }} @@ -35,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio> </div> --> + <FormSplit> <MkInput v-model="ad.ratio" type="number"> <template #label>{{ i18n.ts.ratio }}</template> @@ -46,6 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.expiration }}</template> </MkInput> </FormSplit> + + <MkSwitch v-model="ad.isSensitive"> + <template #label>{{ i18n.ts.sensitive }}</template> + </MkSwitch> + <MkFolder> <template #label>{{ i18n.ts.advancedSettings }}</template> <span> @@ -59,9 +67,11 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </span> </MkFolder> + <MkTextarea v-model="ad.memo"> <template #label>{{ i18n.ts.memo }}</template> </MkTextarea> + <div class="_buttons"> <MkButton inline primary style="margin-right: 12px;" @click="save(ad)"> <i @@ -73,6 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkButton> </div> </div> + <MkButton @click="more()"> <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> @@ -91,10 +102,12 @@ import MkRadios from '@/components/MkRadios.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; const ads = ref<Misskey.entities.Ad[]>([]); @@ -102,7 +115,17 @@ const ads = ref<Misskey.entities.Ad[]>([]); const localTime = new Date(); const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000; const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday]; -const filterType = ref('all'); +const { + model: filterType, + def: filterTypeDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'all' }, + { label: i18n.ts.publishing, value: 'publishing' }, + { label: i18n.ts.expired, value: 'expired' }, + ], + initialValue: 'all', +}); let publishing: boolean | null = null; misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => { @@ -121,7 +144,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => { } }); -const filterItems = (v) => { +const filterItems = (v: typeof filterType.value) => { if (v === 'publishing') { publishing = true; } else if (v === 'expired') { @@ -134,7 +157,7 @@ const filterItems = (v) => { }; // 選択された曜日(index)のビットフラグを操作する -function toggleDayOfWeek(ad, index) { +function toggleDayOfWeek(ad: Misskey.entities.Ad, index: number) { ad.dayOfWeek ^= 1 << index; } @@ -150,10 +173,11 @@ function add() { expiresAt: new Date().toISOString(), startsAt: new Date().toISOString(), dayOfWeek: 0, + isSensitive: false, }); } -function remove(ad) { +function remove(ad: Misskey.entities.Ad) { os.confirm({ type: 'warning', text: i18n.tsx.removeAreYouSure({ x: ad.url }), @@ -169,7 +193,7 @@ function remove(ad) { }); } -function save(ad) { +function save(ad: Misskey.entities.Ad) { if (ad.id === '') { misskeyApi('admin/ad/create', { ...ad, diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index e5903d6257..b90a724b17 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -10,10 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo> <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo> - <MkSelect v-model="announcementsStatus"> + <MkSelect v-model="announcementsStatus" :items="announcementsStatusDef"> <template #label>{{ i18n.ts.filter }}</template> - <option value="active">{{ i18n.ts.active }}</option> - <option value="archived">{{ i18n.ts.archived }}</option> </MkSelect> <MkLoading v-if="loading"/> @@ -98,8 +96,18 @@ import { definePage } from '@/page.js'; import MkFolder from '@/components/MkFolder.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import { genId } from '@/utility/id.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; -const announcementsStatus = ref<'active' | 'archived'>('active'); +const { + model: announcementsStatus, + def: announcementsStatusDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.active, value: 'active' }, + { label: i18n.ts.archived, value: 'archived' }, + ], + initialValue: 'active', +}); const loading = ref(true); const loadingMore = ref(false); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue index 9938d5cc4a..6b5272914b 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue @@ -56,20 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkSelect v-model="model.sensitive" + :items="[ + { label: '-', value: null }, + { label: 'true', value: 'true' }, + { label: 'false', value: 'false' }, + ]" > <template #label>sensitive</template> - <option :value="null">-</option> - <option :value="true">true</option> - <option :value="false">false</option> </MkSelect> <MkSelect v-model="model.localOnly" + :items="[ + { label: '-', value: null }, + { label: 'true', value: 'true' }, + { label: 'false', value: 'false' }, + ]" > <template #label>localOnly</template> - <option :value="null">-</option> - <option :value="true">true</option> - <option :value="false">false</option> </MkSelect> <MkInput v-model="model.updatedAtFrom" diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 621ec8a6a8..c343d88eb1 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -12,11 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}</template> <div class="_gaps"> - <MkSelect v-model="selectedFolderId"> + <MkSelect v-model="selectedFolderId" :items="selectedFolderIdDef"> <template #label>{{ i18n.ts.uploadFolder }}</template> - <option v-for="folder in uploadFolders" :key="folder.id" :value="folder.id"> - {{ folder.name }} - </option> </MkSelect> <MkSwitch v-model="directoryToCategory"> @@ -63,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as Misskey from 'misskey-js'; -import { onMounted, ref, useCssModule } from 'vue'; +import { computed, onMounted, ref, useCssModule } from 'vue'; import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import type { DroppedFile } from '@/utility/file-drop.js'; @@ -87,6 +84,7 @@ import { chooseDriveFile, chooseFileFromPcAndUpload } from '@/utility/drive.js'; import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { prefer } from '@/preferences.js'; @@ -229,7 +227,13 @@ function setupGrid(): GridSetting { const uploadFolders = ref<FolderItem[]>([]); const gridItems = ref<GridItem[]>([]); -const selectedFolderId = ref(prefer.s.uploadFolder); +const { + model: selectedFolderId, + def: selectedFolderIdDef, +} = useMkSelect({ + items: computed(() => uploadFolders.value.map(folder => ({ label: folder.name, value: folder.id || '' }))), + initialValue: prefer.s.uploadFolder, +}); const directoryToCategory = ref<boolean>(false); const registerButtonDisabled = ref<boolean>(false); const requestLogs = ref<RequestLogItem[]>([]); @@ -303,8 +307,8 @@ async function onFileSelectClicked() { const driveFiles = await chooseFileFromPcAndUpload({ multiple: true, folderId: selectedFolderId.value, - // 拡張子は消す - nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''), + // // 拡張子は消す + // nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''), }); gridItems.value.push(...driveFiles.map(fromDriveFile)); diff --git a/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue index 9a311b5772..420219c22c 100644 --- a/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue +++ b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue @@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl'); const { handler: externalTooltipHandler } = useChartTooltip(); -let chartInstance: Chart; +let chartInstance: Chart | null = null; function setData(values) { - if (chartInstance == null) return; + if (chartInstance == null || chartInstance.data.labels == null) return; for (const value of values) { chartInstance.data.labels.push(''); chartInstance.data.datasets[0].data.push(value); @@ -42,7 +42,7 @@ function setData(values) { } function pushData(value) { - if (chartInstance == null) return; + if (chartInstance == null || chartInstance.data.labels == null) return; chartInstance.data.labels.push(''); chartInstance.data.datasets[0].data.push(value); if (chartInstance.data.datasets[0].data.length > 200) { @@ -69,6 +69,8 @@ const color = onMounted(() => { const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + if (chartEl.value == null) return; + chartInstance = new Chart(chartEl.value, { type: 'line', data: { diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index ddc3ff7b79..cbf7dbbff5 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -13,31 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.host }}</template> </MkInput> <FormSplit style="margin-top: var(--MI-margin);"> - <MkSelect v-model="state"> + <MkSelect v-model="state" :items="stateDef"> <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="federating">{{ i18n.ts.federating }}</option> - <option value="subscribing">{{ i18n.ts.subscribing }}</option> - <option value="publishing">{{ i18n.ts.publishing }}</option> - <option value="suspended">{{ i18n.ts.suspended }}</option> - <option value="blocked">{{ i18n.ts.blocked }}</option> - <option value="silenced">{{ i18n.ts.silence }}</option> - <option value="notResponding">{{ i18n.ts.notResponding }}</option> </MkSelect> - <MkSelect v-model="sort"> + <MkSelect v-model="sort" :items="sortDef"> <template #label>{{ i18n.ts.sort }}</template> - <option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option> </MkSelect> </FormSplit> </div> @@ -64,11 +44,46 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { Paginator } from '@/utility/paginator.js'; const host = ref(''); -const state = ref('federating'); -const sort = ref('+pubSub'); +const { + model: state, + def: stateDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'all' }, + { label: i18n.ts.federating, value: 'federating' }, + { label: i18n.ts.subscribing, value: 'subscribing' }, + { label: i18n.ts.publishing, value: 'publishing' }, + { label: i18n.ts.suspended, value: 'suspended' }, + { label: i18n.ts.blocked, value: 'blocked' }, + { label: i18n.ts.silence, value: 'silenced' }, + { label: i18n.ts.notResponding, value: 'notResponding' }, + ], + initialValue: 'federating', +}); +const { + model: sort, + def: sortDef, +} = useMkSelect({ + items: [ + { label: `${i18n.ts.pubSub} (${i18n.ts.descendingOrder})`, value: '+pubSub' }, + { label: `${i18n.ts.pubSub} (${i18n.ts.ascendingOrder})`, value: '-pubSub' }, + { label: `${i18n.ts.notes} (${i18n.ts.descendingOrder})`, value: '+notes' }, + { label: `${i18n.ts.notes} (${i18n.ts.ascendingOrder})`, value: '-notes' }, + { label: `${i18n.ts.users} (${i18n.ts.descendingOrder})`, value: '+users' }, + { label: `${i18n.ts.users} (${i18n.ts.ascendingOrder})`, value: '-users' }, + { label: `${i18n.ts.following} (${i18n.ts.descendingOrder})`, value: '+following' }, + { label: `${i18n.ts.following} (${i18n.ts.ascendingOrder})`, value: '-following' }, + { label: `${i18n.ts.followers} (${i18n.ts.descendingOrder})`, value: '+followers' }, + { label: `${i18n.ts.followers} (${i18n.ts.ascendingOrder})`, value: '-followers' }, + { label: `${i18n.ts.registeredAt} (${i18n.ts.descendingOrder})`, value: '+firstRetrievedAt' }, + { label: `${i18n.ts.registeredAt} (${i18n.ts.ascendingOrder})`, value: '-firstRetrievedAt' }, + ], + initialValue: '+pubSub', +}); const paginator = markRaw(new Paginator('federation/instances', { limit: 10, offsetMode: true, diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index b4ec930997..c8b5980883 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -8,11 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_spacer" style="--MI_SPACER-w: 900px;"> <div class="_gaps"> <div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;"> - <MkSelect v-model="origin" style="margin: 0; flex: 1;"> + <MkSelect v-model="origin" :items="originDef" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.instance }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> </MkSelect> <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams?.value?.origin === 'local'"> <template #label>{{ i18n.ts.host }}</template> @@ -42,9 +39,20 @@ import * as os from '@/os.js'; import { lookupFile } from '@/utility/admin-lookup.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { Paginator } from '@/utility/paginator.js'; -const origin = ref<NonNullable<Misskey.entities.AdminDriveFilesRequest['origin']>>('local'); +const { + model: origin, + def: originDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'combined' }, + { label: i18n.ts.local, value: 'local' }, + { label: i18n.ts.remote, value: 'remote' }, + ], + initialValue: 'local', +}); const type = ref<string | null>(null); const searchHost = ref(''); const userId = ref(''); diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 1c551cb477..d52a57e582 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -26,19 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <div :class="$style.inputs"> - <MkSelect v-model="type" :class="$style.input"> + <MkSelect v-model="type" :items="typeDef" :class="$style.input"> <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="unused">{{ i18n.ts.unused }}</option> - <option value="used">{{ i18n.ts.used }}</option> - <option value="expired">{{ i18n.ts.expired }}</option> </MkSelect> - <MkSelect v-model="sort" :class="$style.input"> + <MkSelect v-model="sort" :items="sortDef" :class="$style.input"> <template #label>{{ i18n.ts.sort }}</template> - <option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option> - <option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option> </MkSelect> </div> <MkPagination :paginator="paginator"> @@ -67,10 +59,33 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { Paginator } from '@/utility/paginator.js'; -const type = ref<NonNullable<Misskey.entities.AdminInviteListRequest['type']>>('all'); -const sort = ref<NonNullable<Misskey.entities.AdminInviteListRequest['sort']>>('+createdAt'); +const { + model: type, + def: typeDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'all' }, + { label: i18n.ts.unused, value: 'unused' }, + { label: i18n.ts.used, value: 'used' }, + { label: i18n.ts.expired, value: 'expired' }, + ], + initialValue: 'all', +}); +const { + model: sort, + def: sortDef, +} = useMkSelect({ + items: [ + { label: `${i18n.ts.createdAt} (${i18n.ts.ascendingOrder})`, value: '+createdAt' }, + { label: `${i18n.ts.createdAt} (${i18n.ts.descendingOrder})`, value: '-createdAt' }, + { label: `${i18n.ts.usedAt} (${i18n.ts.ascendingOrder})`, value: '+usedAt' }, + { label: `${i18n.ts.usedAt} (${i18n.ts.descendingOrder})`, value: '-usedAt' }, + ], + initialValue: '+createdAt', +}); const paginator = markRaw(new Paginator('admin/invite/list', { limit: 10, diff --git a/packages/frontend/src/pages/admin/job-queue.vue b/packages/frontend/src/pages/admin/job-queue.vue index 0856bac860..b18049cb11 100644 --- a/packages/frontend/src/pages/admin/job-queue.vue +++ b/packages/frontend/src/pages/admin/job-queue.vue @@ -210,6 +210,7 @@ async function fetchCurrentQueue() { } async function fetchJobs() { + if (tab.value === '-') return; jobsFetching.value = true; const state = jobState.value; jobs.value = await misskeyApi('admin/queue/jobs', { @@ -307,6 +308,7 @@ async function removeJobs() { } async function refreshJob(jobId: string) { + if (tab.value === '-') return; const newJob = await misskeyApi('admin/queue/show-job', { queue: tab.value, jobId }); const index = jobs.value.findIndex((job) => job.id === jobId); if (index !== -1) { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 435dd9c462..a11278b68a 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -25,18 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </SearchMarker> <SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']"> - <MkSelect - v-model="ugcVisibilityForVisitor" :items="[{ - value: 'all', - label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all, - }, { - value: 'local', - label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly + ' (' + i18n.ts.recommended + ')', - }, { - value: 'none', - label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none, - }] as const" @update:modelValue="onChange_ugcVisibilityForVisitor" - > + <MkSelect v-model="ugcVisibilityForVisitor" :items="ugcVisibilityForVisitorDef" @update:modelValue="onChange_ugcVisibilityForVisitor"> <template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template> <template #caption> <div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div> @@ -176,6 +165,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -185,7 +175,17 @@ const meta = await misskeyApi('admin/meta'); const enableRegistration = ref(!meta.disableRegistration); const emailRequiredForSignup = ref(meta.emailRequiredForSignup); -const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor); +const { + model: ugcVisibilityForVisitor, + def: ugcVisibilityForVisitorDef, +} = useMkSelect({ + items: [ + { label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all, value: 'all' }, + { label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly, value: 'local' }, + { label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none, value: 'none' }, + ], + initialValue: meta.ugcVisibilityForVisitor, +}); const sensitiveWords = ref(meta.sensitiveWords.join('\n')); const prohibitedWords = ref(meta.prohibitedWords.join('\n')); const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n')); @@ -221,7 +221,7 @@ function onChange_emailRequiredForSignup(value: boolean) { }); } -function onChange_ugcVisibilityForVisitor(value: Misskey.entities.AdminUpdateMetaRequest['ugcVisibilityForVisitor']) { +function onChange_ugcVisibilityForVisitor(value: typeof ugcVisibilityForVisitor.value) { os.apiWithDialog('admin/update-meta', { ugcVisibilityForVisitor: value, }).then(() => { diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index 08bdc8d254..cb75be7edd 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -8,10 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_spacer" style="--MI_SPACER-w: 900px;"> <div class="_gaps"> <MkPaginationControl :paginator="paginator" canFilter> - <MkSelect v-model="type" style="margin: 0; flex: 1;"> + <MkSelect v-model="type" :items="typeDef" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.type }}</template> - <option :value="null">{{ i18n.ts.all }}</option> - <option v-for="t in Misskey.moderationLogTypes" :key="t" :value="t">{{ i18n.ts._moderationLogTypes[t] ?? t }}</option> </MkSelect> <MkInput v-model="moderatorId" style="margin: 0; flex: 1;"> @@ -54,12 +52,22 @@ import MkTl from '@/components/MkTl.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { prefer } from '@/preferences.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkButton from '@/components/MkButton.vue'; import MkPaginationControl from '@/components/MkPaginationControl.vue'; import { Paginator } from '@/utility/paginator.js'; -const type = ref<string | null>(null); +const { + model: type, + def: typeDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: null }, + ...Misskey.moderationLogTypes.map(t => ({ label: i18n.ts._moderationLogTypes[t] ?? t, value: t })), + ], + initialValue: null, +}); const moderatorId = ref(''); const paginator = markRaw(new Paginator('admin/show-moderation-logs', { diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue index 6c85f11cb1..32a5a6976e 100644 --- a/packages/frontend/src/pages/admin/overview.active-users.vue +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -26,7 +26,7 @@ initChart(); const chartEl = useTemplateRef('chartEl'); const now = new Date(); -let chartInstance: Chart = null; +let chartInstance: Chart | null = null; const chartLimit = 7; const fetching = ref(true); diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index 50f12cbf45..3c737ad32b 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="item _panel sub"> <div class="icon"><i class="ti ti-world-download"></i></div> <div class="body"> - <div class="value"> + <div v-if="federationSubActive != null" class="value"> {{ number(federationSubActive) }} - <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff> + <MkNumberDiff v-if="federationSubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff> </div> <div class="label">Sub</div> </div> @@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="item _panel pub"> <div class="icon"><i class="ti ti-world-upload"></i></div> <div class="body"> - <div class="value"> + <div v-if="federationPubActive != null" class="value"> {{ number(federationPubActive) }} - <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff> + <MkNumberDiff v-if="federationPubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff> </div> <div class="label">Pub</div> </div> diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue index 7b2b142b16..5edc01404c 100644 --- a/packages/frontend/src/pages/admin/overview.heatmap.vue +++ b/packages/frontend/src/pages/admin/overview.heatmap.vue @@ -5,23 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel" :class="$style.root"> - <MkSelect v-model="src" style="margin: 0 0 12px 0;" small> - <option value="active-users">Active users</option> - <option value="notes">Notes</option> - <option value="ap-requests-inbox-received">AP Requests: inboxReceived</option> - <option value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option> - <option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option> + <MkSelect v-model="src" :items="srcDef" style="margin: 0 0 12px 0;" small> </MkSelect> <MkHeatmap :src="src"/> </div> </template> <script lang="ts" setup> -import { ref } from 'vue'; import MkHeatmap from '@/components/MkHeatmap.vue'; import MkSelect from '@/components/MkSelect.vue'; +import { useMkSelect } from '@/composables/use-mkselect.js'; -const src = ref('active-users'); +const { + model: src, + def: srcDef, +} = useMkSelect({ + items: [ + { label: 'Active users', value: 'active-users' }, + { label: 'Notes', value: 'notes' }, + { label: 'AP Requests: inboxReceived', value: 'ap-requests-inbox-received' }, + { label: 'AP Requests: deliverSucceeded', value: 'ap-requests-deliver-succeeded' }, + { label: 'AP Requests: deliverFailed', value: 'ap-requests-deliver-failed' }, + ], + initialValue: 'active-users', +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue index ec2b558cee..2e874b3505 100644 --- a/packages/frontend/src/pages/admin/overview.pie.vue +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -32,15 +32,17 @@ const { handler: externalTooltipHandler } = useChartTooltip({ position: 'middle', }); -let chartInstance: Chart; +let chartInstance: Chart | null = null; onMounted(() => { + if (chartEl.value == null) return; + chartInstance = new Chart(chartEl.value, { type: 'doughnut', data: { labels: props.data.map(x => x.name), datasets: [{ - backgroundColor: props.data.map(x => x.color), + backgroundColor: props.data.map(x => x.color ?? '#000'), borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'), borderWidth: 2, hoverOffset: 0, @@ -57,9 +59,10 @@ onMounted(() => { }, }, onClick: (ev) => { - const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0]; - if (hit && props.data[hit.index].onClick) { - props.data[hit.index].onClick(); + if (ev.native == null) return; + const hit = chartInstance!.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0]; + if (hit && props.data[hit.index].onClick != null) { + props.data[hit.index].onClick!(); } }, plugins: { diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue index 9b9618c4ac..771b35c09f 100644 --- a/packages/frontend/src/pages/admin/overview.queue.chart.vue +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl'); const { handler: externalTooltipHandler } = useChartTooltip(); -let chartInstance: Chart; +let chartInstance: Chart | null = null; -function setData(values) { - if (chartInstance == null) return; +function setData(values: number[]) { + if (chartInstance == null || chartInstance.data.labels == null) return; for (const value of values) { chartInstance.data.labels.push(''); chartInstance.data.datasets[0].data.push(value); @@ -41,8 +41,8 @@ function setData(values) { chartInstance.update(); } -function pushData(value) { - if (chartInstance == null) return; +function pushData(value: number) { + if (chartInstance == null || chartInstance.data.labels == null) return; chartInstance.data.labels.push(''); chartInstance.data.datasets[0].data.push(value); if (chartInstance.data.datasets[0].data.length > 100) { @@ -67,6 +67,8 @@ const color = '?' as never; onMounted(() => { + if (chartEl.value == null) return; + const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; chartInstance = new Chart(chartEl.value, { diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index e7e139b74d..e57df3744a 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import XChart from './overview.queue.chart.vue'; -import type { ApQueueDomain } from '@/pages/admin/queue.vue'; +import type { ApQueueDomain } from '@/pages/admin/federation-job-queue.vue'; import number from '@/filters/number.js'; import { useStream } from '@/stream.js'; import { genId } from '@/utility/id.js'; @@ -64,10 +64,10 @@ function onStats(stats: Misskey.entities.QueueStats) { delayed.value = stats[props.domain].delayed; waiting.value = stats[props.domain].waiting; - chartProcess.value.pushData(stats[props.domain].activeSincePrevTick); - chartActive.value.pushData(stats[props.domain].active); - chartDelayed.value.pushData(stats[props.domain].delayed); - chartWaiting.value.pushData(stats[props.domain].waiting); + chartProcess.value?.pushData(stats[props.domain].activeSincePrevTick); + chartActive.value?.pushData(stats[props.domain].active); + chartDelayed.value?.pushData(stats[props.domain].delayed); + chartWaiting.value?.pushData(stats[props.domain].waiting); } function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { @@ -83,10 +83,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { dataWaiting.push(stats[props.domain].waiting); } - chartProcess.value.setData(dataProcess); - chartActive.value.setData(dataActive); - chartDelayed.value.setData(dataDelayed); - chartWaiting.value.setData(dataWaiting); + chartProcess.value?.setData(dataProcess); + chartActive.value?.setData(dataActive); + chartDelayed.value?.setData(dataDelayed); + chartWaiting.value?.setData(dataWaiting); } onMounted(() => { diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index fd8145b308..b0669bc557 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> - <div v-else :class="$style.root"> + <div v-else-if="stats != null" :class="$style.root"> <div class="item _panel users"> <div class="icon"><i class="ti ti-users"></i></div> <div class="body"> <div class="value"> <MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/> - <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff> + <MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff> </div> <div class="label">Users</div> </div> @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="body"> <div class="value"> <MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/> - <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff> + <MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff> </div> <div class="label">Notes</div> </div> @@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> + <MkError v-else/> </Transition> </div> </template> @@ -71,8 +72,8 @@ import { customEmojis } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; const stats = ref<Misskey.entities.StatsResponse | null>(null); -const usersComparedToThePrevDay = ref<number>(); -const notesComparedToThePrevDay = ref<number>(); +const usersComparedToThePrevDay = ref<number | null>(null); +const notesComparedToThePrevDay = ref<number | null>(null); const onlineUsersCount = ref(0); const fetching = ref(true); @@ -85,11 +86,11 @@ onMounted(async () => { onlineUsersCount.value = _onlineUsersCount; misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => { - usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1]; + usersComparedToThePrevDay.value = _stats.originalUsersCount - chart.local.total[1]; }); misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => { - notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1]; + notesComparedToThePrevDay.value = _stats.originalNotesCount - chart.local.total[1]; }); fetching.value = false; diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 2ad5173618..2c550bd9c3 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -95,7 +95,7 @@ const federationPubActiveDiff = ref<number | null>(null); const federationSubActive = ref<number | null>(null); const federationSubActiveDiff = ref<number | null>(null); const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null); -const activeInstances = shallowRef<Misskey.entities.FederationInstance | null>(null); +const activeInstances = shallowRef<Misskey.entities.FederationInstancesResponse | null>(null); const queueStatsConnection = markRaw(useStream().useChannel('queueStats')); const now = new Date(); const filesPagination = { diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index e98b4f0129..5f8950f07e 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -30,19 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._role.descriptionOfDisplayOrder }}</template> </MkInput> - <MkSelect v-model="rolePermission" :readonly="readonly"> + <MkSelect v-model="rolePermission" :items="rolePermissionDef" :readonly="readonly"> <template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template> <template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template> - <option value="normal">{{ i18n.ts.normalUser }}</option> - <option value="moderator">{{ i18n.ts.moderator }}</option> - <option value="administrator">{{ i18n.ts.administrator }}</option> </MkSelect> - <MkSelect v-model="role.target" :readonly="readonly"> + <MkSelect v-model="role.target" :items="[{ label: i18n.ts._role.manual, value: 'manual' }, { label: i18n.ts._role.conditional, value: 'conditional' }]" :readonly="readonly"> <template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template> <template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template> - <option value="manual">{{ i18n.ts._role.manual }}</option> - <option value="conditional">{{ i18n.ts._role.conditional }}</option> </MkSelect> <MkFolder v-if="role.target === 'conditional'" defaultOpen> @@ -176,11 +171,17 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="role.policies.chatAvailability.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts._role.useBaseValue }}</template> </MkSwitch> - <MkSelect v-model="role.policies.chatAvailability.value" :disabled="role.policies.chatAvailability.useDefault" :readonly="readonly"> + <MkSelect + v-model="role.policies.chatAvailability.value" + :items="[ + { label: i18n.ts.enabled, value: 'available' }, + { label: i18n.ts.readonly, value: 'readonly' }, + { label: i18n.ts.disabled, value: 'unavailable' }, + ]" + :disabled="role.policies.chatAvailability.useDefault" + :readonly="readonly" + > <template #label>{{ i18n.ts.enable }}</template> - <option value="available">{{ i18n.ts.enabled }}</option> - <option value="readonly">{{ i18n.ts.readonly }}</option> - <option value="unavailable">{{ i18n.ts.disabled }}</option> </MkSelect> <MkRange v-model="role.policies.chatAvailability.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> @@ -419,6 +420,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <MkInput v-model="role.policies.maxFileSizeMb.value" :disabled="role.policies.maxFileSizeMb.useDefault" type="number" :readonly="readonly"> <template #suffix>MB</template> + <template #caption> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._role._options.maxFileSize_caption }}</div> + </template> </MkInput> <MkRange v-model="role.policies.maxFileSizeMb.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> @@ -801,6 +805,25 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])"> + <template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template> + <template #suffix> + <span v-if="role.policies.scheduledNoteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.scheduledNoteLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduledNoteLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.scheduledNoteLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.scheduledNoteLimit.value" :disabled="role.policies.scheduledNoteLimit.useDefault" type="number" :readonly="readonly"> + </MkInput> + <MkRange v-model="role.policies.scheduledNoteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])"> <template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template> <template #suffix> @@ -830,7 +853,7 @@ import { watch, ref, computed } from 'vue'; import { throttle } from 'throttle-debounce'; import * as Misskey from 'misskey-js'; import RolesEditorFormula from './RolesEditorFormula.vue'; -import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue'; +import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue'; import MkInput from '@/components/MkInput.vue'; import MkColorInput from '@/components/MkColorInput.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -871,11 +894,17 @@ function updateAvatarDecorationLimit(value: string | number) { role.value.policies.avatarDecorationLimit.value = limited; } -const rolePermission = computed({ +const rolePermissionDef = [ + { label: i18n.ts.normalUser, value: 'normal' }, + { label: i18n.ts.moderator, value: 'moderator' }, + { label: i18n.ts.administrator, value: 'administrator' }, +] as const satisfies MkSelectItem[]; + +const rolePermission = computed<GetMkSelectValueTypesFromDef<typeof rolePermissionDef>>({ get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal', set: (val) => { - role.value.isAdministrator = val === 'administrator'; - role.value.isModerator = val === 'moderator'; + role.value.isAdministrator = (val === 'administrator'); + role.value.isModerator = (val === 'moderator'); }, }); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index c6c3165828..2e249eee50 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -71,7 +71,7 @@ import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); const props = defineProps<{ - id?: string; + id: string; }>(); const usersPaginator = markRaw(new Paginator('admin/roles/users', { @@ -115,15 +115,15 @@ async function assign() { const { canceled: canceled2, result: period } = await os.select({ title: i18n.ts.period + ': ' + role.name, items: [{ - value: 'indefinitely', text: i18n.ts.indefinitely, + value: 'indefinitely', label: i18n.ts.indefinitely, }, { - value: 'oneHour', text: i18n.ts.oneHour, + value: 'oneHour', label: i18n.ts.oneHour, }, { - value: 'oneDay', text: i18n.ts.oneDay, + value: 'oneDay', label: i18n.ts.oneDay, }, { - value: 'oneWeek', text: i18n.ts.oneWeek, + value: 'oneWeek', label: i18n.ts.oneWeek, }, { - value: 'oneMonth', text: i18n.ts.oneMonth, + value: 'oneMonth', label: i18n.ts.oneMonth, }], default: 'indefinitely', }); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 5323d042cf..e65a3c5ba8 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -52,11 +52,15 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])"> <template #label>{{ i18n.ts._role._options.chatAvailability }}</template> <template #suffix>{{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template> - <MkSelect v-model="policies.chatAvailability"> + <MkSelect + v-model="policies.chatAvailability" + :items="[ + { label: i18n.ts.enabled, value: 'available' }, + { label: i18n.ts.readonly, value: 'readonly' }, + { label: i18n.ts.disabled, value: 'unavailable' }, + ]" + > <template #label>{{ i18n.ts.enable }}</template> - <option value="available">{{ i18n.ts.enabled }}</option> - <option value="readonly">{{ i18n.ts.readonly }}</option> - <option value="unavailable">{{ i18n.ts.disabled }}</option> </MkSelect> </MkFolder> @@ -151,6 +155,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #suffix>{{ policies.maxFileSizeMb }}MB</template> <MkInput v-model="policies.maxFileSizeMb" type="number"> <template #suffix>MB</template> + <template #caption> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._role._options.maxFileSize_caption }}</div> + </template> </MkInput> </MkFolder> @@ -300,6 +307,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])"> + <template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template> + <template #suffix>{{ policies.scheduledNoteLimit }}</template> + <MkInput v-model="policies.scheduledNoteLimit" type="number" :min="0"> + </MkInput> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])"> <template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template> <template #suffix>{{ policies.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}</template> @@ -346,6 +360,7 @@ import { definePage } from '@/page.js'; import { instance, fetchInstance } from '@/instance.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { useRouter } from '@/router.js'; +import { deepClone } from '@/utility/clone.js'; import MkTextarea from '@/components/MkTextarea.vue'; const router = useRouter(); @@ -353,10 +368,7 @@ const baseRoleQ = ref(''); const roles = await misskeyApi('admin/roles/list'); -const policies = reactive<Record<typeof Misskey.rolePolicies[number], any>>({}); -for (const ROLE_POLICY of Misskey.rolePolicies) { - policies[ROLE_POLICY] = instance.policies[ROLE_POLICY]; -} +const policies = reactive(deepClone(instance.policies)); const avatarDecorationLimit = computed({ get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)), @@ -376,6 +388,7 @@ function matchQuery(keywords: string[]): boolean { async function updateBaseRole() { await os.apiWithDialog('admin/roles/update-default-policies', { + //@ts-expect-error misskey-js側の型定義が不十分 policies, }); fetchInstance(true); diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 7cbaeba8c7..2f7ecca521 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -11,26 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton style="margin-left: auto" @click="resetQuery">{{ i18n.ts.reset }}</MkButton> </div> <div :class="$style.inputs"> - <MkSelect v-model="sort" style="flex: 1;"> + <MkSelect v-model="sort" :items="sortDef" style="flex: 1;"> <template #label>{{ i18n.ts.sort }}</template> - <option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option> - <option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option> - <option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.descendingOrder }})</option> </MkSelect> - <MkSelect v-model="state" style="flex: 1;"> + <MkSelect v-model="state" :items="stateDef" style="flex: 1;"> <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="available">{{ i18n.ts.normal }}</option> - <option value="admin">{{ i18n.ts.administrator }}</option> - <option value="moderator">{{ i18n.ts.moderator }}</option> - <option value="suspended">{{ i18n.ts.suspend }}</option> </MkSelect> - <MkSelect v-model="origin" style="flex: 1;"> + <MkSelect v-model="origin" :items="originDef" style="flex: 1;"> <template #label>{{ i18n.ts.instance }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> </MkSelect> </div> <div :class="$style.inputs"> @@ -67,23 +55,57 @@ import * as os from '@/os.js'; import { lookupUser } from '@/utility/admin-lookup.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { dateString } from '@/filters/date.js'; import { Paginator } from '@/utility/paginator.js'; type SearchQuery = { - sort?: string; - state?: string; - origin?: string; + sort?: '-createdAt' | '+createdAt' | '-updatedAt' | '+updatedAt'; + state?: 'all' | 'available' | 'admin' | 'moderator' | 'suspended'; + origin?: 'combined' | 'local' | 'remote'; username?: string; hostname?: string; }; const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery; -const sort = ref(storedQuery.sort ?? '+createdAt'); -const state = ref(storedQuery.state ?? 'all'); -const origin = ref(storedQuery.origin ?? 'local'); +const { + model: sort, + def: sortDef, +} = useMkSelect({ + items: [ + { label: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`, value: '-createdAt' }, + { label: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`, value: '+createdAt' }, + { label: `${i18n.ts.lastUsed} (${i18n.ts.ascendingOrder})`, value: '-updatedAt' }, + { label: `${i18n.ts.lastUsed} (${i18n.ts.descendingOrder})`, value: '+updatedAt' }, + ], + initialValue: storedQuery.sort ?? '+createdAt', +}); +const { + model: state, + def: stateDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'all' }, + { label: i18n.ts.normal, value: 'available' }, + { label: i18n.ts.administrator, value: 'admin' }, + { label: i18n.ts.moderator, value: 'moderator' }, + { label: i18n.ts.suspend, value: 'suspended' }, + ], + initialValue: storedQuery.state ?? 'all', +}); +const { + model: origin, + def: originDef, +} = useMkSelect({ + items: [ + { label: i18n.ts.all, value: 'combined' }, + { label: i18n.ts.local, value: 'local' }, + { label: i18n.ts.remote, value: 'remote' }, + ], + initialValue: storedQuery.origin ?? 'local', +}); const searchUsername = ref(storedQuery.username ?? ''); const searchHost = ref(storedQuery.hostname ?? ''); const paginator = markRaw(new Paginator('admin/show-users', { |