Skip to content

Commit b941c89

Browse files
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 <[email protected]>
1 parent 153ebd4 commit b941c89

34 files changed

+503
-282
lines changed

packages/backend/src/core/entities/ReversiGameEntityService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
1414
import { IdService } from '@/core/IdService.js';
1515
import { UserEntityService } from './UserEntityService.js';
1616

17+
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
18+
return ['random', '1', '2'].includes(bw);
19+
}
20+
1721
@Injectable()
1822
export class ReversiGameEntityService {
1923
constructor(
@@ -58,7 +62,7 @@ export class ReversiGameEntityService {
5862
surrenderedUserId: game.surrenderedUserId,
5963
timeoutUserId: game.timeoutUserId,
6064
black: game.black,
61-
bw: game.bw,
65+
bw: assertBw(game.bw) ? game.bw : 'random',
6266
isLlotheo: game.isLlotheo,
6367
canPutEverywhere: game.canPutEverywhere,
6468
loopedBoard: game.loopedBoard,
@@ -116,7 +120,7 @@ export class ReversiGameEntityService {
116120
surrenderedUserId: game.surrenderedUserId,
117121
timeoutUserId: game.timeoutUserId,
118122
black: game.black,
119-
bw: game.bw,
123+
bw: assertBw(game.bw) ? game.bw : 'random',
120124
isLlotheo: game.isLlotheo,
121125
canPutEverywhere: game.canPutEverywhere,
122126
loopedBoard: game.loopedBoard,

packages/backend/src/models/json-schema/reversi-game.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
8181
bw: {
8282
type: 'string',
8383
optional: false, nullable: false,
84+
enum: ['random', '1', '2'],
8485
},
8586
noIrregularRules: {
8687
type: 'boolean',
@@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
199200
bw: {
200201
type: 'string',
201202
optional: false, nullable: false,
203+
enum: ['random', '1', '2'],
202204
},
203205
noIrregularRules: {
204206
type: 'boolean',

packages/frontend/src/components/MkDialog.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import MkModal from '@/components/MkModal.vue';
5252
import MkButton from '@/components/MkButton.vue';
5353
import MkInput from '@/components/MkInput.vue';
5454
import MkSelect from '@/components/MkSelect.vue';
55-
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
55+
import type { MkSelectItem } from '@/components/MkSelect.vue';
56+
import type { OptionValue } from '@/types/option-value.js';
5657
import { useMkSelect } from '@/composables/use-mkselect.js';
5758
import { i18n } from '@/i18n.js';
5859

packages/frontend/src/components/MkForm.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
2626
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
2727
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
2828
</MkSelect>
29-
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
29+
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)">
3030
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
31-
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
3231
</MkRadios>
3332
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
3433
<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';
6059
import MkRadios from '@/components/MkRadios.vue';
6160
import { i18n } from '@/i18n.js';
6261
import type { MkSelectItem } from '@/components/MkSelect.vue';
62+
import type { MkRadiosOption } from '@/components/MkRadios.vue';
6363
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
6464
6565
const props = defineProps<{
@@ -113,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
113113
});
114114
}
115115
116-
function getRadioKey(e: RadioFormItem['options'][number]) {
117-
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
116+
function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
117+
return def.options.map<MkRadiosOption>((v) => {
118+
if (typeof v === 'string') {
119+
return { value: v, label: v };
120+
} else {
121+
return { value: v.value, label: v.label };
122+
}
123+
});
118124
}
119125
</script>

packages/frontend/src/components/MkImageEffectorFxForm.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
2828
<template #label>{{ v.label ?? k }}</template>
2929
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
3030
</MkRange>
31-
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]">
31+
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
3232
<template #label>{{ v.label ?? k }}</template>
3333
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
34-
<option v-for="item in v.enum" :value="item.value">
35-
<i v-if="item.icon" :class="item.icon"></i>
36-
<template v-else>{{ item.label }}</template>
37-
</option>
3834
</MkRadios>
3935
<div v-else-if="v.type === 'seed'">
4036
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">

packages/frontend/src/components/MkMenu.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
323323
type: 'radioOption',
324324
text: key,
325325
action: () => {
326-
item.ref = value;
326+
if ('value' in item.ref) {
327+
item.ref.value = value;
328+
} else {
329+
// @ts-expect-error リアクティビティは保たれる
330+
item.ref = value;
331+
}
327332
},
328-
active: computed(() => item.ref === value),
333+
active: computed(() => {
334+
if ('value' in item.ref) {
335+
return item.ref.value === value;
336+
} else {
337+
return item.ref === value;
338+
}
339+
}),
329340
};
330341
});
331342

packages/frontend/src/components/MkRadio.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only
2424
</div>
2525
</template>
2626

27-
<script lang="ts" setup generic="T extends unknown">
27+
<script lang="ts" setup generic="T extends OptionValue | null">
2828
import { computed } from 'vue';
29+
import type { OptionValue } from '@/types/option-value.js';
2930
3031
const props = defineProps<{
3132
modelValue: T;
@@ -52,7 +53,7 @@ function toggle(): void {
5253
align-items: center;
5354
text-align: left;
5455
cursor: pointer;
55-
padding: 7px 10px;
56+
padding: 8px 10px;
5657
min-width: 60px;
5758
background-color: var(--MI_THEME-panel);
5859
background-clip: padding-box !important;
@@ -130,7 +131,6 @@ function toggle(): void {
130131
.label {
131132
margin-left: 8px;
132133
display: block;
133-
line-height: 20px;
134134
cursor: pointer;
135135
}
136136
</style>

packages/frontend/src/components/MkRadios.vue

Lines changed: 111 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,99 +3,128 @@ SPDX-FileCopyrightText: syuilo and misskey-project
33
SPDX-License-Identifier: AGPL-3.0-only
44
-->
55

6+
<template>
7+
<div :class="{ [$style.vertical]: vertical }">
8+
<div :class="$style.label">
9+
<slot name="label"></slot>
10+
</div>
11+
<div :class="$style.body">
12+
<MkRadio
13+
v-for="option in options"
14+
:key="getKey(option.value)"
15+
v-model="model"
16+
:disabled="option.disabled"
17+
:value="option.value"
18+
>
19+
<div :class="[$style.optionContent, { [$style.checked]: model === option.value }]">
20+
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
21+
<div>
22+
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
23+
<template v-else>
24+
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
25+
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
26+
</template>
27+
</div>
28+
</div>
29+
</MkRadio>
30+
</div>
31+
<div :class="$style.caption">
32+
<slot name="caption"></slot>
33+
</div>
34+
</div>
35+
</template>
36+
637
<script lang="ts">
7-
import { Comment, defineComponent, h, ref, watch } from 'vue';
38+
import type { StyleValue } from 'vue';
39+
import type { OptionValue } from '@/types/option-value.js';
40+
41+
export type MkRadiosOption<T = OptionValue, S = string> = {
42+
value: T;
43+
slotId?: S;
44+
label?: string;
45+
labelStyle?: StyleValue;
46+
icon?: string;
47+
iconStyle?: StyleValue;
48+
caption?: string;
49+
disabled?: boolean;
50+
};
51+
</script>
52+
53+
<script setup lang="ts" generic="const T extends MkRadiosOption">
854
import MkRadio from './MkRadio.vue';
9-
import type { VNode } from 'vue';
10-
11-
export default defineComponent({
12-
props: {
13-
modelValue: {
14-
required: false,
15-
},
16-
vertical: {
17-
type: Boolean,
18-
default: false,
19-
},
20-
},
21-
setup(props, context) {
22-
const value = ref(props.modelValue);
23-
watch(value, () => {
24-
context.emit('update:modelValue', value.value);
25-
});
26-
watch(() => props.modelValue, v => {
27-
value.value = v;
28-
});
29-
if (!context.slots.default) return null;
30-
let options = context.slots.default();
31-
const label = context.slots.label && context.slots.label();
32-
const caption = context.slots.caption && context.slots.caption();
33-
34-
// なぜかFragmentになることがあるため
35-
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
36-
37-
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
38-
options = options.filter(vnode => vnode.type !== Comment);
39-
40-
return () => h('div', {
41-
class: [
42-
'novjtcto',
43-
...(props.vertical ? ['vertical'] : []),
44-
],
45-
}, [
46-
...(label ? [h('div', {
47-
class: 'label',
48-
}, label)] : []),
49-
h('div', {
50-
class: 'body',
51-
}, options.map(option => h(MkRadio, {
52-
key: option.key as string,
53-
value: option.props?.value,
54-
disabled: option.props?.disabled,
55-
modelValue: value.value,
56-
'onUpdate:modelValue': _v => value.value = _v,
57-
}, () => option.children)),
58-
),
59-
...(caption ? [h('div', {
60-
class: 'caption',
61-
}, caption)] : []),
62-
]);
63-
},
64-
});
55+
56+
defineProps<{
57+
options: T[];
58+
vertical?: boolean;
59+
}>();
60+
61+
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
62+
63+
defineSlots<{
64+
label?: () => any;
65+
caption?: () => any;
66+
} & {
67+
[K in `option-${SlotNames}`]: () => any;
68+
}>();
69+
70+
const model = defineModel<T['value']>({ required: true });
71+
72+
function getKey(value: OptionValue): PropertyKey {
73+
if (value === null) return 'null';
74+
return value;
75+
}
6576
</script>
6677

67-
<style lang="scss">
68-
.novjtcto {
69-
> .label {
70-
font-size: 0.85em;
71-
padding: 0 0 8px 0;
72-
user-select: none;
78+
<style lang="scss" module>
79+
.label {
80+
font-size: 0.85em;
81+
padding: 0 0 8px 0;
82+
user-select: none;
7383
74-
&:empty {
75-
display: none;
76-
}
84+
&:empty {
85+
display: none;
7786
}
87+
}
7888
79-
> .body {
80-
display: flex;
81-
gap: 10px;
82-
flex-wrap: wrap;
83-
}
89+
.body {
90+
display: flex;
91+
gap: 10px;
92+
flex-wrap: wrap;
93+
}
8494
85-
> .caption {
86-
font-size: 0.85em;
87-
padding: 8px 0 0 0;
88-
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
95+
.caption {
96+
font-size: 0.85em;
97+
padding: 8px 0 0 0;
98+
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
8999
90-
&:empty {
91-
display: none;
92-
}
100+
&:empty {
101+
display: none;
93102
}
103+
}
94104
95-
&.vertical {
96-
> .body {
97-
flex-direction: column;
98-
}
105+
.optionContent {
106+
display: flex;
107+
align-items: center;
108+
gap: 6px;
109+
}
110+
111+
.optionCaption {
112+
font-size: 0.85em;
113+
padding: 2px 0 0 0;
114+
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
115+
}
116+
117+
.optionContent.checked {
118+
.optionCaption {
119+
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
99120
}
100121
}
122+
123+
.optionIcon {
124+
flex-shrink: 0;
125+
}
126+
127+
.vertical > .body {
128+
flex-direction: column;
129+
}
101130
</style>

packages/frontend/src/components/MkSelect.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
4040
</template>
4141

4242
<script lang="ts">
43-
export type OptionValue = string | number | null;
43+
import type { OptionValue } from '@/types/option-value.js';
4444
4545
export type ItemOption<T extends OptionValue = OptionValue> = {
4646
type?: 'option';

0 commit comments

Comments
 (0)