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:
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user