From 0aa667b8cc5e0df129574194d91252a66c913f60 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 22 Aug 2026 21:42:41 +0200 Subject: [PATCH] feat: preview widget settings live --- .../app/[locale]/widgets/[kind]/_content.tsx | 3 + .../components/board/items/item-actions.tsx | 22 +++++++ .../src/components/board/items/item-menu.tsx | 29 ++++----- .../board/items/widget-context-menu.tsx | 20 ++++-- .../widgets/src/modals/widget-edit-modal.tsx | 64 +++++++++++++++---- 5 files changed, 103 insertions(+), 35 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx index 0e23b728d..80e8a6d1a 100644 --- a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx +++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx @@ -59,6 +59,7 @@ export const WidgetPreviewPageContent = ({ kind }: WidgetPreviewPageContentProps preloadWidgetEditModal(); try { const integrationData = hasIntegrationSupport ? await utils.integration.all.ensureData() : []; + const originalState = state; openWidgetEditModal( { kind, @@ -67,6 +68,8 @@ export const WidgetPreviewPageContent = ({ kind }: WidgetPreviewPageContentProps onSuccessfulEdit: (value) => { setState(value); }, + onPreviewChange: setState, + onPreviewRestore: () => setState(originalState), integrationData: integrationData.filter((integration) => (currentDefinition.supportedIntegrations ?? []).includes(integration.kind), ), diff --git a/apps/nextjs/src/components/board/items/item-actions.tsx b/apps/nextjs/src/components/board/items/item-actions.tsx index 28f23179a..27bcbb271 100644 --- a/apps/nextjs/src/components/board/items/item-actions.tsx +++ b/apps/nextjs/src/components/board/items/item-actions.tsx @@ -30,6 +30,13 @@ interface UpdateItemIntegrations { newIntegrations: string[]; } +interface UpdateItemSettings { + itemId: string; + options: Record; + advancedOptions: BoardItemAdvancedOptions; + integrationIds: string[]; +} + export const useItemActions = () => { const { updateBoard } = useUpdateBoard(); const currentLayoutId = useCurrentLayout(); @@ -77,6 +84,20 @@ export const useItemActions = () => { [updateBoard], ); + const updateItemSettings = useCallback( + ({ itemId, options, advancedOptions, integrationIds }: UpdateItemSettings) => { + updateBoard((previous) => ({ + ...previous, + items: previous.items.map((item) => { + if (item.id !== itemId || !("integrationIds" in item)) return item; + + return { ...item, options, advancedOptions, integrationIds }; + }), + })); + }, + [updateBoard], + ); + const moveAndResizeItem = useCallback( (input: Omit) => { updateBoard(moveAndResizeItemCallback({ ...input, layoutId: currentLayoutId })); @@ -105,6 +126,7 @@ export const useItemActions = () => { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, + updateItemSettings, duplicateItem, createItem, }; diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index 6b6f2ce06..5220f5f06 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -42,8 +42,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM const { openModal } = useModalAction(LazyWidgetEditModal); const openMoveModal = useOpenItemMoveModal(); const [isMenuOpen, setIsMenuOpen] = useState(false); - const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } = - useItemActions(); + const { updateItemSettings, duplicateItem, removeItem } = useItemActions(); const { integrations: integrationData, section } = useSectionContext(); const settings = useSettings(); const label = item.advancedOptions.title?.trim() || getWidgetName(item.kind, t); @@ -57,6 +56,11 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM }, [item, resetErrorBoundary]); const openEditModal = () => { + const originalSettings = { + options: item.options, + advancedOptions: item.advancedOptions, + integrationIds: item.integrationIds, + }; openModal( { kind: item.kind, @@ -66,19 +70,14 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM options: item.options, integrationIds: item.integrationIds, }, - onSuccessfulEdit: ({ options, integrationIds, advancedOptions }) => { - updateItemOptions({ - itemId: item.id, - newOptions: options, - }); - updateItemAdvancedOptions({ - itemId: item.id, - newAdvancedOptions: advancedOptions, - }); - updateItemIntegrations({ - itemId: item.id, - newIntegrations: integrationIds, - }); + onPreviewChange: (value) => { + updateItemSettings({ itemId: item.id, ...value }); + }, + onPreviewRestore: () => { + updateItemSettings({ itemId: item.id, ...originalSettings }); + }, + onSuccessfulEdit: (value) => { + updateItemSettings({ itemId: item.id, ...value }); refResetErrorBoundaryOnNextRender.current = true; }, onIntegrationSaved: resetErrorBoundary, diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 1767298ff..b3499f91f 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -58,7 +58,7 @@ export const WidgetContextMenu = ({ const tCommon = useI18n("common.action"); const settings = useSettings(); const { openModal } = useModalAction(LazyWidgetEditModal); - const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations } = useItemActions(); + const { updateItemSettings } = useItemActions(); const hasSupportedIntegrations = (definition.supportedIntegrations?.length ?? 0) > 0; const { data: integrationData = [], isPending } = clientApi.integration.all.useQuery(undefined, { enabled: hasSupportedIntegrations, @@ -154,6 +154,11 @@ export const WidgetContextMenu = ({ }) ?? []; const openEditModal = useCallback(() => { + const originalSettings = { + options: item.options, + advancedOptions: item.advancedOptions, + integrationIds: item.integrationIds, + }; openModal( { kind: item.kind, @@ -163,10 +168,13 @@ export const WidgetContextMenu = ({ options, integrationIds: item.integrationIds, }, + onPreviewChange: (value) => { + updateItemSettings({ itemId: item.id, ...value }); + }, + onPreviewRestore: () => { + updateItemSettings({ itemId: item.id, ...originalSettings }); + }, onSuccessfulEdit: (editResult) => { - updateItemOptions({ itemId: item.id, newOptions: editResult.options }); - updateItemAdvancedOptions({ itemId: item.id, newAdvancedOptions: editResult.advancedOptions }); - updateItemIntegrations({ itemId: item.id, newIntegrations: editResult.integrationIds }); persistBoard((previous) => ({ ...previous, items: previous.items.map((boardItem) => @@ -208,9 +216,7 @@ export const WidgetContextMenu = ({ options, persistBoard, settings, - updateItemAdvancedOptions, - updateItemIntegrations, - updateItemOptions, + updateItemSettings, ]); const handleToggle = useCallback( diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index f52fe2a4f..8c4e680cf 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -52,6 +52,8 @@ export interface WidgetEditModalProps { definition: WidgetDefinition; value: WidgetEditModalState; onSuccessfulEdit: (value: WidgetEditModalState) => void; + onPreviewChange?: (value: WidgetEditModalState) => void; + onPreviewRestore?: () => void; integrationData: IntegrationSelectOption[]; integrationSupport: boolean; settings: SettingsContextProps; @@ -84,6 +86,16 @@ export const WidgetEditModal = createModal>(({ const [mountedIntegrationIds, setMountedIntegrationIds] = useState([]); const [activeIntegrationId, setActiveIntegrationId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const hasPreviewedRef = useRef(false); + const hasSavedRef = useRef(false); + const previewCallbacksRef = useRef({ + onPreviewChange: innerProps.onPreviewChange, + onPreviewRestore: innerProps.onPreviewRestore, + }); + previewCallbacksRef.current = { + onPreviewChange: innerProps.onPreviewChange, + onPreviewRestore: innerProps.onPreviewRestore, + }; const widgetFormId = useId(); const { focusRect, inspectorSide } = useWidgetEditFocus(innerProps.itemId, innerProps.focusTargetId); @@ -117,25 +129,47 @@ export const WidgetEditModal = createModal>(({ if (integrationsRequired) integrationIdsSchema = integrationIdsSchema.min(1); if (Number.isFinite(maxIntegrations)) integrationIdsSchema = integrationIdsSchema.max(maxIntegrations); + const widgetFormSchema = z.object({ + options: optionsSchema, + integrationIds: integrationIdsSchema, + }); + const applyPreview = (value: WidgetEditModalState) => { + if (!innerProps.onPreviewChange) return; + + hasPreviewedRef.current = true; + innerProps.onPreviewChange(value); + }; const form = useForm({ mode: "controlled", - initialValues: innerProps.value, - validate: schemaResolver( - z.object({ - options: optionsSchema, - integrationIds: integrationIdsSchema, - advancedOptions: z.object({ - customCssClasses: z.array(z.string()), - borderColor: z.string(), - }), - }), - { sync: true }, - ), + initialValues: { + options: innerProps.value.options, + integrationIds: innerProps.value.integrationIds, + }, + validate: schemaResolver(widgetFormSchema, { sync: true }), validateInputOnBlur: true, validateInputOnChange: true, + onValuesChange: (values) => { + if (!widgetFormSchema.safeParse(values).success) return; + + applyPreview({ ...values, advancedOptions }); + }, }); const { openModal } = useModalAction(WidgetAdvancedOptionsModal); + useEffect( + () => () => { + if (!hasPreviewedRef.current || hasSavedRef.current) return; + + const { onPreviewChange, onPreviewRestore } = previewCallbacksRef.current; + if (onPreviewRestore) { + onPreviewRestore(); + return; + } + onPreviewChange?.(innerProps.value); + }, + [innerProps.value], + ); + const canModifyApps = session?.user.permissions.includes("app-modify-all") ?? false; const canConfigureWidget = innerProps.kind !== "customApi" || (session?.user.permissions.includes("admin") ?? false); const appId = innerProps.appId; @@ -215,6 +249,7 @@ export const WidgetEditModal = createModal>(({ ...values, advancedOptions, }); + hasSavedRef.current = true; actions.closeModal(); } finally { setIsSubmitting(false); @@ -403,7 +438,10 @@ export const WidgetEditModal = createModal>(({ onClick={() => openModal({ advancedOptions, - onSuccess: setAdvancedOptions, + onSuccess: (value) => { + setAdvancedOptions(value); + applyPreview({ ...form.getValues(), advancedOptions: value }); + }, }) } >