summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkSelect.vue
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-09-13 21:00:33 +0900
committerGitHub <noreply@github.com>2025-09-13 21:00:33 +0900
commitd4654dd7bd5bf1c7faa74ed89f592448c0076be8 (patch)
treeb4f51e86f174717fef469fbedca48faa2a55e841 /packages/frontend/src/components/MkSelect.vue
parentfix(deps): update dependency vite [security] (#16535) (diff)
downloadmisskey-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.vue174
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, {