fix(frontend): follow-up of #17033 (#17047)

* wip

* fix

* ref -> reactive

* tweak throttle threshold

* tweak throttle threshold

* rss設定にはmanualSaveを使用するように

* Update MkWidgetSettingsDialog.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり 2025-12-30 14:32:40 +09:00 committed by GitHub
parent 14f58255ee
commit 4285303c81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 36 additions and 27 deletions

View File

@ -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"> <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-for="v, k in form">
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template> <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"> <MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <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> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput> </MkInput>
<MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm"> <MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <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> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput> </MkInput>
<MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm"> <MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <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> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkTextarea> </MkTextarea>

View File

@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<component <component
:is="`widget-${widgetName}`" :is="`widget-${widgetName}`"
:key="currentId"
:widget="{ name: widgetName, id: '__PREVIEW__', data: settings }" :widget="{ name: widgetName, id: '__PREVIEW__', data: settings }"
></component> ></component>
</div> </div>
@ -48,13 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { reactive, useTemplateRef, ref, computed, watch, onBeforeUnmount, onMounted } from 'vue'; import { reactive, useTemplateRef, ref, computed, watch, onBeforeUnmount, onMounted } from 'vue';
import MkPreviewWithControls from './MkPreviewWithControls.vue';
import type { Form } from '@/utility/form.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { genId } from '@/utility/id.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkPreviewWithControls from './MkPreviewWithControls.vue';
import MkForm from '@/components/MkForm.vue'; import MkForm from '@/components/MkForm.vue';
import type { Form } from '@/utility/form.js';
const props = defineProps<{ const props = defineProps<{
widgetName: string; widgetName: string;
@ -71,11 +69,6 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog'); const dialog = useTemplateRef('dialog');
const settings = reactive<Record<string, any>>(deepClone(props.currentSettings)); const settings = reactive<Record<string, any>>(deepClone(props.currentSettings));
const currentId = ref(genId());
watch(settings, () => {
currentId.value = genId();
});
function save() { function save() {
emit('saved', deepClone(settings)); emit('saved', deepClone(settings));

View File

@ -25,6 +25,7 @@ export interface StringFormItem extends FormItemBase {
required?: boolean; required?: boolean;
multiline?: boolean; multiline?: boolean;
treatAsMfm?: boolean; treatAsMfm?: boolean;
manualSave?: boolean;
} }
export interface NumberFormItem extends FormItemBase { export interface NumberFormItem extends FormItemBase {
@ -33,6 +34,7 @@ export interface NumberFormItem extends FormItemBase {
description?: string; description?: string;
required?: boolean; required?: boolean;
step?: number; step?: number;
manualSave?: boolean;
} }
export interface BooleanFormItem extends FormItemBase { export interface BooleanFormItem extends FormItemBase {
@ -145,3 +147,11 @@ type GetItemType<Item extends FormItem> =
export type GetFormResultType<F extends Form> = { export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>; [P in keyof F]: GetItemType<F[P]>;
}; };
export function getDefaultFormValues<F extends FormWithDefault>(form: F): GetFormResultType<F> {
const result = {} as GetFormResultType<F>;
for (const key of Object.keys(form) as (keyof F)[]) {
result[key] = form[key].default as GetItemType<F[typeof key]>;
}
return result;
}

View File

@ -28,7 +28,6 @@ import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue'; import MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js';
const name = 'rss'; const name = 'rss';
@ -36,6 +35,7 @@ const widgetPropsDef = {
url: { url: {
type: 'string', type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
manualSave: true,
}, },
refreshIntervalSec: { refreshIntervalSec: {
type: 'number', type: 'number',
@ -68,7 +68,7 @@ const fetching = ref(true);
const fetchEndpoint = computed(() => { const fetchEndpoint = computed(() => {
const url = new URL('/api/fetch-rss', base); const url = new URL('/api/fetch-rss', base);
url.searchParams.set('url', widgetProps.url); url.searchParams.set('url', widgetProps.url);
return url; return url.toString();
}); });
const intervalClear = ref<(() => void) | undefined>(); const intervalClear = ref<(() => void) | undefined>();
@ -83,7 +83,7 @@ const tick = () => {
}); });
}; };
watch(() => fetchEndpoint, tick); watch(fetchEndpoint, tick);
watch(() => widgetProps.refreshIntervalSec, () => { watch(() => widgetProps.refreshIntervalSec, () => {
if (intervalClear.value) { if (intervalClear.value) {
intervalClear.value(); intervalClear.value();

View File

@ -44,6 +44,7 @@ const widgetPropsDef = {
url: { url: {
type: 'string', type: 'string',
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
manualSave: true,
}, },
shuffle: { shuffle: {
type: 'boolean', type: 'boolean',
@ -119,7 +120,7 @@ const tick = () => {
}); });
}; };
watch(() => fetchEndpoint, tick); watch(fetchEndpoint, tick);
watch(() => widgetProps.refreshIntervalSec, () => { watch(() => widgetProps.refreshIntervalSec, () => {
if (intervalClear.value) { if (intervalClear.value) {
intervalClear.value(); intervalClear.value();

View File

@ -4,8 +4,9 @@
*/ */
import { defineAsyncComponent, reactive, watch } from 'vue'; import { defineAsyncComponent, reactive, watch } from 'vue';
import type { Reactive } from 'vue';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
import { getDefaultFormValues } from '@/utility/form.js';
import type { Reactive } from 'vue';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
@ -39,19 +40,23 @@ export const useWidgetPropsManager = <F extends FormWithDefault>(
save: () => void; save: () => void;
configure: () => void; configure: () => void;
} => { } => {
const widgetProps = reactive<GetFormResultType<F>>((props.widget ? deepClone(props.widget.data) : {}) as GetFormResultType<F>); const widgetProps = reactive((() => {
const np = getDefaultFormValues(propsDef);
const mergeProps = () => { if (props.widget?.data != null) {
for (const prop of Object.keys(propsDef)) { for (const key of Object.keys(props.widget.data) as (keyof F)[]) {
if (typeof widgetProps[prop] === 'undefined') { np[key] = props.widget.data[key] as GetFormResultType<F>[typeof key];
widgetProps[prop] = propsDef[prop].default;
} }
} }
}; return np;
})());
watch(widgetProps, () => { watch(() => props.widget?.data, (to) => {
mergeProps(); if (to != null) {
}, { deep: true, immediate: true }); for (const key of Object.keys(propsDef)) {
widgetProps[key] = to[key];
}
}
}, { deep: true });
const save = throttle(3000, () => { const save = throttle(3000, () => {
emit('updateProps', widgetProps as GetFormResultType<F>); emit('updateProps', widgetProps as GetFormResultType<F>);