mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-04 18:55:56 +02:00
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>
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.value === model.value) {
|
||||
found = item;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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, {
|
||||
|
||||
@@ -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()];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user