From ec8381522789a7fc4329a627770d80a0cd7774a4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 27 Feb 2025 09:32:39 +0900
Subject: fix(frontend): MkSelectの初期値が表示されない場合がある (#15559)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix

* Update CHANGELOG.md
---
 packages/frontend/src/components/MkSelect.vue | 130 ++++++++++++++++++++------
 1 file changed, 100 insertions(+), 30 deletions(-)

(limited to 'packages/frontend/src/components/MkSelect.vue')

diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 83a79d0c9f..15717802ec 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -41,11 +41,28 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
-import type { VNode, VNodeChild } 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';
 
+type ItemOption = {
+	type?: 'option';
+	value: string | number | null;
+	label: string;
+};
+
+type ItemGroup = {
+	type: 'group';
+	label: string;
+	items: ItemOption[];
+};
+
+export type MkSelectItem = ItemOption | ItemGroup;
+
+// TODO: itemsをslot内のoptionで指定する用法は廃止する(props.itemsを必須化する)
+// see: https://github.com/misskey-dev/misskey/issues/15558
+
 const props = defineProps<{
 	modelValue: string | number | null;
 	required?: boolean;
@@ -56,6 +73,7 @@ const props = defineProps<{
 	inline?: boolean;
 	small?: boolean;
 	large?: boolean;
+	items?: MkSelectItem[];
 }>();
 
 const emit = defineEmits<{
@@ -107,7 +125,30 @@ onMounted(() => {
 	});
 });
 
-watch(modelValue, () => {
+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;
+					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;
@@ -130,7 +171,7 @@ watch(modelValue, () => {
 	};
 
 	scanOptions(slots.default!());
-}, { immediate: true });
+}, { immediate: true, deep: true });
 
 function show() {
 	if (opening.value) return;
@@ -139,41 +180,70 @@ function show() {
 	opening.value = true;
 
 	const menu: MenuItem[] = [];
-	let options = slots.default!();
-
-	const pushOption = (option: VNode) => {
-		menu.push({
-			text: option.children as string,
-			active: computed(() => modelValue.value === option.props?.value),
-			action: () => {
-				emit('update:modelValue', option.props?.value);
-			},
-		});
-	};
 
-	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 (props.items) {
+		for (const item of props.items) {
+			if (item.type === 'group') {
 				menu.push({
 					type: 'label',
-					text: optgroup.props?.label,
+					text: item.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?
+				for (const option of item.items) {
+					menu.push({
+						text: option.label,
+						active: computed(() => modelValue.value === option.value),
+						action: () => {
+							emit('update:modelValue', option.value);
+						},
+					});
+				}
 			} else {
-				const option = vnode;
-				pushOption(option);
+				menu.push({
+					text: item.label,
+					active: computed(() => modelValue.value === item.value),
+					action: () => {
+						emit('update:modelValue', item.value);
+					},
+				});
 			}
 		}
-	};
+	} else {
+		let options = slots.default!();
+
+		const pushOption = (option: VNode) => {
+			menu.push({
+				text: option.children as string,
+				active: computed(() => modelValue.value === option.props?.value),
+				action: () => {
+					emit('update:modelValue', option.props?.value);
+				},
+			});
+		};
+
+		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);
+		scanOptions(options);
+	}
 
 	os.popupMenu(menu, container.value, {
 		width: container.value?.offsetWidth,
-- 
cgit v1.2.3-freya

