summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/admin
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-10-08 13:18:08 +0000
committerGitHub <noreply@github.com>2025-10-08 13:18:08 +0000
commit56cc89b521e8ca0d302230d123c3924e4461556d (patch)
tree242411d50ffd1ed7096f95ecdafe91b482628a46 /packages/frontend/src/pages/admin
parentMerge pull request #16521 from misskey-dev/develop (diff)
parentRelease: 2025.10.0 (diff)
downloadmisskey-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')
-rw-r--r--packages/frontend/src/pages/admin/RolesEditorFormula.vue51
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue42
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue18
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue52
-rw-r--r--packages/frontend/src/pages/admin/ads.vue42
-rw-r--r--packages/frontend/src/pages/admin/announcements.vue16
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue16
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.register.vue20
-rw-r--r--packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue8
-rw-r--r--packages/frontend/src/pages/admin/federation.vue63
-rw-r--r--packages/frontend/src/pages/admin/files.vue18
-rw-r--r--packages/frontend/src/pages/admin/invites.vue39
-rw-r--r--packages/frontend/src/pages/admin/job-queue.vue2
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue28
-rw-r--r--packages/frontend/src/pages/admin/modlog.vue16
-rw-r--r--packages/frontend/src/pages/admin/overview.active-users.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.federation.vue8
-rw-r--r--packages/frontend/src/pages/admin/overview.heatmap.vue23
-rw-r--r--packages/frontend/src/pages/admin/overview.pie.vue13
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.chart.vue12
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.vue18
-rw-r--r--packages/frontend/src/pages/admin/overview.stats.vue15
-rw-r--r--packages/frontend/src/pages/admin/overview.vue2
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue59
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue12
-rw-r--r--packages/frontend/src/pages/admin/roles.vue29
-rw-r--r--packages/frontend/src/pages/admin/users.vue64
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', {