diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-09-13 21:00:33 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-13 21:00:33 +0900 |
| commit | d4654dd7bd5bf1c7faa74ed89f592448c0076be8 (patch) | |
| tree | b4f51e86f174717fef469fbedca48faa2a55e841 /packages/frontend/src/components/MkSelect.vue | |
| parent | fix(deps): update dependency vite [security] (#16535) (diff) | |
| download | misskey-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/components/MkSelect.vue')
| -rw-r--r-- | packages/frontend/src/components/MkSelect.vue | 174 |
1 files changed, 54 insertions, 120 deletions
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, { |