* 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:
parent
14f58255ee
commit
4285303c81
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue