1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-14 19:15:52 +02:00

refactor(frontend): MkRadiosの指定をpropsから行うように (#16597)

* refactor(frontend): MkRadiosの指定をpropsから行うように

* spdx

* fix lint

* fix: mkradiosを動的slotsに対応させる

* fix: remove comment [ci skip]

* fix lint

* fix lint

* migrate

* rename

* fix

* fix

* fix types

* remove unused imports

* fix

* wip

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり
2026-01-14 14:02:50 +09:00
committed by GitHub
parent 153ebd4392
commit b941c896aa
34 changed files with 505 additions and 284 deletions

View File

@@ -52,7 +52,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 type { MkSelectItem } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';

View File

@@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(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.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
</MkRadios>
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@@ -60,6 +59,7 @@ import MkButton from '@/components/MkButton.vue';
import MkRadios from '@/components/MkRadios.vue';
import { i18n } from '@/i18n.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
const props = defineProps<{
@@ -113,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
});
}
function getRadioKey(e: RadioFormItem['options'][number]) {
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
return def.options.map<MkRadiosOption>((v) => {
if (typeof v === 'string') {
return { value: v, label: v };
} else {
return { value: v.value, label: v.label };
}
});
}
</script>

View File

@@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]">
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
<option v-for="item in v.enum" :value="item.value">
<i v-if="item.icon" :class="item.icon"></i>
<template v-else>{{ item.label }}</template>
</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">

View File

@@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
type: 'radioOption',
text: key,
action: () => {
item.ref = value;
if ('value' in item.ref) {
item.ref.value = value;
} else {
// @ts-expect-error リアクティビティは保たれる
item.ref = value;
}
},
active: computed(() => item.ref === value),
active: computed(() => {
if ('value' in item.ref) {
return item.ref.value === value;
} else {
return item.ref === value;
}
}),
};
});

View File

@@ -24,8 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup generic="T extends unknown">
<script lang="ts" setup generic="T extends OptionValue | null">
import { computed } from 'vue';
import type { OptionValue } from '@/types/option-value.js';
const props = defineProps<{
modelValue: T;
@@ -52,7 +53,7 @@ function toggle(): void {
align-items: center;
text-align: left;
cursor: pointer;
padding: 7px 10px;
padding: 8px 10px;
min-width: 60px;
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
@@ -130,7 +131,6 @@ function toggle(): void {
.label {
margin-left: 8px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style>

View File

@@ -3,99 +3,128 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="{ [$style.vertical]: vertical }">
<div :class="$style.label">
<slot name="label"></slot>
</div>
<div :class="$style.body">
<MkRadio
v-for="option in options"
:key="getKey(option.value)"
v-model="model"
:disabled="option.disabled"
:value="option.value"
>
<div :class="[$style.optionContent, { [$style.checked]: model === option.value }]">
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
<div>
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
<template v-else>
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
</template>
</div>
</div>
</MkRadio>
</div>
<div :class="$style.caption">
<slot name="caption"></slot>
</div>
</div>
</template>
<script lang="ts">
import { Comment, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
import type { StyleValue } from 'vue';
import type { OptionValue } from '@/types/option-value.js';
export default defineComponent({
props: {
modelValue: {
required: false,
},
vertical: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const value = ref(props.modelValue);
watch(value, () => {
context.emit('update:modelValue', value.value);
});
watch(() => props.modelValue, v => {
value.value = v;
});
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
const caption = context.slots.caption && context.slots.caption();
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
options = options.filter(vnode => vnode.type !== Comment);
return () => h('div', {
class: [
'novjtcto',
...(props.vertical ? ['vertical'] : []),
],
}, [
...(label ? [h('div', {
class: 'label',
}, label)] : []),
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
key: option.key as string,
value: option.props?.value,
disabled: option.props?.disabled,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {
class: 'caption',
}, caption)] : []),
]);
},
});
export type MkRadiosOption<T = OptionValue, S = string> = {
value: T;
slotId?: S;
label?: string;
labelStyle?: StyleValue;
icon?: string;
iconStyle?: StyleValue;
caption?: string;
disabled?: boolean;
};
</script>
<style lang="scss">
.novjtcto {
> .label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
<script setup lang="ts" generic="const T extends MkRadiosOption">
import MkRadio from './MkRadio.vue';
&:empty {
display: none;
}
}
defineProps<{
options: T[];
vertical?: boolean;
}>();
> .body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
defineSlots<{
label?: () => any;
caption?: () => any;
} & {
[K in `option-${SlotNames}`]: () => any;
}>();
&:empty {
display: none;
}
}
const model = defineModel<T['value']>({ required: true });
&.vertical {
> .body {
flex-direction: column;
}
function getKey(value: OptionValue): PropertyKey {
if (value === null) return 'null';
return value;
}
</script>
<style lang="scss" module>
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
&:empty {
display: none;
}
}
.body {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
}
}
.optionContent {
display: flex;
align-items: center;
gap: 6px;
}
.optionCaption {
font-size: 0.85em;
padding: 2px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
}
.optionContent.checked {
.optionCaption {
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
}
}
.optionIcon {
flex-shrink: 0;
}
.vertical > .body {
flex-direction: column;
}
</style>

View File

@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
export type OptionValue = string | number | null;
import type { OptionValue } from '@/types/option-value.js';
export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option';

View File

@@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-settings-question"></i></template>
<div class="_gaps_s">
<MkRadios v-model="q_use" :vertical="true">
<option value="single">
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div>
</option>
<option value="group">
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div>
</option>
<option value="open">
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
</option>
<MkRadios
v-model="q_use"
:options="[
{ value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description },
{ value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description },
{ value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
@@ -40,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-users"></i></template>
<div class="_gaps_s">
<MkRadios v-model="q_scale" :vertical="true">
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option>
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option>
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option>
<MkRadios
v-model="q_scale"
:options="[
{ value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' },
{ value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' },
{ value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
@@ -57,9 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
<MkRadios v-model="q_federation" :vertical="true">
<option value="yes">{{ i18n.ts.yes }}</option>
<option value="no">{{ i18n.ts.no }}</option>
<MkRadios
v-model="q_federation"
:options="[
{ value: 'yes', label: i18n.ts.yes },
{ value: 'no', label: i18n.ts.no },
]"
vertical
>
</MkRadios>
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
@@ -212,9 +218,9 @@ const props = withDefaults(defineProps<{
});
const q_name = ref('');
const q_use = ref('single');
const q_scale = ref('small');
const q_federation = ref('yes');
const q_use = ref<'single' | 'group' | 'open'>('single');
const q_scale = ref<'small' | 'medium' | 'large'>('small');
const q_federation = ref<'yes' | 'no'>('no');
const q_remoteContentsCleaning = ref(true);
const q_adminName = ref('');
const q_adminEmail = ref('');

View File

@@ -22,18 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="text">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkRadios v-model="icon">
<MkRadios
v-model="icon"
:options="[
{ value: 'info', icon: 'ti ti-info-circle' },
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
]"
>
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
<MkRadios v-model="display">
<MkRadios
v-model="display"
:options="[
{ value: 'normal', label: i18n.ts.normal },
{ value: 'banner', label: i18n.ts.banner },
{ value: 'dialog', label: i18n.ts.dialog },
]"
>
<template #label>{{ i18n.ts.display }}</template>
<option value="normal">{{ i18n.ts.normal }}</option>
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkSwitch v-model="needConfirmationToRead">
{{ i18n.ts._announcement.needConfirmationToRead }}