diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2026-01-13 15:02:50 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-13 15:02:50 +0900 |
| commit | f3aa5081ed994af857a97798528e1788d7762d36 (patch) | |
| tree | 954f29b7743842dc62c76466c200c0dfce885d89 | |
| parent | fix(frontend): add "px" suffix to borderWidth of Ui:C:container (#17088) (diff) | |
| download | misskey-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.vue | 41 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkFormDialog.vue | 12 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkInput.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkTextarea.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkWidgetSettingsDialog.vue | 12 |
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(); } |