summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-09-13 21:00:33 +0900
committerGitHub <noreply@github.com>2025-09-13 21:00:33 +0900
commitd4654dd7bd5bf1c7faa74ed89f592448c0076be8 (patch)
treeb4f51e86f174717fef469fbedca48faa2a55e841 /packages/frontend/src
parentfix(deps): update dependency vite [security] (#16535) (diff)
downloadmisskey-d4654dd7bd5bf1c7faa74ed89f592448c0076be8.tar.gz
misskey-d4654dd7bd5bf1c7faa74ed89f592448c0076be8.tar.bz2
misskey-d4654dd7bd5bf1c7faa74ed89f592448c0076be8.zip
refactor(frontend): os.select, MkSelectのitem指定をオブジェクトによる定義に統一し、型を狭める (#16475)
* refactor(frontend): MkSelectのitem指定をオブジェクトによる定義に統一 * fix * spdx * fix * fix os.select * fix lint * add comment * fix * fix: os.select対応漏れを修正 * fix * fix * fix: MkSelectのmodelに対する型チェックを厳格化 * fix * fix * fix * Update packages/frontend/src/components/MkEmbedCodeGenDialog.vue Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> * fix * fix types * fix * fix * Update packages/frontend/src/pages/admin/roles.editor.vue Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> * fix: MkSelectに直接配列を指定している場合に正常に型が解決されるように --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.vue43
-rw-r--r--packages/frontend/src/components/MkAsUi.vue18
-rw-r--r--packages/frontend/src/components/MkDialog.vue34
-rw-r--r--packages/frontend/src/components/MkEmbedCodeGenDialog.vue19
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue24
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue126
-rw-r--r--packages/frontend/src/components/MkPaginationControl.vue14
-rw-r--r--packages/frontend/src/components/MkPollEditor.vue38
-rw-r--r--packages/frontend/src/components/MkPostForm.vue10
-rw-r--r--packages/frontend/src/components/MkRoleSelectDialog.vue8
-rw-r--r--packages/frontend/src/components/MkSelect.vue174
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue16
-rw-r--r--packages/frontend/src/components/MkWidgets.vue14
-rw-r--r--packages/frontend/src/composables/use-mkselect.ts38
-rw-r--r--packages/frontend/src/os.ts44
-rw-r--r--packages/frontend/src/pages/about.federation.vue89
-rw-r--r--packages/frontend/src/pages/admin-user.vue43
-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.vue26
-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.vue16
-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/moderation.vue28
-rw-r--r--packages/frontend/src/pages/admin/modlog.vue16
-rw-r--r--packages/frontend/src/pages/admin/overview.heatmap.vue24
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue36
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue10
-rw-r--r--packages/frontend/src/pages/admin/roles.vue12
-rw-r--r--packages/frontend/src/pages/admin/users.vue64
-rw-r--r--packages/frontend/src/pages/avatar-decoration-edit-dialog.vue8
-rw-r--r--packages/frontend/src/pages/debug.vue43
-rw-r--r--packages/frontend/src/pages/drop-and-fusion.vue22
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue8
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue16
-rw-r--r--packages/frontend/src/pages/instance-info.vue34
-rw-r--r--packages/frontend/src/pages/page-editor/common.ts11
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue16
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue20
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.vue32
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue4
-rw-r--r--packages/frontend/src/pages/settings/notifications.notification-config.vue31
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue87
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue110
-rw-r--r--packages/frontend/src/pages/settings/profile.vue19
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue15
-rw-r--r--packages/frontend/src/pages/settings/statusbar.statusbar.vue31
-rw-r--r--packages/frontend/src/pages/settings/theme.manage.vue28
-rw-r--r--packages/frontend/src/preferences/manager.ts14
-rw-r--r--packages/frontend/src/preferences/utility.ts2
-rw-r--r--packages/frontend/src/ui/deck.vue2
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue16
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue7
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue15
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue7
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue8
-rw-r--r--packages/frontend/src/utility/form.ts3
-rw-r--r--packages/frontend/src/utility/get-user-menu.ts20
-rw-r--r--packages/frontend/src/widgets/WidgetUserList.vue8
64 files changed, 1171 insertions, 765 deletions
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index e2febf7225..a41fdbc45d 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
- <MkSelect v-model="src">
+ <MkSelect v-model="src" :items="antennaSourcesSelectDef">
<template #label>{{ i18n.ts.antennaSource }}</template>
- <option value="all">{{ i18n.ts._antennaSources.all }}</option>
- <!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
- <option value="users">{{ i18n.ts._antennaSources.users }}</option>
- <!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
- <option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect>
- <MkSelect v-if="src === 'list'" v-model="userListId">
+ <MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
<template #label>{{ i18n.ts.userList }}</template>
- <option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template>
@@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch, ref } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import type { DeepPartial } from '@/utility/merge.js';
import MkButton from '@/components/MkButton.vue';
@@ -64,6 +58,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { deepMerge } from '@/utility/merge.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
id?: string;
@@ -99,9 +94,35 @@ const emit = defineEmits<{
(ev: 'deleted'): void,
}>();
+const {
+ model: src,
+ def: antennaSourcesSelectDef,
+} = useMkSelect({
+ items: [
+ { value: 'all', label: i18n.ts._antennaSources.all },
+ //{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
+ { value: 'users', label: i18n.ts._antennaSources.users },
+ //{ value: 'list', label: i18n.ts._antennaSources.userList },
+ { value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
+ ],
+ initialValue: initialAntenna.src,
+});
+
+const {
+ model: userListId,
+ def: userListsSelectDef,
+} = useMkSelect({
+ items: computed(() => {
+ if (userLists.value == null) return [];
+ return userLists.value.map(list => ({
+ value: list.id,
+ label: list.name,
+ }));
+ }),
+ initialValue: initialAntenna.userListId,
+});
+
const name = ref<string>(initialAntenna.name);
-const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
-const userListId = ref<string | null>(initialAntenna.userListId);
const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 20a953c72c..a3b6112629 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
+ <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
- <option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
@@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
import MkFolder from '@/components/MkFolder.vue';
import MkPostForm from '@/components/MkPostForm.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
component: AsUiComponent;
@@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
}
}
-const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
+const {
+ model: valueForSelect,
+ def: selectDef,
+} = useMkSelect({
+ items: computed(() => {
+ if (c.type !== 'select') return [];
+ return (c.items ?? []).map(item => ({
+ value: item.value,
+ label: item.text,
+ }));
+ }),
+ initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
+});
function onSelectUpdate(v) {
valueForSelect.value = v;
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 3f7519a43f..705301a6a6 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
- <MkSelect v-if="select" v-model="selectedValue" autofocus>
- <template v-if="select.items">
- <template v-for="item in select.items">
- <optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
- <option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
- </optgroup>
- <option v-else :value="item.value">{{ item.text }}</option>
- </template>
- </template>
- </MkSelect>
+ <MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
@@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
+import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';
type Input = {
@@ -67,17 +60,9 @@ type Input = {
maxLength?: number;
};
-type SelectItem = {
- value: any;
- text: string;
-};
-
type Select = {
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- })[];
- default: string | null;
+ items: MkSelectItem[];
+ default: OptionValue | null;
};
type Result = string | number | true | null;
@@ -115,7 +100,6 @@ const emit = defineEmits<{
const modal = useTemplateRef('modal');
const inputValue = ref<string | number | null>(props.input?.default ?? null);
-const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
@@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
return null;
});
+const {
+ def: selectDef,
+ model: selectedValue,
+} = useMkSelect({
+ items: computed(() => props.select?.items ?? []),
+ initialValue: props.select?.default ?? null,
+});
+
// overload function を使いたいので lint エラーを無視する
function done(canceled: true): void;
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index 17823deb85..0cb8499699 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix>px</template>
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
</MkInput>
- <MkSelect v-model="colorMode">
+ <MkSelect v-model="colorMode" :items="colorModeDef">
<template #label>{{ i18n.ts.theme }}</template>
- <option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
- <option value="light">{{ i18n.ts.light }}</option>
- <option value="dark">{{ i18n.ts.dark }}</option>
</MkSelect>
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
@@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
@@ -162,7 +160,18 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro
const header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
-const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
+const {
+ model: colorMode,
+ def: colorModeDef,
+} = useMkSelect({
+ items: [
+ { value: 'auto', label: i18n.ts.syncDeviceDarkMode },
+ { value: 'light', label: i18n.ts.light },
+ { value: 'dark', label: i18n.ts.dark },
+ ],
+ initialValue: props.params?.colorMode ?? 'auto',
+});
+
const rounded = ref(props.params?.rounded ?? true);
const border = ref(props.params?.border ?? true);
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 8d697499a5..142ccb12a3 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-text="v.label || k"></span>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkSwitch>
- <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
+ <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
- <option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
import XFile from './MkFormDialog.file.vue';
-import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
+import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@@ -120,16 +120,14 @@ function cancel() {
dialog.value?.close();
}
-function getEnumLabel(e: EnumItem) {
- return typeof e === 'string' ? e : e.label;
-}
-
-function getEnumValue(e: EnumItem) {
- return typeof e === 'string' ? e : e.value;
-}
-
-function getEnumKey(e: EnumItem) {
- return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
+function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
+ return def.enum.map((v) => {
+ if (typeof v === 'string') {
+ return { value: v, label: v };
+ } else {
+ return { value: v.value, label: v.label };
+ }
+ });
}
function getRadioKey(e: RadioFormItem['options'][number]) {
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 15578ca1c9..13048a2e1b 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>Chart</template>
<div :class="$style.chart">
<div class="selects">
- <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
- <option value="federation">{{ i18n.ts._charts.federation }}</option>
- <option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
- </optgroup>
- <optgroup :label="i18n.ts.users">
- <option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
- <option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
- <option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
- </optgroup>
- <optgroup :label="i18n.ts.notes">
- <option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
- <option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
- <option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
- <option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
- </optgroup>
- <optgroup :label="i18n.ts.drive">
- <option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
- <option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
- </optgroup>
- </MkSelect>
- <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
- <option value="hour">{{ i18n.ts.perHour }}</option>
- <option value="day">{{ i18n.ts.perDay }}</option>
- </MkSelect>
+ <MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
+ <MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
</div>
<div class="chart _panel">
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
@@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection class="item">
<template #header>Active users heatmap</template>
- <MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
- <option value="active-users">Active users</option>
- <option value="notes">Notes</option>
- <option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
- <option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
- <option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
- </MkSelect>
+ <MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
<div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div>
@@ -84,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, ref, computed, useTemplateRef } from 'vue';
+import { onMounted, computed, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
-import type { HeatmapSource } from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
+import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import type { ChartSrc } from '@/components/MkChart.vue';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
@@ -101,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
initChart();
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
const chartLimit = 500;
-const chartSpan = ref<'hour' | 'day'>('hour');
-const chartSrc = ref<ChartSrc>('active-users');
-const heatmapSrc = ref<HeatmapSource>('active-users');
+const {
+ model: chartSpan,
+ def: chartSpanDef,
+} = useMkSelect({
+ items: [
+ { value: 'hour', label: i18n.ts.perHour },
+ { value: 'day', label: i18n.ts.perDay },
+ ],
+ initialValue: 'hour',
+});
+const {
+ model: chartSrc,
+ def: chartSrcDef,
+} = useMkSelect({
+ items: computed<MkSelectItem<ChartSrc>[]>(() => {
+ const items: MkSelectItem<ChartSrc>[] = [];
+
+ if (shouldShowFederation.value) {
+ items.push({
+ type: 'group',
+ label: i18n.ts.federation,
+ items: [
+ { value: 'federation', label: i18n.ts._charts.federation },
+ { value: 'ap-request', label: i18n.ts._charts.apRequest },
+ ],
+ });
+ }
+
+ items.push({
+ type: 'group',
+ label: i18n.ts.users,
+ items: [
+ { value: 'users', label: i18n.ts._charts.usersIncDec },
+ { value: 'users-total', label: i18n.ts._charts.usersTotal },
+ { value: 'active-users', label: i18n.ts._charts.activeUsers },
+ ],
+ });
+
+ const notesItems: ItemOption<ChartSrc>[] = [
+ { value: 'notes', label: i18n.ts._charts.notesIncDec },
+ { value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
+ ];
+
+ if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
+
+ notesItems.push(
+ { value: 'notes-total', label: i18n.ts._charts.notesTotal },
+ );
+
+ items.push({
+ type: 'group',
+ label: i18n.ts.notes,
+ items: notesItems,
+ });
+
+ items.push({
+ type: 'group',
+ label: i18n.ts.drive,
+ items: [
+ { value: 'drive-files', label: i18n.ts._charts.filesIncDec },
+ { value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
+ ],
+ });
+
+ return items;
+ }),
+ initialValue: 'active-users',
+});
+const {
+ model: heatmapSrc,
+ def: heatmapSrcDef,
+} = useMkSelect({
+ items: computed(() => [
+ { value: 'active-users' as const, label: 'Active Users' },
+ { value: 'notes' as const, label: 'Notes' },
+ ...(shouldShowFederation.value ? [
+ { value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
+ { value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
+ { value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
+ ] : []),
+ ]),
+ initialValue: 'active-users',
+});
const subDoughnutEl = useTemplateRef('subDoughnutEl');
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
diff --git a/packages/frontend/src/components/MkPaginationControl.vue b/packages/frontend/src/components/MkPaginationControl.vue
index 10bed575a4..55aa3f2dc2 100644
--- a/packages/frontend/src/components/MkPaginationControl.vue
+++ b/packages/frontend/src/components/MkPaginationControl.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<div :class="$style.control">
- <MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
+ <MkSelect v-model="order" :class="$style.order" :items="orderDef">
<template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect>
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
@@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
paginator: T;
@@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened);
-const order = ref<'newest' | 'oldest'>('newest');
+const {
+ model: order,
+ def: orderDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._order.newest, value: 'newest' },
+ { label: i18n.ts._order.oldest, value: 'oldest' },
+ ],
+ initialValue: 'newest',
+});
const date = ref<number | null>(null);
const q = ref<string | null>(null);
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 174c923bcf..b7c3d1f42d 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -22,11 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
<section>
<div>
- <MkSelect v-model="expiration" small>
+ <MkSelect v-model="expiration" :items="expirationDef" small>
<template #label>{{ i18n.ts._poll.expiration }}</template>
- <option value="infinite">{{ i18n.ts._poll.infinite }}</option>
- <option value="at">{{ i18n.ts._poll.at }}</option>
- <option value="after">{{ i18n.ts._poll.after }}</option>
</MkSelect>
<section v-if="expiration === 'at'">
<MkInput v-model="atDate" small type="date" class="input">
@@ -40,12 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="after" small type="number" :min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput>
- <MkSelect v-model="unit" small>
- <option value="second">{{ i18n.ts._time.second }}</option>
- <option value="minute">{{ i18n.ts._time.minute }}</option>
- <option value="hour">{{ i18n.ts._time.hour }}</option>
- <option value="day">{{ i18n.ts._time.day }}</option>
- </MkSelect>
+ <MkSelect v-model="unit" :items="unitDef" small></MkSelect>
</section>
</div>
</section>
@@ -61,6 +53,7 @@ import MkButton from './MkButton.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
import { addTime } from '@/utility/time.js';
import { i18n } from '@/i18n.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
export type PollEditorModelValue = {
expiresAt: number | null;
@@ -78,11 +71,32 @@ const emit = defineEmits<{
const choices = ref(props.modelValue.choices);
const multiple = ref(props.modelValue.multiple);
-const expiration = ref('infinite');
+const {
+ model: expiration,
+ def: expirationDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._poll.infinite, value: 'infinite' },
+ { label: i18n.ts._poll.at, value: 'at' },
+ { label: i18n.ts._poll.after, value: 'after' },
+ ],
+ initialValue: 'infinite',
+});
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
const atTime = ref('00:00');
const after = ref(0);
-const unit = ref('second');
+const {
+ model: unit,
+ def: unitDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._time.second, value: 'second' },
+ { label: i18n.ts._time.minute, value: 'minute' },
+ { label: i18n.ts._time.hour, value: 'hour' },
+ { label: i18n.ts._time.day, value: 'day' },
+ ],
+ initialValue: 'second',
+});
if (props.modelValue.expiresAt) {
expiration.value = 'at';
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index a3ff89fc4d..9fec7ea4da 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -567,11 +567,11 @@ async function toggleReactionAcceptance() {
const select = await os.select({
title: i18n.ts.reactionAcceptance,
items: [
- { value: null, text: i18n.ts.all },
- { value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
- { value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
- { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
- { value: 'likeOnly' as const, text: i18n.ts.likeOnly },
+ { value: null, label: i18n.ts.all },
+ { value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
+ { value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
+ { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
+ { value: 'likeOnly' as const, label: i18n.ts.likeOnly },
],
default: reactionAcceptance.value,
});
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
index f1cc98def4..937804703d 100644
--- a/packages/frontend/src/components/MkRoleSelectDialog.vue
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -102,12 +102,12 @@ async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
- .map(r => ({ text: r.name, value: r }));
+ .map(r => ({ label: r.name, value: r.id }));
- const { canceled, result: role } = await os.select({ items });
- if (canceled || role == null) return;
+ const { canceled, result: roleId } = await os.select({ items });
+ if (canceled || roleId == null) return;
- selectedRoleIds.value.push(role.id);
+ selectedRoleIds.value.push(roleId);
}
async function removeRole(roleId: string) {
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 9cbaf676c7..e79236fe54 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-type ItemOption = {
+export type OptionValue = string | number | null;
+
+export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option';
- value: string | number | null;
+ value: T;
label: string;
};
-type ItemGroup = {
+export type ItemGroup<T extends OptionValue = OptionValue> = {
type: 'group';
- label: string;
- items: ItemOption[];
+ label?: string;
+ items: ItemOption<T>[];
};
-export type MkSelectItem = ItemOption | ItemGroup;
+export type MkSelectItem<T extends OptionValue = OptionValue> = ItemOption<T> | ItemGroup<T>;
-type ValuesOfItems<T> = T extends (infer U)[]
- ? U extends { type: 'group'; items: infer V }
- ? V extends (infer W)[]
- ? W extends { value: infer X }
- ? X
- : never
- : never
- : U extends { value: infer Y }
- ? Y
- : never
+export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
+ ? T['items'][number]['value']
+ : T extends ItemOption
+ ? T['value']
+ : never;
+
+export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
+ ? GetMkSelectValueType<T[number]>
: never;
</script>
-<script lang="ts" setup generic="T extends MkSelectItem[]">
-import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
+<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
+import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
-import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
-// TODO: itemsをslot内のoptionで指定する用法は廃止する(props.itemsを必須化する)
-// see: https://github.com/misskey-dev/misskey/issues/15558
-// あと型推論と相性が良くない
-
const props = defineProps<{
- modelValue: ValuesOfItems<T>;
+ items: ITEMS;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@@ -88,16 +83,17 @@ const props = defineProps<{
inline?: boolean;
small?: boolean;
large?: boolean;
- items?: T;
}>();
-const emit = defineEmits<{
- (ev: 'update:modelValue', value: ValuesOfItems<T>): void;
-}>();
+type ModelTChecked = MODELT & (
+ MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
+ ? unknown
+ : 'Error: The type of model does not match the type of items.'
+);
-const slots = useSlots();
+const model = defineModel<ModelTChecked>({ required: true });
-const { modelValue, autofocus } = toRefs(props);
+const { autofocus } = toRefs(props);
const focused = ref(false);
const opening = ref(false);
const currentValueText = ref<string | null>(null);
@@ -140,52 +136,26 @@ onMounted(() => {
});
});
-watch([modelValue, () => props.items], () => {
- if (props.items) {
- let found: ItemOption | null = null;
- for (const item of props.items) {
- if (item.type === 'group') {
- for (const option of item.items) {
- if (option.value === modelValue.value) {
- found = option;
- break;
- }
- }
- } else {
- if (item.value === modelValue.value) {
- found = item;
+watch([model, () => props.items], () => {
+ let found: ItemOption | null = null;
+ for (const item of props.items) {
+ if (item.type === 'group') {
+ for (const option of item.items) {
+ if (option.value === model.value) {
+ found = option;
break;
}
}
- }
- if (found) {
- currentValueText.value = found.label;
- }
- return;
- }
-
- const scanOptions = (options: VNodeChild[]) => {
- for (const vnode of options) {
- if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
- if (vnode.type === 'optgroup') {
- const optgroup = vnode;
- if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
- } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
- const fragment = vnode;
- if (Array.isArray(fragment.children)) scanOptions(fragment.children);
- } else if (vnode.props == null) { // v-if で条件が false のときにこうなる
- // nop?
- } else {
- const option = vnode;
- if (option.props?.value === modelValue.value) {
- currentValueText.value = option.children as string;
- break;
- }
+ } else {
+ if (item.value === model.value) {
+ found = item;
+ break;
}
}
- };
-
- scanOptions(slots.default!());
+ }
+ if (found) {
+ currentValueText.value = found.label;
+ }
}, { immediate: true, deep: true });
function show() {
@@ -196,68 +166,32 @@ function show() {
const menu: MenuItem[] = [];
- if (props.items) {
- for (const item of props.items) {
- if (item.type === 'group') {
+ for (const item of props.items) {
+ if (item.type === 'group') {
+ if (item.label != null) {
menu.push({
type: 'label',
text: item.label,
});
- for (const option of item.items) {
- menu.push({
- text: option.label,
- active: computed(() => modelValue.value === option.value),
- action: () => {
- emit('update:modelValue', option.value);
- },
- });
- }
- } else {
+ }
+ for (const option of item.items) {
menu.push({
- text: item.label,
- active: computed(() => modelValue.value === item.value),
+ text: option.label,
+ active: computed(() => model.value === option.value),
action: () => {
- emit('update:modelValue', item.value);
+ model.value = option.value as ModelTChecked;
},
});
}
- }
- } else {
- let options = slots.default!();
-
- const pushOption = (option: VNode) => {
+ } else {
menu.push({
- text: option.children as string,
- active: computed(() => modelValue.value === option.props?.value),
+ text: item.label,
+ active: computed(() => model.value === item.value),
action: () => {
- emit('update:modelValue', option.props?.value);
+ model.value = item.value as ModelTChecked;
},
});
- };
-
- const scanOptions = (options: VNodeChild[]) => {
- for (const vnode of options) {
- if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
- if (vnode.type === 'optgroup') {
- const optgroup = vnode;
- menu.push({
- type: 'label',
- text: optgroup.props?.label,
- });
- if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
- } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
- const fragment = vnode;
- if (Array.isArray(fragment.children)) scanOptions(fragment.children);
- } else if (vnode.props == null) { // v-if で条件が false のときにこうなる
- // nop?
- } else {
- const option = vnode;
- pushOption(option);
- }
- }
- };
-
- scanOptions(options);
+ }
}
os.popupMenu(menu, container.value, {
diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
index 206298b194..75a45548fd 100644
--- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue
+++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
- <MkSelect v-model="type" :items="[{ label: i18n.ts._watermarkEditor.text, value: 'text' }, { label: i18n.ts._watermarkEditor.image, value: 'image' }, { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' }]">
+ <MkSelect v-model="type" :items="typeDef">
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
</MkSelect>
@@ -86,6 +86,7 @@ import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js';
import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
const $i = ensureSignin();
@@ -186,7 +187,18 @@ async function cancel() {
dialog.value?.close();
}
-const type = ref(preset.layers.length > 1 ? 'advanced' : preset.layers[0].type);
+const {
+ model: type,
+ def: typeDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._watermarkEditor.text, value: 'text' },
+ { label: i18n.ts._watermarkEditor.image, value: 'image' },
+ { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' },
+ ],
+ initialValue: preset.layers.length > 1 ? 'advanced' : preset.layers[0].type,
+});
+
watch(type, () => {
if (type.value === 'text') {
preset.layers = [createTextLayer()];
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 08a018ea9b..cf7c2cda80 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -7,9 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<template v-if="edit">
<header :class="$style.editHeader">
- <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
+ <MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
- <option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect>
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
@@ -59,6 +58,7 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -89,7 +89,15 @@ const widgetRefs = {};
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
-const widgetAdderSelected = ref<string | null>(null);
+
+const {
+ model: widgetAdderSelected,
+ def: widgetAdderSelectedDef,
+} = useMkSelect({
+ items: computed(() => [{ label: i18n.ts.none, value: null }, ..._widgetDefs.value.map(x => ({ label: i18n.ts._widgets[x], value: x }))]),
+ initialValue: null,
+});
+
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
diff --git a/packages/frontend/src/composables/use-mkselect.ts b/packages/frontend/src/composables/use-mkselect.ts
new file mode 100644
index 0000000000..7cb470d169
--- /dev/null
+++ b/packages/frontend/src/composables/use-mkselect.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import type { Ref, MaybeRefOrGetter } from 'vue';
+import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
+
+type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;
+
+/** 指定したオプション定義をもとに型を狭めたrefを生成するコンポーサブル */
+export function useMkSelect<
+ const TItemsInput extends MaybeRefOrGetter<MkSelectItem[]>,
+ const TItems extends TItemsInput extends MaybeRefOrGetter<infer U> ? U : never,
+ TInitialValue extends OptionValue | void = void,
+ TItemsValue = GetMkSelectValueTypesFromDef<UnwrapReadonlyItems<TItems>>,
+ ModelType = TInitialValue extends void
+ ? TItemsValue
+ : (TItemsValue | TInitialValue)
+>(opts: {
+ items: TItemsInput;
+ initialValue?: (TInitialValue | (OptionValue extends TItemsValue ? OptionValue : TInitialValue)) & (
+ TItemsValue extends TInitialValue
+ ? unknown
+ : { 'Error: Type of initialValue must include all types of items': TItemsValue }
+ );
+}): {
+ def: TItemsInput;
+ model: Ref<ModelType>;
+} {
+ const model = ref(opts.initialValue ?? null);
+
+ return {
+ def: opts.items,
+ model: model as Ref<ModelType>,
+ };
+}
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 504ae19f1d..6c5f04c6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -14,6 +14,7 @@ import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
+import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -502,50 +503,15 @@ export function authenticateDialog(): Promise<{
});
}
-type SelectItem<C> = {
- value: C;
- text: string;
-};
-
-// default が指定されていたら result は null になり得ないことを保証する overload function
-export function select<C = unknown>(props: {
+export function select<C extends OptionValue, D extends C | null = null>(props: {
title?: string;
text?: string;
- default: string;
- items: (SelectItem<C> | {
- sectionTitle: string;
- items: SelectItem<C>[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C;
-}>;
-export function select<C = unknown>(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem<C> | {
- sectionTitle: string;
- items: SelectItem<C>[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C | null;
-}>;
-export function select<C = unknown>(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem<C> | {
- sectionTitle: string;
- items: SelectItem<C>[];
- } | undefined)[];
+ default?: D;
+ items: (MkSelectItem<C> | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
- canceled: false; result: C | null;
+ canceled: false; result: Exclude<D, undefined> extends null ? C | null : C;
}> {
return new Promise(resolve => {
const { dispose } = popup(MkDialog, {
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index 568bf9fd1a..bbfb9a3b7c 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -11,56 +11,10 @@ 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="silenced">{{ i18n.ts.silence }}</option>
- <option value="blocked">{{ i18n.ts.blocked }}</option>
- <option value="notResponding">{{ i18n.ts.notResponding }}</option>
</MkSelect>
- <MkSelect
- v-model="sort" :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',
- }]"
- >
+ <MkSelect v-model="sort" :items="sortDef">
<template #label>{{ i18n.ts.sort }}</template>
</MkSelect>
</FormSplit>
@@ -85,11 +39,46 @@ import MkPagination from '@/components/MkPagination.vue';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import FormSplit from '@/components/form/split.vue';
import { i18n } from '@/i18n.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { Paginator } from '@/utility/paginator.js';
const host = ref('');
-const state = ref('federating');
-const sort = ref<NonNullable<Misskey.entities.FederationInstancesRequest['sort']>>('+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.silence, value: 'silenced' },
+ { label: i18n.ts.blocked, value: 'blocked' },
+ { 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-user.vue b/packages/frontend/src/pages/admin-user.vue
index 86dfd38d81..24f2d0d5eb 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -153,17 +153,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'announcements'" class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
- <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>
<MkPagination :paginator="announcementsPaginator">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
- <span style="margin-right: 0.5em;">
+ <span v-if="'icon' in announcement" style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
@@ -184,8 +182,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshm">
<div class="selects">
- <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
- <option value="per-user-notes">{{ i18n.ts.notes }}</option>
+ <MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0 10px 0 0; flex: 1;">
</MkSelect>
</div>
<div class="charts">
@@ -229,6 +226,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { acct } from '@/filters/user.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -247,7 +245,15 @@ const props = withDefaults(defineProps<{
const result = await _fetch_();
const tab = ref(props.initialTab);
-const chartSrc = ref<ChartSrc>('per-user-notes');
+const {
+ model: chartSrc,
+ def: chartSrcDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts.notes, value: 'per-user-notes' },
+],
+ initialValue: 'per-user-notes',
+});
const user = ref(result.user);
const info = ref(result.info);
const ips = ref(result.ips);
@@ -264,7 +270,16 @@ const filesPaginator = markRaw(new Paginator('admin/drive/files', {
})),
}));
-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 announcementsPaginator = markRaw(new Paginator('admin/announcements/list', {
limit: 10,
@@ -428,22 +443,22 @@ async function assignRole() {
const { canceled, result: roleId } = await os.select({
title: i18n.ts._role.chooseRoleToAssign,
- items: roles.map(r => ({ text: r.name, value: r.id })),
+ items: roles.map(r => ({ label: r.name, value: r.id })),
});
if (canceled || roleId == null) return;
const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.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/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..17008e0c13 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -6,11 +6,8 @@ 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">
@@ -95,6 +92,7 @@ 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 +100,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 +129,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 +142,7 @@ const filterItems = (v) => {
};
// 選択された曜日(index)のビットフラグを操作する
-function toggleDayOfWeek(ad, index) {
+function toggleDayOfWeek(ad: Misskey.entities.Ad, index: number) {
ad.dayOfWeek ^= 1 << index;
}
@@ -153,7 +161,7 @@ function add() {
});
}
-function remove(ad) {
+function remove(ad: Misskey.entities.Ad) {
os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: ad.url }),
@@ -169,7 +177,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 176d1121c5..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[]>([]);
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/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.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue
index 6192d6eb0f..5edc01404c 100644
--- a/packages/frontend/src/pages/admin/overview.heatmap.vue
+++ b/packages/frontend/src/pages/admin/overview.heatmap.vue
@@ -5,24 +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 type { HeatmapSource } from '@/components/MkHeatmap.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
-const src = ref<HeatmapSource>('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/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 66004a44bb..e10eb1163e 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>
@@ -841,6 +842,7 @@ import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/utility/clone.js';
+import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
const emit = defineEmits<{
(ev: 'update:modelValue', v: any): void;
@@ -870,11 +872,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 8e83cdb667..2e249eee50 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -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 6483bf16a8..7a49860b2d 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>
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', {
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index ddc4e89ef1..a8ce527523 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -101,12 +101,12 @@ async function addRole() {
const roles = await misskeyApi('admin/roles/list');
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
- const { canceled, result: role } = await os.select({
- items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+ const { canceled, result: roleId } = await os.select({
+ items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ label: r.name, value: r.id })),
});
- if (canceled || role == null) return;
+ if (canceled || roleId == null) return;
- rolesThatCanBeUsedThisDecoration.value.push(role);
+ rolesThatCanBeUsedThisDecoration.value.push(roles.find(r => r.id === roleId)!);
}
async function removeRole(role, ev) {
diff --git a/packages/frontend/src/pages/debug.vue b/packages/frontend/src/pages/debug.vue
index 5cd68c2c3a..9c0761f0b1 100644
--- a/packages/frontend/src/pages/debug.vue
+++ b/packages/frontend/src/pages/debug.vue
@@ -11,11 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkResult v-if="resultType === 'notFound'" type="notFound"/>
<MkResult v-if="resultType === 'error'" type="error"/>
<MkSelect
- v-model="resultType" :items="[
- { label: 'empty', value: 'empty' },
- { label: 'notFound', value: 'notFound' },
- { label: 'error', value: 'error' },
- ]"
+ v-model="resultType" :items="resultTypeDef"
></MkSelect>
<MkSystemIcon v-if="iconType === 'info'" type="info" style="width: 150px;"/>
@@ -25,14 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSystemIcon v-if="iconType === 'error'" type="error" style="width: 150px;"/>
<MkSystemIcon v-if="iconType === 'waiting'" type="waiting" style="width: 150px;"/>
<MkSelect
- v-model="iconType" :items="[
- { label: 'info', value: 'info' },
- { label: 'question', value: 'question' },
- { label: 'success', value: 'success' },
- { label: 'warn', value: 'warn' },
- { label: 'error', value: 'error' },
- { label: 'waiting', value: 'waiting' },
- ]"
+ v-model="iconType" :items="iconTypeDef"
></MkSelect>
<div class="_buttons">
@@ -56,10 +45,34 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import * as os from '@/os.js';
-const resultType = ref('empty');
-const iconType = ref('info');
+const {
+ model: resultType,
+ def: resultTypeDef,
+} = useMkSelect({
+ items: [
+ { label: 'empty', value: 'empty' },
+ { label: 'notFound', value: 'notFound' },
+ { label: 'error', value: 'error' },
+ ],
+ initialValue: 'empty',
+});
+const {
+ model: iconType,
+ def: iconTypeDef,
+} = useMkSelect({
+ items: [
+ { label: 'info', value: 'info' },
+ { label: 'question', value: 'question' },
+ { label: 'success', value: 'success' },
+ { label: 'warn', value: 'warn' },
+ { label: 'error', value: 'error' },
+ { label: 'waiting', value: 'waiting' },
+ ],
+ initialValue: 'info',
+});
definePage(() => ({
title: 'DEBUG ROOM',
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index c1a8b992b7..0a69dbdd70 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -23,12 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_woodenFrame" style="text-align: center;">
<div class="_woodenFrameInner">
<div class="_gaps" style="padding: 16px;">
- <MkSelect v-model="gameMode">
- <option value="normal">NORMAL</option>
- <option value="square">SQUARE</option>
- <option value="yen">YEN</option>
- <option value="sweets">SWEETS</option>
- <!--<option value="space">SPACE</option>-->
+ <MkSelect v-model="gameMode" :items="gameModeDef">
</MkSelect>
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
</div>
@@ -92,11 +87,24 @@ import XGame from './drop-and-fusion.game.vue';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { misskeyApiGet } from '@/utility/misskey-api.js';
-const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
+const {
+ model: gameMode,
+ def: gameModeDef,
+} = useMkSelect({
+ items: [
+ { label: 'NORMAL', value: 'normal' },
+ { label: 'SQUARE', value: 'square' },
+ { label: 'YEN', value: 'yen' },
+ { label: 'SWEETS', value: 'sweets' },
+ //{ label: 'SPACE', value: 'space' },
+ ],
+ initialValue: 'normal',
+});
const gameStarted = ref(false);
const mute = ref(false);
const ranking = ref<Misskey.entities.BubbleGameRankingResponse | null>(null);
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 033e3376a5..ea4863950d 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -135,12 +135,12 @@ async function addRole() {
const roles = await misskeyApi('admin/roles/list');
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id);
- const { canceled, result: role } = await os.select({
- items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+ const { canceled, result: roleId } = await os.select({
+ items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ label: r.name, value: r.id })),
});
- if (canceled || role == null) return;
+ if (canceled || roleId == null) return;
- rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
+ rolesThatCanBeUsedThisEmojiAsReaction.value.push(roles.find(r => r.id === roleId)!);
}
async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 81b9d1cead..b3e8e88c23 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -10,11 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="title">
<template #label>{{ i18n.ts._play.title }}</template>
</MkInput>
- <MkSelect v-model="visibility">
+ <MkSelect v-model="visibility" :items="visibilityDef">
<template #label>{{ i18n.ts.visibility }}</template>
<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
- <option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
- <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
</MkSelect>
<MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
<template #label>{{ i18n.ts._play.summary }}</template>
@@ -52,6 +50,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { useRouter } from '@/router.js';
const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION}
@@ -384,7 +383,16 @@ if (props.id) {
const title = ref(flash.value?.title ?? 'New Play');
const summary = ref(flash.value?.summary ?? '');
const permissions = ref([]); // not implemented yet
-const visibility = ref<'private' | 'public'>(flash.value?.visibility ?? 'public');
+const {
+ model: visibility,
+ def: visibilityDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts.public, value: 'public' },
+ { label: i18n.ts.private, value: 'private' },
+ ],
+ initialValue: flash.value?.visibility ?? 'public',
+});
const script = ref(flash.value?.script ?? PRESET_DEFAULT);
function selectPreset(ev: MouseEvent) {
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 473207fe6e..61a40202c0 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -92,18 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div>
<div :class="$style.selects">
- <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
- <option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
- <option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
- <option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
- <option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
- <option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
- <option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
- <option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
- <option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
- <option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
- <option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
+ <MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0 10px 0 0; flex: 1;">
</MkSelect>
</div>
<div>
@@ -154,6 +143,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination from '@/components/MkPagination.vue';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
import { dateString } from '@/filters/date.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import MkTextarea from '@/components/MkTextarea.vue';
import { Paginator } from '@/utility/paginator.js';
@@ -163,7 +153,25 @@ const props = defineProps<{
const tab = ref('overview');
-const chartSrc = ref<ChartSrc>('instance-requests');
+const {
+ model: chartSrc,
+ def: chartSrcDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._instanceCharts.requests, value: 'instance-requests' },
+ { label: i18n.ts._instanceCharts.users, value: 'instance-users' },
+ { label: i18n.ts._instanceCharts.usersTotal, value: 'instance-users-total' },
+ { label: i18n.ts._instanceCharts.notes, value: 'instance-notes' },
+ { label: i18n.ts._instanceCharts.notesTotal, value: 'instance-notes-total' },
+ { label: i18n.ts._instanceCharts.ff, value: 'instance-ff' },
+ { label: i18n.ts._instanceCharts.ffTotal, value: 'instance-ff-total' },
+ { label: i18n.ts._instanceCharts.cacheSize, value: 'instance-drive-usage' },
+ { label: i18n.ts._instanceCharts.cacheSizeTotal, value: 'instance-drive-usage-total' },
+ { label: i18n.ts._instanceCharts.files, value: 'instance-drive-files' },
+ { label: i18n.ts._instanceCharts.filesTotal, value: 'instance-drive-files-total' },
+ ],
+ initialValue: 'instance-requests',
+});
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null);
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding' | 'softwareSuspended'>('none');
diff --git a/packages/frontend/src/pages/page-editor/common.ts b/packages/frontend/src/pages/page-editor/common.ts
index 420c8fc967..64cd9cde7a 100644
--- a/packages/frontend/src/pages/page-editor/common.ts
+++ b/packages/frontend/src/pages/page-editor/common.ts
@@ -4,12 +4,13 @@
*/
import { i18n } from '@/i18n.js';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
export function getPageBlockList() {
return [
- { value: 'section', text: i18n.ts._pages.blocks.section },
- { value: 'text', text: i18n.ts._pages.blocks.text },
- { value: 'image', text: i18n.ts._pages.blocks.image },
- { value: 'note', text: i18n.ts._pages.blocks.note },
- ];
+ { value: 'section', label: i18n.ts._pages.blocks.section },
+ { value: 'text', label: i18n.ts._pages.blocks.text },
+ { value: 'image', label: i18n.ts._pages.blocks.image },
+ { value: 'note', label: i18n.ts._pages.blocks.note },
+ ] as const satisfies MkSelectItem[];
}
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 3c51f25676..3dd83b25c5 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -30,10 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="alignCenter">{{ i18n.ts._pages.alignCenter }}</MkSwitch>
- <MkSelect v-model="font">
+ <MkSelect v-model="font" :items="fontDef">
<template #label>{{ i18n.ts._pages.font }}</template>
- <option value="serif">{{ i18n.ts._pages.fontSerif }}</option>
- <option value="sans-serif">{{ i18n.ts._pages.fontSansSerif }}</option>
</MkSelect>
<MkSwitch v-model="hideTitleWhenPinned">{{ i18n.ts._pages.hideTitleWhenPinned }}</MkSwitch>
@@ -76,6 +74,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';
import { mainRouter } from '@/router.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { getPageBlockList } from '@/pages/page-editor/common.js';
const props = defineProps<{
@@ -95,7 +94,16 @@ const summary = ref<string | null>(null);
const name = ref(Date.now().toString());
const eyeCatchingImage = ref<Misskey.entities.DriveFile | null>(null);
const eyeCatchingImageId = ref<string | null>(null);
-const font = ref<'sans-serif' | 'serif'>('sans-serif');
+const {
+ model: font,
+ def: fontDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._pages.fontSansSerif, value: 'sans-serif' },
+ { label: i18n.ts._pages.fontSerif, value: 'serif' },
+ ],
+ initialValue: 'sans-serif',
+});
const content = ref<Misskey.entities.Page['content']>([]);
const alignCenter = ref(false);
const hideTitleWhenPinned = ref(false);
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 63b3c95233..57192c0fb7 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -5,9 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
- <MkSelect v-model="sortModeSelect">
+ <MkSelect v-model="sortModeSelect" :items="sortModeSelectDef">
<template #label>{{ i18n.ts.sort }}</template>
- <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option>
</MkSelect>
<div v-if="!fetching">
<MkPagination v-slot="{items}" :paginator="paginator">
@@ -60,6 +59,7 @@ import { i18n } from '@/i18n.js';
import bytes from '@/filters/bytes.js';
import { definePage } from '@/page.js';
import MkSelect from '@/components/MkSelect.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
@@ -69,15 +69,19 @@ const paginator = markRaw(new Paginator('drive/files', {
computedParams: computed(() => ({ sort: sortMode.value })),
}));
-const sortOptions = [
- { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc },
- { value: 'createdAtAsc', displayName: i18n.ts._drivecleaner.orderByCreatedAtAsc },
-];
-
const capacity = ref<number>(0);
const usage = ref<number>(0);
const fetching = ref(true);
-const sortModeSelect = ref('sizeDesc');
+const {
+ model: sortModeSelect,
+ def: sortModeSelectDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._drivecleaner.orderBySizeDesc, value: 'sizeDesc' },
+ { label: i18n.ts._drivecleaner.orderByCreatedAtAsc, value: 'createdAtAsc' },
+ ],
+ initialValue: 'sizeDesc',
+});
fetchDriveInfo();
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index 5ff5f45a2f..34bf1c14af 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -36,20 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['main', 'palette']">
<MkPreferenceContainer k="emojiPaletteForMain">
- <MkSelect v-model="emojiPaletteForMain">
+ <MkSelect v-model="emojiPaletteForMain" :items="emojiPaletteForMainDef">
<template #label><SearchLabel>{{ i18n.ts._emojiPalette.paletteForMain }}</SearchLabel></template>
- <option key="-" :value="null">({{ i18n.ts.auto }})</option>
- <option v-for="palette in prefer.r.emojiPalettes.value" :key="palette.id" :value="palette.id">{{ palette.name === '' ? '(' + i18n.ts.noName + ')' : palette.name }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['reaction', 'palette']">
<MkPreferenceContainer k="emojiPaletteForReaction">
- <MkSelect v-model="emojiPaletteForReaction">
+ <MkSelect v-model="emojiPaletteForReaction" :items="emojiPaletteForReactionDef">
<template #label><SearchLabel>{{ i18n.ts._emojiPalette.paletteForReaction }}</SearchLabel></template>
- <option key="-" :value="null">({{ i18n.ts.auto }})</option>
- <option v-for="palette in prefer.r.emojiPalettes.value" :key="palette.id" :value="palette.id">{{ palette.name === '' ? '(' + i18n.ts.noName + ')' : palette.name }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@@ -99,12 +95,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'picker', 'style']">
<MkPreferenceContainer k="emojiPickerStyle">
- <MkSelect v-model="emojiPickerStyle">
+ <MkSelect v-model="emojiPickerStyle" :items="[
+ { label: i18n.ts.auto, value: 'auto' },
+ { label: i18n.ts.popup, value: 'popup' },
+ { label: i18n.ts.drawer, value: 'drawer' },
+ ]">
<template #label><SearchLabel>{{ i18n.ts.style }}</SearchLabel></template>
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
- <option value="auto">{{ i18n.ts.auto }}</option>
- <option value="popup">{{ i18n.ts.popup }}</option>
- <option value="drawer">{{ i18n.ts.drawer }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@@ -125,6 +122,7 @@ import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkSelect from '@/components/MkSelect.vue';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -135,7 +133,21 @@ import MkSwitch from '@/components/MkSwitch.vue';
import { emojiPicker } from '@/utility/emoji-picker.js';
const emojiPaletteForReaction = prefer.model('emojiPaletteForReaction');
+const emojiPaletteForReactionDef = computed<MkSelectItem[]>(() => [
+ { label: `(${i18n.ts.auto})`, value: null },
+ ...prefer.s.emojiPalettes.map(palette => ({
+ label: palette.name === '' ? `(${i18n.ts.noName})` : palette.name,
+ value: palette.id,
+ })),
+]);
const emojiPaletteForMain = prefer.model('emojiPaletteForMain');
+const emojiPaletteForMainDef = computed<MkSelectItem[]>(() => [
+ { label: `(${i18n.ts.auto})`, value: null },
+ ...prefer.s.emojiPalettes.map(palette => ({
+ label: palette.name === '' ? `(${i18n.ts.noName})` : palette.name,
+ value: palette.id,
+ })),
+]);
const emojiPickerScale = prefer.model('emojiPickerScale');
const emojiPickerWidth = prefer.model('emojiPickerWidth');
const emojiPickerHeight = prefer.model('emojiPickerHeight');
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index f7c634b42e..c8cbc0977f 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -86,9 +86,9 @@ async function addItem() {
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: [...menu.map(k => ({
- value: k, text: navbarItemDef[k].title,
+ value: k, label: navbarItemDef[k].title,
})), {
- value: '-', text: i18n.ts.divider,
+ value: '-', label: i18n.ts.divider,
}],
});
if (canceled || item == null) return;
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index 0ea415f673..78c3312c27 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -5,13 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <MkSelect v-model="type">
- <option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
+ <MkSelect v-model="type" :items="typeDef">
</MkSelect>
- <MkSelect v-if="type === 'list'" v-model="userListId">
+ <MkSelect v-if="type === 'list'" v-model="userListId" :items="userListIdDef">
<template #label>{{ i18n.ts.userList }}</template>
- <option v-for="list in props.userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<div class="_buttons">
@@ -41,9 +39,10 @@ export type NotificationConfig = {
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -66,8 +65,26 @@ const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[numb
never: i18n.ts.none,
};
-const type = ref(props.value.type);
-const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
+const {
+ model: type,
+ def: typeDef,
+} = useMkSelect({
+ items: computed(() => (props.configurableTypes ?? notificationConfigTypes).map((t: NotificationConfig['type']) => ({
+ label: notificationConfigTypesI18nMap[t],
+ value: t,
+ }))),
+ initialValue: props.value.type,
+});
+const {
+ model: userListId,
+ def: userListIdDef,
+} = useMkSelect({
+ items: computed(() => props.userLists.map(list => ({
+ label: list.name,
+ value: list.id,
+ }))),
+ initialValue: props.value.type === 'list' ? props.value.userListId : null,
+});
function save() {
emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index ba35dd7f43..78f0891a58 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -18,9 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['language']">
- <MkSelect v-model="lang">
+ <MkSelect v-model="lang" :items="langs.map(x => ({ label: x[1], value: x[0] }))">
<template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
- <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
<template #caption>
<I18n :src="i18n.ts.i18nInfo" tag="span">
<template #link>
@@ -272,22 +271,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
<MkPreferenceContainer k="instanceTicker">
- <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
+ <MkSelect
+ v-if="instance.federation !== 'none'"
+ v-model="instanceTicker"
+ :items="[
+ { label: i18n.ts._instanceTicker.none, value: 'none' },
+ { label: i18n.ts._instanceTicker.remote, value: 'remote' },
+ { label: i18n.ts._instanceTicker.always, value: 'always' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
- <option value="none">{{ i18n.ts._instanceTicker.none }}</option>
- <option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
- <option value="always">{{ i18n.ts._instanceTicker.always }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
<MkPreferenceContainer k="nsfw">
- <MkSelect v-model="nsfw">
+ <MkSelect
+ v-model="nsfw"
+ :items="[
+ { label: i18n.ts._displayOfSensitiveMedia.respect, value: 'respect' },
+ { label: i18n.ts._displayOfSensitiveMedia.ignore, value: 'ignore' },
+ { label: i18n.ts._displayOfSensitiveMedia.force, value: 'force' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
- <option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
- <option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
- <option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@@ -339,11 +347,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkPreferenceContainer k="defaultNoteVisibility">
- <MkSelect v-model="defaultNoteVisibility">
- <option value="public">{{ i18n.ts._visibility.public }}</option>
- <option value="home">{{ i18n.ts._visibility.home }}</option>
- <option value="followers">{{ i18n.ts._visibility.followers }}</option>
- <option value="specified">{{ i18n.ts._visibility.specified }}</option>
+ <MkSelect
+ v-model="defaultNoteVisibility"
+ :items="[
+ { label: i18n.ts._visibility.public, value: 'public' },
+ { label: i18n.ts._visibility.home, value: 'home' },
+ { label: i18n.ts._visibility.followers, value: 'followers' },
+ { label: i18n.ts._visibility.specified, value: 'specified' },
+ ]"
+ >
</MkSelect>
</MkPreferenceContainer>
@@ -528,22 +540,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']">
<MkPreferenceContainer k="menuStyle">
- <MkSelect v-model="menuStyle">
+ <MkSelect
+ v-model="menuStyle"
+ :items="[
+ { label: i18n.ts.auto, value: 'auto' },
+ { label: i18n.ts.popup, value: 'popup' },
+ { label: i18n.ts.drawer, value: 'drawer' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template>
- <option value="auto">{{ i18n.ts.auto }}</option>
- <option value="popup">{{ i18n.ts.popup }}</option>
- <option value="drawer">{{ i18n.ts.drawer }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['contextmenu', 'system', 'native']">
<MkPreferenceContainer k="contextMenu">
- <MkSelect v-model="contextMenu">
+ <MkSelect
+ v-model="contextMenu"
+ :items="[
+ { label: i18n.ts._contextMenu.app, value: 'app' },
+ { label: i18n.ts._contextMenu.appWithShift, value: 'appWithShift' },
+ { label: i18n.ts._contextMenu.native, value: 'native' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template>
- <option value="app">{{ i18n.ts._contextMenu.app }}</option>
- <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
- <option value="native">{{ i18n.ts._contextMenu.native }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@@ -719,11 +739,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
<MkPreferenceContainer k="serverDisconnectedBehavior">
- <MkSelect v-model="serverDisconnectedBehavior">
+ <MkSelect
+ v-model="serverDisconnectedBehavior"
+ :items="[
+ { label: i18n.ts._serverDisconnectedBehavior.reload, value: 'reload' },
+ { label: i18n.ts._serverDisconnectedBehavior.dialog, value: 'dialog' },
+ { label: i18n.ts._serverDisconnectedBehavior.quiet, value: 'quiet' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
- <option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
- <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
- <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@@ -984,16 +1008,15 @@ function removeEmojiIndex(lang: string) {
async function setPinnedList() {
const lists = await misskeyApi('users/lists/list');
- const { canceled, result: list } = await os.select({
+ const { canceled, result: listId } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
});
- if (canceled) return;
- if (list == null) return;
+ if (canceled || listId == null) return;
- prefer.commit('pinnedUserLists', [list]);
+ prefer.commit('pinnedUserLists', [lists.find((x) => x.id === listId)!]);
}
function removePinnedList() {
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 8ae8c79ebd..d3020f068b 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -33,20 +33,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['following', 'visibility']">
- <MkSelect v-model="followingVisibility" @update:modelValue="save()">
+ <MkSelect v-model="followingVisibility" :items="followingVisibilityDef" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.followingVisibility }}</SearchLabel></template>
- <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
- <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
- <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
</MkSelect>
</SearchMarker>
<SearchMarker :keywords="['follower', 'visibility']">
- <MkSelect v-model="followersVisibility" @update:modelValue="save()">
+ <MkSelect v-model="followersVisibility" :items="followersVisibilityDef" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.followersVisibility }}</SearchLabel></template>
- <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
- <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
- <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
</MkSelect>
</SearchMarker>
@@ -85,13 +79,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkInfo v-if="$i.policies.chatAvailability === 'unavailable'">{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
<SearchMarker :keywords="['chat']">
- <MkSelect v-model="chatScope" @update:modelValue="save()">
+ <MkSelect v-model="chatScope" :items="chatScopeDef" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts._chat.chatAllowedUsers }}</SearchLabel></template>
- <option value="everyone">{{ i18n.ts._chat._chatAllowedUsers.everyone }}</option>
- <option value="followers">{{ i18n.ts._chat._chatAllowedUsers.followers }}</option>
- <option value="following">{{ i18n.ts._chat._chatAllowedUsers.following }}</option>
- <option value="mutual">{{ i18n.ts._chat._chatAllowedUsers.mutual }}</option>
- <option value="none">{{ i18n.ts._chat._chatAllowedUsers.none }}</option>
<template #caption>{{ i18n.ts._chat.chatAllowedUsers_note }}</template>
</MkSelect>
</SearchMarker>
@@ -119,15 +108,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</SearchLabel></template>
<div class="_gaps_s">
- <MkSelect v-model="makeNotesFollowersOnlyBefore_type">
- <option :value="null">{{ i18n.ts.none }}</option>
- <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
- <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+ <MkSelect
+ v-model="makeNotesFollowersOnlyBefore_type"
+ :items="[
+ { label: i18n.ts.none, value: null },
+ { label: i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod, value: 'relative' },
+ { label: i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime, value: 'absolute' },
+ ]"
+ >
</MkSelect>
- <MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore_selection">
- <option v-for="preset in makeNotesFollowersOnlyBefore_presets" :value="preset.value">{{ preset.label }}</option>
- <option value="custom">{{ i18n.ts.custom }}</option>
+ <MkSelect
+ v-if="makeNotesFollowersOnlyBefore_type === 'relative'"
+ v-model="makeNotesFollowersOnlyBefore_selection"
+ :items="[
+ ...makeNotesFollowersOnlyBefore_presets,
+ { label: i18n.ts.custom, value: 'custom' },
+ ]"
+ >
</MkSelect>
<MkInput
@@ -162,22 +160,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<MkSelect
v-model="makeNotesHiddenBefore_type"
- :items="[{
- value: null,
- label: i18n.ts.none
- }, {
- value: 'relative',
- label: i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod
- }, {
- value: 'absolute',
- label: i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime
- }]"
+ :items="[
+ { label: i18n.ts.none, value: null },
+ { label: i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod, value: 'relative' },
+ { label: i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime, value: 'absolute' },
+ ]"
>
</MkSelect>
- <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore_selection">
- <option v-for="preset in makeNotesHiddenBefore_presets" :value="preset.value">{{ preset.label }}</option>
- <option value="custom">{{ i18n.ts.custom }}</option>
+ <MkSelect
+ v-if="makeNotesHiddenBefore_type === 'relative'"
+ v-model="makeNotesHiddenBefore_selection"
+ :items="[
+ ...makeNotesHiddenBefore_presets,
+ { label: i18n.ts.custom, value: 'custom' },
+ ]"
+ >
</MkSelect>
<MkInput
@@ -217,6 +215,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed, watch } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
@@ -225,6 +224,7 @@ import { ensureSignin } from '@/i.js';
import { definePage } from '@/page.js';
import FormSlot from '@/components/form/slot.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';
import MkDisableSection from '@/components/MkDisableSection.vue';
@@ -243,9 +243,41 @@ const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null
const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
-const followingVisibility = ref($i.followingVisibility);
-const followersVisibility = ref($i.followersVisibility);
-const chatScope = ref($i.chatScope);
+const {
+ model: followingVisibility,
+ def: followingVisibilityDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts.public, value: 'public' },
+ { label: i18n.ts.followers, value: 'followers' },
+ { label: i18n.ts.private, value: 'private' },
+ ],
+ initialValue: $i.followingVisibility,
+});
+const {
+ model: followersVisibility,
+ def: followersVisibilityDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts.public, value: 'public' },
+ { label: i18n.ts.followers, value: 'followers' },
+ { label: i18n.ts.private, value: 'private' },
+ ],
+ initialValue: $i.followersVisibility,
+});
+const {
+ model: chatScope,
+ def: chatScopeDef,
+} = useMkSelect({
+ items: [
+ { label: i18n.ts._chat._chatAllowedUsers.everyone, value: 'everyone' },
+ { label: i18n.ts._chat._chatAllowedUsers.followers, value: 'followers' },
+ { label: i18n.ts._chat._chatAllowedUsers.following, value: 'following' },
+ { label: i18n.ts._chat._chatAllowedUsers.mutual, value: 'mutual' },
+ { label: i18n.ts._chat._chatAllowedUsers.none, value: 'none' },
+ ],
+ initialValue: $i.chatScope,
+});
const makeNotesFollowersOnlyBefore_type = computed({
get: () => {
@@ -276,7 +308,7 @@ const makeNotesFollowersOnlyBefore_presets = [
{ label: i18n.ts.oneMonth, value: -2592000 },
{ label: i18n.ts.threeMonths, value: -7776000 },
{ label: i18n.ts.oneYear, value: -31104000 },
-];
+] satisfies MkSelectItem[];
const makeNotesFollowersOnlyBefore_isCustomMode = ref(
makeNotesFollowersOnlyBefore.value != null &&
@@ -328,7 +360,7 @@ const makeNotesHiddenBefore_presets = [
{ label: i18n.ts.oneMonth, value: -2592000 },
{ label: i18n.ts.threeMonths, value: -7776000 },
{ label: i18n.ts.oneYear, value: -31104000 },
-];
+] satisfies MkSelectItem[];
const makeNotesHiddenBefore_isCustomMode = ref(
makeNotesHiddenBefore.value != null &&
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 4816a6e33b..17e8505474 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -53,9 +53,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['language', 'locale']">
- <MkSelect v-model="profile.lang">
+ <MkSelect v-model="profile.lang" :items="Object.entries(langmap).map(([code, def]) => ({ label: def.nativeName, value: code }))">
<template #label><SearchLabel>{{ i18n.ts.language }}</SearchLabel></template>
- <option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
</MkSelect>
</SearchMarker>
@@ -117,13 +116,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['reaction']">
- <MkSelect v-model="reactionAcceptance">
+ <MkSelect
+ v-model="reactionAcceptance"
+ :items="[
+ { label: i18n.ts.all, value: null },
+ { label: i18n.ts.likeOnlyForRemote, value: 'likeOnlyForRemote' },
+ { label: i18n.ts.nonSensitiveOnly, value: 'nonSensitiveOnly' },
+ { label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote, value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' },
+ { label: i18n.ts.likeOnly, value: 'likeOnly' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.reactionAcceptance }}</SearchLabel></template>
- <option :value="null">{{ i18n.ts.all }}</option>
- <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option>
- <option value="nonSensitiveOnly">{{ i18n.ts.nonSensitiveOnly }}</option>
- <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
- <option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
</MkSelect>
</SearchMarker>
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 9e9671487e..31fe9a64db 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -5,9 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <MkSelect v-model="type">
+ <MkSelect v-model="type" :items="typeDef">
<template #label>{{ i18n.ts.sound }}</template>
- <option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
</MkSelect>
<div v-if="type === '_driveFile_' && driveFileError === true" :class="$style.fileSelectorRoot">
<MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
@@ -38,6 +37,7 @@ import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/drive.js';
@@ -51,7 +51,16 @@ const emit = defineEmits<{
(ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void;
}>();
-const type = ref<SoundType>(props.def.type);
+const {
+ model: type,
+ def: typeDef,
+} = useMkSelect({
+ items: soundsTypes.map((x) => ({
+ label: getSoundTypeName(x),
+ value: x,
+ })),
+ initialValue: props.def.type,
+});
const fileId = ref('fileId' in props.def ? props.def.fileId : undefined);
const fileUrl = ref('fileUrl' in props.def ? props.def.fileUrl : undefined);
const fileName = ref<string>('');
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 561d31148f..b69fd2596d 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -5,11 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <MkSelect v-model="statusbar.type" placeholder="Please select">
+ <MkSelect v-model="statusbar.type" :items="statusbarTypeDef">
<template #label>{{ i18n.ts.type }}</template>
- <option value="rss">RSS</option>
- <option v-if="instance.federation !== 'none'" value="federation">Federation</option>
- <option value="userList">User list timeline</option>
</MkSelect>
<MkInput v-model="statusbar.name" manualSave>
@@ -63,9 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
<template v-else-if="statusbar.type === 'userList' && userLists != null">
- <MkSelect v-model="statusbar.props.userListId">
+ <MkSelect v-model="statusbar.props.userListId" :items="userListsDef">
<template #label>{{ i18n.ts.userList }}</template>
- <option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number">
<template #label>{{ i18n.ts.refreshInterval }}</template>
@@ -86,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { reactive, watch } from 'vue';
+import { reactive, computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
@@ -98,13 +94,32 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/utility/clone.js';
import { prefer } from '@/preferences.js';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
+import type { StatusbarStore } from '@/preferences/def.js';
const props = defineProps<{
_id: string;
userLists: Misskey.entities.UserList[] | null;
}>();
-const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id))!);
+const statusbar = reactive<StatusbarStore>(deepClone(prefer.s.statusbars.find(x => x.id === props._id)!));
+
+const statusbarTypeDef = computed(() => {
+ const items = [
+ { label: 'RSS', value: 'rss' },
+ ] satisfies MkSelectItem[];
+ if (instance.federation !== 'none') {
+ items.push({ label: 'Federation', value: 'federation' });
+ }
+ if (props.userLists != null) {
+ items.push({ label: i18n.ts.userList, value: 'userList' });
+ }
+ return items;
+});
+
+const userListsDef = computed(() => {
+ return (props.userLists ?? []).map(x => ({ label: x.name, value: x.id })) satisfies MkSelectItem[];
+});
watch(() => statusbar.type, () => {
if (statusbar.type === 'rss') {
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index e972184278..7bb877ec39 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -5,16 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <MkSelect v-model="selectedThemeId">
+ <MkSelect v-model="selectedThemeId" :items="selectedThemeIdDef">
<template #label>{{ i18n.ts.theme }}</template>
- <optgroup :label="i18n.ts._theme.installedThemes">
- <option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="i18n.ts._theme.builtinThemes">
- <option v-for="x in builtinThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
</MkSelect>
- <template v-if="selectedTheme">
+ <template v-if="selectedTheme != null">
<MkInput readonly :modelValue="selectedTheme.author">
<template #label>{{ i18n.ts.author }}</template>
</MkInput>
@@ -43,10 +37,26 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
+import { useMkSelect } from '@/composables/use-mkselect.js';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef();
-const selectedThemeId = ref<string | null>(null);
+const {
+ model: selectedThemeId,
+ def: selectedThemeIdDef,
+} = useMkSelect({
+ items: computed<MkSelectItem<string | null>[]>(() => [{
+ type: 'group',
+ label: i18n.ts._theme.installedThemes,
+ items: installedThemes.value.map(x => ({ label: x.name, value: x.id })),
+ }, {
+ type: 'group',
+ label: i18n.ts._theme.builtinThemes,
+ items: builtinThemes.value.map(x => ({ label: x.name, value: x.id })),
+ }]),
+ initialValue: null,
+});
const themes = computed(() => [...installedThemes.value, ...builtinThemes.value]);
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index d26d590851..b6d3d55a5f 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -447,16 +447,16 @@ export class PreferencesManager {
title: i18n.ts.preferenceSyncConflictTitle,
text: i18n.ts.preferenceSyncConflictText,
items: [...(mergedValue !== undefined ? [{
- text: i18n.ts.preferenceSyncConflictChoiceMerge,
- value: 'merge',
+ label: i18n.ts.preferenceSyncConflictChoiceMerge,
+ value: 'merge' as const,
}] : []), {
- text: i18n.ts.preferenceSyncConflictChoiceServer,
- value: 'remote',
+ label: i18n.ts.preferenceSyncConflictChoiceServer,
+ value: 'remote' as const,
}, {
- text: i18n.ts.preferenceSyncConflictChoiceDevice,
- value: 'local',
+ label: i18n.ts.preferenceSyncConflictChoiceDevice,
+ value: 'local' as const,
}, {
- text: i18n.ts.preferenceSyncConflictChoiceCancel,
+ label: i18n.ts.preferenceSyncConflictChoiceCancel,
value: null,
}],
default: mergedValue !== undefined ? 'merge' : 'remote',
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index 80949f4971..33d379509a 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -187,7 +187,7 @@ export async function restoreFromCloudBackup() {
const select = await os.select({
title: i18n.ts._preferencesBackup.selectBackupToRestore,
items: backups.map(backup => ({
- text: backup.name,
+ label: backup.name,
value: backup.name,
})),
});
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 9f6d8267f7..e2ee4b658e 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -168,7 +168,7 @@ const addColumn = async (ev) => {
const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn,
items: columnTypes.map(column => ({
- value: column, text: i18n.ts._deck._columns[column],
+ value: column, label: i18n.ts._deck._columns[column],
})),
});
if (canceled || column == null) return;
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index 0042882728..0423a22ce1 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -51,22 +51,24 @@ watch(soundSetting, v => {
async function setAntenna() {
const antennas = await misskeyApi('antennas/list');
- const { canceled, result: antenna } = await os.select<MisskeyEntities.Antenna | '_CREATE_'>({
+ const { canceled, result: antennaIdOrOperation } = await os.select({
title: i18n.ts.selectAntenna,
items: [
- { value: '_CREATE_', text: i18n.ts.createNew },
+ { value: '_CREATE_', label: i18n.ts.createNew },
(antennas.length > 0 ? {
- sectionTitle: i18n.ts.createdAntennas,
+ type: 'group' as const,
+ label: i18n.ts.createdAntennas,
items: antennas.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
} : undefined),
],
default: props.column.antennaId,
});
- if (canceled || antenna == null) return;
- if (antenna === '_CREATE_') {
+ if (canceled || antennaIdOrOperation == null) return;
+
+ if (antennaIdOrOperation === '_CREATE_') {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkAntennaEditorDialog.vue').then(x => x.default), {}, {
created: (newAntenna: MisskeyEntities.Antenna) => {
antennasCache.delete();
@@ -82,6 +84,8 @@ async function setAntenna() {
return;
}
+ const antenna = antennas.find(x => x.id === antennaIdOrOperation)!;
+
updateColumn(props.column.id, {
antennaId: antenna.id,
timelineNameCache: antenna.name,
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index c02499e2d7..35ca9f5cc6 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -58,14 +58,15 @@ watch(soundSetting, v => {
async function setChannel() {
const channels = await favoritedChannelsCache.fetch();
- const { canceled, result: chosenChannel } = await os.select({
+ const { canceled, result: chosenChannelId } = await os.select({
title: i18n.ts.selectChannel,
items: channels.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
default: props.column.channelId,
});
- if (canceled || chosenChannel == null) return;
+ if (canceled || chosenChannelId == null) return;
+ const chosenChannel = channels.find(x => x.id === chosenChannelId)!;
updateColumn(props.column.id, {
channelId: chosenChannel.id,
timelineNameCache: chosenChannel.name,
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 5c5891ece8..7fb0aba1e1 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -58,22 +58,23 @@ watch(soundSetting, v => {
async function setList() {
const lists = await misskeyApi('users/lists/list');
- const { canceled, result: list } = await os.select<MisskeyEntities.UserList | '_CREATE_'>({
+ const { canceled, result: listIdOrOperation } = await os.select({
title: i18n.ts.selectList,
items: [
- { value: '_CREATE_', text: i18n.ts.createNew },
+ { value: '_CREATE_', label: i18n.ts.createNew },
(lists.length > 0 ? {
- sectionTitle: i18n.ts.createdLists,
+ type: 'group' as const,
+ label: i18n.ts.createdLists,
items: lists.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
} : undefined),
],
default: props.column.listId,
});
- if (canceled || list == null) return;
+ if (canceled || listIdOrOperation == null) return;
- if (list === '_CREATE_') {
+ if (listIdOrOperation === '_CREATE_') {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName,
});
@@ -87,6 +88,8 @@ async function setList() {
timelineNameCache: res.name,
});
} else {
+ const list = lists.find(x => x.id === listIdOrOperation)!;
+
updateColumn(props.column.id, {
listId: list.id,
timelineNameCache: list.name,
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 0aafeb56d7..beb679169c 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -49,14 +49,15 @@ watch(soundSetting, v => {
async function setRole() {
const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable);
- const { canceled, result: role } = await os.select({
+ const { canceled, result: roleId } = await os.select({
title: i18n.ts.role,
items: roles.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
default: props.column.roleId,
});
- if (canceled || role == null) return;
+ if (canceled || roleId == null) return;
+ const role = roles.find(x => x.id === roleId)!;
updateColumn(props.column.id, {
roleId: role.id,
timelineNameCache: role.name,
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 37814f0914..afaa08e6d0 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -96,13 +96,13 @@ async function setType() {
const { canceled, result: src } = await os.select({
title: i18n.ts.timeline,
items: [{
- value: 'home' as const, text: i18n.ts._timelines.home,
+ value: 'home', label: i18n.ts._timelines.home,
}, {
- value: 'local' as const, text: i18n.ts._timelines.local,
+ value: 'local', label: i18n.ts._timelines.local,
}, {
- value: 'social' as const, text: i18n.ts._timelines.social,
+ value: 'social', label: i18n.ts._timelines.social,
}, {
- value: 'global' as const, text: i18n.ts._timelines.global,
+ value: 'global', label: i18n.ts._timelines.global,
}],
});
if (canceled) {
diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts
index b0b09f0f6b..cb4a227f67 100644
--- a/packages/frontend/src/utility/form.ts
+++ b/packages/frontend/src/utility/form.ts
@@ -4,10 +4,11 @@
*/
import * as Misskey from 'misskey-js';
+import type { OptionValue } from '@/components/MkSelect.vue';
export type EnumItem = string | {
label: string;
- value: unknown;
+ value: OptionValue;
};
type Hidden = boolean | ((v: any) => boolean);
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
index d6ddebc868..89dcae28e1 100644
--- a/packages/frontend/src/utility/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -37,15 +37,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const { canceled, result: period } = await os.select({
title: i18n.ts.mutePeriod,
items: [{
- value: 'indefinitely', text: i18n.ts.indefinitely,
+ value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
- value: 'tenMinutes', text: i18n.ts.tenMinutes,
+ value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
- 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,
}],
default: 'indefinitely',
});
@@ -312,15 +312,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const { canceled, result: period } = await os.select({
title: i18n.ts.period + ': ' + r.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/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index d87ea5ade2..9e914fa648 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -67,15 +67,15 @@ const fetching = ref(true);
async function chooseList() {
const lists = await misskeyApi('users/lists/list');
- const { canceled, result: list } = await os.select({
+ const { canceled, result: listId } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
- value: x, text: x.name,
+ value: x.id, label: x.name,
})),
default: widgetProps.listId,
});
- if (canceled || list == null) return;
-
+ if (canceled || listId == null) return;
+ const list = lists.find(x => x.id === listId)!;
widgetProps.listId = list.id;
save();
fetch();