Skip to content

Commit f3aa508

Browse files
fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように (#17096)
* fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように * fix lint
1 parent c0d5c0d commit f3aa508

File tree

5 files changed

+68
-7
lines changed

5 files changed

+68
-7
lines changed

packages/frontend/src/components/MkForm.vue

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
77
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
88
<template v-for="v, k in form">
99
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
10-
<MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave">
10+
<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)">
1111
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
1212
<template v-if="v.description" #caption>{{ v.description }}</template>
1313
</MkInput>
14-
<MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave">
14+
<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)">
1515
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
1616
<template v-if="v.description" #caption>{{ v.description }}</template>
1717
</MkInput>
18-
<MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave">
18+
<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)">
1919
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
2020
<template v-if="v.description" #caption>{{ v.description }}</template>
2121
</MkTextarea>
@@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
4949
</template>
5050

5151
<script lang="ts" setup>
52+
import { computed, ref, watch } from 'vue';
5253
import XFile from '@/components/MkForm.file.vue';
5354
import MkInput from '@/components/MkInput.vue';
5455
import MkTextarea from '@/components/MkTextarea.vue';
@@ -65,9 +66,43 @@ const props = defineProps<{
6566
form: Form;
6667
}>();
6768
69+
const emit = defineEmits<{
70+
(ev: 'canSaveStateChange', canSave: boolean): void;
71+
}>();
72+
6873
// TODO: ジェネリックにしたい
6974
const values = defineModel<Record<string, any>>({ required: true });
7075
76+
// 保存可能状態の管理
77+
const inputSavingStates = ref<Record<string, { changed: boolean; invalid: boolean }>>({});
78+
79+
function onSavingStateChange(key: string, changed: boolean, invalid: boolean) {
80+
inputSavingStates.value[key] = { changed, invalid };
81+
}
82+
83+
const canSave = computed(() => {
84+
for (const key in inputSavingStates.value) {
85+
const state = inputSavingStates.value[key];
86+
if (
87+
('manualSave' in props.form[key] && props.form[key].manualSave && state.changed) ||
88+
state.invalid
89+
) {
90+
return false;
91+
}
92+
if ('required' in props.form[key] && props.form[key].required) {
93+
const val = values.value[key];
94+
if (val === null || val === undefined || val === '') {
95+
return false;
96+
}
97+
}
98+
}
99+
return true;
100+
});
101+
102+
watch(canSave, (newCanSave) => {
103+
emit('canSaveStateChange', newCanSave);
104+
}, { immediate: true });
105+
71106
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
72107
return def.enum.map((v) => {
73108
if (typeof v === 'string') {

packages/frontend/src/components/MkFormDialog.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
99
:width="450"
1010
:canClose="false"
1111
:withOkButton="true"
12-
:okButtonDisabled="false"
12+
:okButtonDisabled="!canSave"
1313
@click="cancel()"
1414
@ok="ok()"
1515
@close="cancel()"
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
2020
</template>
2121

2222
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;">
23-
<MkForm v-model="values" :form="form"/>
23+
<MkForm v-model="values" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
2424
</div>
2525
</MkModalWindow>
2626
</template>
@@ -59,7 +59,15 @@ const values = ref((() => {
5959
return obj;
6060
})());
6161
62+
const canSave = ref(true);
63+
64+
function onCanSaveStateChanged(newCanSave: boolean) {
65+
canSave.value = newCanSave;
66+
}
67+
6268
function ok() {
69+
if (!canSave.value) return;
70+
6371
emit('done', {
6472
result: values.value,
6573
});

packages/frontend/src/components/MkInput.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const emit = defineEmits<{
9292
(ev: 'keydown', _ev: KeyboardEvent): void;
9393
(ev: 'enter', _ev: KeyboardEvent): void;
9494
(ev: 'update:modelValue', value: ModelValueType<T>): void;
95+
(ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
9596
}>();
9697
9798
const { modelValue } = toRefs(props);
@@ -152,6 +153,10 @@ watch(v, () => {
152153
invalid.value = inputEl.value?.validity.badInput ?? true;
153154
});
154155
156+
watch([changed, invalid], ([newChanged, newInvalid]) => {
157+
emit('savingStateChange', newChanged, newInvalid);
158+
}, { immediate: true });
159+
155160
// このコンポーネントが作成された時、非表示状態である場合がある
156161
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
157162
useInterval(() => {

packages/frontend/src/components/MkTextarea.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const emit = defineEmits<{
6767
(ev: 'keydown', _ev: KeyboardEvent): void;
6868
(ev: 'enter'): void;
6969
(ev: 'update:modelValue', value: string): void;
70+
(ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
7071
}>();
7172
7273
const { modelValue, autofocus } = toRefs(props);
@@ -131,6 +132,10 @@ watch(v, () => {
131132
invalid.value = inputEl.value?.validity.badInput ?? true;
132133
});
133134
135+
watch([changed, invalid], ([newChanged, newInvalid]) => {
136+
emit('savingStateChange', newChanged, newInvalid);
137+
}, { immediate: true });
138+
134139
onMounted(() => {
135140
nextTick(() => {
136141
if (autofocus.value) {

packages/frontend/src/components/MkWidgetSettingsDialog.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
1010
:height="600"
1111
:scroll="false"
1212
:withOkButton="true"
13+
:okButtonDisabled="!canSave"
1314
@close="cancel()"
1415
@ok="save()"
1516
@closed="emit('closed')"
@@ -38,15 +39,15 @@ SPDX-License-Identifier: AGPL-3.0-only
3839

3940
<template #controls>
4041
<div class="_spacer">
41-
<MkForm v-model="settings" :form="form"/>
42+
<MkForm v-model="settings" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
4243
</div>
4344
</template>
4445
</MkPreviewWithControls>
4546
</MkModalWindow>
4647
</template>
4748

4849
<script setup lang="ts">
49-
import { reactive, useTemplateRef, ref, computed, watch, onBeforeUnmount, onMounted } from 'vue';
50+
import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
5051
import MkPreviewWithControls from './MkPreviewWithControls.vue';
5152
import type { Form } from '@/utility/form.js';
5253
import { deepClone } from '@/utility/clone.js';
@@ -70,7 +71,14 @@ const dialog = useTemplateRef('dialog');
7071
7172
const settings = ref<Record<string, any>>(deepClone(props.currentSettings));
7273
74+
const canSave = ref(true);
75+
76+
function onCanSaveStateChanged(newCanSave: boolean) {
77+
canSave.value = newCanSave;
78+
}
79+
7380
function save() {
81+
if (!canSave.value) return;
7482
emit('saved', deepClone(settings.value));
7583
dialog.value?.close();
7684
}

0 commit comments

Comments
 (0)