summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2026-01-13 15:02:50 +0900
committerGitHub <noreply@github.com>2026-01-13 15:02:50 +0900
commitf3aa5081ed994af857a97798528e1788d7762d36 (patch)
tree954f29b7743842dc62c76466c200c0dfce885d89
parentfix(frontend): add "px" suffix to borderWidth of Ui:C:container (#17088) (diff)
downloadmisskey-f3aa5081ed994af857a97798528e1788d7762d36.tar.gz
misskey-f3aa5081ed994af857a97798528e1788d7762d36.tar.bz2
misskey-f3aa5081ed994af857a97798528e1788d7762d36.zip
fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように (#17096)
* fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように * fix lint
-rw-r--r--packages/frontend/src/components/MkForm.vue41
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue12
-rw-r--r--packages/frontend/src/components/MkInput.vue5
-rw-r--r--packages/frontend/src/components/MkTextarea.vue5
-rw-r--r--packages/frontend/src/components/MkWidgetSettingsDialog.vue12
5 files changed, 68 insertions, 7 deletions
diff --git a/packages/frontend/src/components/MkForm.vue b/packages/frontend/src/components/MkForm.vue
index 711aa611c3..3d4724e6b7 100644
--- a/packages/frontend/src/components/MkForm.vue
+++ b/packages/frontend/src/components/MkForm.vue
@@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
<template v-for="v, k in form">
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
- <MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave">
+ <MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput>
- <MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave">
+ <MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput>
- <MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave">
+ <MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkTextarea>
@@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
import XFile from '@/components/MkForm.file.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -65,9 +66,43 @@ const props = defineProps<{
form: Form;
}>();
+const emit = defineEmits<{
+ (ev: 'canSaveStateChange', canSave: boolean): void;
+}>();
+
// TODO: ジェネリックにしたい
const values = defineModel<Record<string, any>>({ required: true });
+// 保存可能状態の管理
+const inputSavingStates = ref<Record<string, { changed: boolean; invalid: boolean }>>({});
+
+function onSavingStateChange(key: string, changed: boolean, invalid: boolean) {
+ inputSavingStates.value[key] = { changed, invalid };
+}
+
+const canSave = computed(() => {
+ for (const key in inputSavingStates.value) {
+ const state = inputSavingStates.value[key];
+ if (
+ ('manualSave' in props.form[key] && props.form[key].manualSave && state.changed) ||
+ state.invalid
+ ) {
+ return false;
+ }
+ if ('required' in props.form[key] && props.form[key].required) {
+ const val = values.value[key];
+ if (val === null || val === undefined || val === '') {
+ return false;
+ }
+ }
+ }
+ return true;
+});
+
+watch(canSave, (newCanSave) => {
+ emit('canSaveStateChange', newCanSave);
+}, { immediate: true });
+
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
return def.enum.map((v) => {
if (typeof v === 'string') {
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index c314de7c89..091721b40b 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:width="450"
:canClose="false"
:withOkButton="true"
- :okButtonDisabled="false"
+ :okButtonDisabled="!canSave"
@click="cancel()"
@ok="ok()"
@close="cancel()"
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;">
- <MkForm v-model="values" :form="form"/>
+ <MkForm v-model="values" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
</div>
</MkModalWindow>
</template>
@@ -59,7 +59,15 @@ const values = ref((() => {
return obj;
})());
+const canSave = ref(true);
+
+function onCanSaveStateChanged(newCanSave: boolean) {
+ canSave.value = newCanSave;
+}
+
function ok() {
+ if (!canSave.value) return;
+
emit('done', {
result: values.value,
});
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 4f6ca083a3..8a49fd231d 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -92,6 +92,7 @@ const emit = defineEmits<{
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: ModelValueType<T>): void;
+ (ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
}>();
const { modelValue } = toRefs(props);
@@ -152,6 +153,10 @@ watch(v, () => {
invalid.value = inputEl.value?.validity.badInput ?? true;
});
+watch([changed, invalid], ([newChanged, newInvalid]) => {
+ emit('savingStateChange', newChanged, newInvalid);
+}, { immediate: true });
+
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
useInterval(() => {
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index d53d4ec018..fe4f7b7aaf 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -67,6 +67,7 @@ const emit = defineEmits<{
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter'): void;
(ev: 'update:modelValue', value: string): void;
+ (ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
}>();
const { modelValue, autofocus } = toRefs(props);
@@ -131,6 +132,10 @@ watch(v, () => {
invalid.value = inputEl.value?.validity.badInput ?? true;
});
+watch([changed, invalid], ([newChanged, newInvalid]) => {
+ emit('savingStateChange', newChanged, newInvalid);
+}, { immediate: true });
+
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
diff --git a/packages/frontend/src/components/MkWidgetSettingsDialog.vue b/packages/frontend/src/components/MkWidgetSettingsDialog.vue
index 63f294770c..41fec2a9e0 100644
--- a/packages/frontend/src/components/MkWidgetSettingsDialog.vue
+++ b/packages/frontend/src/components/MkWidgetSettingsDialog.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:height="600"
:scroll="false"
:withOkButton="true"
+ :okButtonDisabled="!canSave"
@close="cancel()"
@ok="save()"
@closed="emit('closed')"
@@ -38,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #controls>
<div class="_spacer">
- <MkForm v-model="settings" :form="form"/>
+ <MkForm v-model="settings" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
</div>
</template>
</MkPreviewWithControls>
@@ -46,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
-import { reactive, useTemplateRef, ref, computed, watch, onBeforeUnmount, onMounted } from 'vue';
+import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
import MkPreviewWithControls from './MkPreviewWithControls.vue';
import type { Form } from '@/utility/form.js';
import { deepClone } from '@/utility/clone.js';
@@ -70,7 +71,14 @@ const dialog = useTemplateRef('dialog');
const settings = ref<Record<string, any>>(deepClone(props.currentSettings));
+const canSave = ref(true);
+
+function onCanSaveStateChanged(newCanSave: boolean) {
+ canSave.value = newCanSave;
+}
+
function save() {
+ if (!canSave.value) return;
emit('saved', deepClone(settings.value));
dialog.value?.close();
}