diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-12-30 14:32:40 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-30 14:32:40 +0900 |
| commit | 4285303c8155dd91be7dcbb865d5e8f7cb0e1c71 (patch) | |
| tree | 51a193c90401ada45f25921e4efdc07b46095ac5 | |
| parent | enhance(frontend): ウィジェットの設定画面を改良 (#17033) (diff) | |
| download | misskey-4285303c8155dd91be7dcbb865d5e8f7cb0e1c71.tar.gz misskey-4285303c8155dd91be7dcbb865d5e8f7cb0e1c71.tar.bz2 misskey-4285303c8155dd91be7dcbb865d5e8f7cb0e1c71.zip | |
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>
| -rw-r--r-- | packages/frontend/src/components/MkForm.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkWidgetSettingsDialog.vue | 11 | ||||
| -rw-r--r-- | packages/frontend/src/utility/form.ts | 10 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/WidgetRss.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/WidgetRssTicker.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/widget.ts | 27 |
6 files changed, 36 insertions, 27 deletions
diff --git a/packages/frontend/src/components/MkForm.vue b/packages/frontend/src/components/MkForm.vue index 750ffa77df..711aa611c3 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"> + <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 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"> + <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 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"> + <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 v-if="v.description" #caption>{{ v.description }}</template> </MkTextarea> diff --git a/packages/frontend/src/components/MkWidgetSettingsDialog.vue b/packages/frontend/src/components/MkWidgetSettingsDialog.vue index cebbe93986..0dc26e054d 100644 --- a/packages/frontend/src/components/MkWidgetSettingsDialog.vue +++ b/packages/frontend/src/components/MkWidgetSettingsDialog.vue @@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only > <component :is="`widget-${widgetName}`" - :key="currentId" :widget="{ name: widgetName, id: '__PREVIEW__', data: settings }" ></component> </div> @@ -48,13 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> 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 { genId } from '@/utility/id.js'; import { i18n } from '@/i18n.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import MkPreviewWithControls from './MkPreviewWithControls.vue'; import MkForm from '@/components/MkForm.vue'; -import type { Form } from '@/utility/form.js'; const props = defineProps<{ widgetName: string; @@ -71,11 +69,6 @@ const emit = defineEmits<{ const dialog = useTemplateRef('dialog'); const settings = reactive<Record<string, any>>(deepClone(props.currentSettings)); -const currentId = ref(genId()); - -watch(settings, () => { - currentId.value = genId(); -}); function save() { emit('saved', deepClone(settings)); diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts index cb4a227f67..fe89333285 100644 --- a/packages/frontend/src/utility/form.ts +++ b/packages/frontend/src/utility/form.ts @@ -25,6 +25,7 @@ export interface StringFormItem extends FormItemBase { required?: boolean; multiline?: boolean; treatAsMfm?: boolean; + manualSave?: boolean; } export interface NumberFormItem extends FormItemBase { @@ -33,6 +34,7 @@ export interface NumberFormItem extends FormItemBase { description?: string; required?: boolean; step?: number; + manualSave?: boolean; } export interface BooleanFormItem extends FormItemBase { @@ -145,3 +147,11 @@ type GetItemType<Item extends FormItem> = export type GetFormResultType<F extends Form> = { [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; +} diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index e5499aa0da..5fa9e833b8 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -28,7 +28,6 @@ import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { i18n } from '@/i18n.js'; const name = 'rss'; @@ -36,6 +35,7 @@ const widgetPropsDef = { url: { type: 'string', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', + manualSave: true, }, refreshIntervalSec: { type: 'number', @@ -68,7 +68,7 @@ const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); url.searchParams.set('url', widgetProps.url); - return url; + return url.toString(); }); const intervalClear = ref<(() => void) | undefined>(); @@ -83,7 +83,7 @@ const tick = () => { }); }; -watch(() => fetchEndpoint, tick); +watch(fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { if (intervalClear.value) { intervalClear.value(); diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 95f82f7d7b..b0d9eec883 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -44,6 +44,7 @@ const widgetPropsDef = { url: { type: 'string', default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', + manualSave: true, }, shuffle: { type: 'boolean', @@ -119,7 +120,7 @@ const tick = () => { }); }; -watch(() => fetchEndpoint, tick); +watch(fetchEndpoint, tick); watch(() => widgetProps.refreshIntervalSec, () => { if (intervalClear.value) { intervalClear.value(); diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts index 6c5ff36b16..d4209f6251 100644 --- a/packages/frontend/src/widgets/widget.ts +++ b/packages/frontend/src/widgets/widget.ts @@ -4,8 +4,9 @@ */ import { defineAsyncComponent, reactive, watch } from 'vue'; -import type { Reactive } from 'vue'; 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 * as os from '@/os.js'; import { deepClone } from '@/utility/clone.js'; @@ -39,19 +40,23 @@ export const useWidgetPropsManager = <F extends FormWithDefault>( save: () => void; configure: () => void; } => { - const widgetProps = reactive<GetFormResultType<F>>((props.widget ? deepClone(props.widget.data) : {}) as GetFormResultType<F>); - - const mergeProps = () => { - for (const prop of Object.keys(propsDef)) { - if (typeof widgetProps[prop] === 'undefined') { - widgetProps[prop] = propsDef[prop].default; + const widgetProps = reactive((() => { + const np = getDefaultFormValues(propsDef); + if (props.widget?.data != null) { + for (const key of Object.keys(props.widget.data) as (keyof F)[]) { + np[key] = props.widget.data[key] as GetFormResultType<F>[typeof key]; } } - }; + return np; + })()); - watch(widgetProps, () => { - mergeProps(); - }, { deep: true, immediate: true }); + watch(() => props.widget?.data, (to) => { + if (to != null) { + for (const key of Object.keys(propsDef)) { + widgetProps[key] = to[key]; + } + } + }, { deep: true }); const save = throttle(3000, () => { emit('updateProps', widgetProps as GetFormResultType<F>); |