From c520994cbe0f1df581d1f7ae11eb82dc02bacd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Wed, 22 Jan 2025 10:16:51 +0100 Subject: [PATCH 01/21] fix: improve action separator and treeactions bar --- src/actionbar/ActionButton.tsx | 3 +-- src/actionbar/DashboardActionBar.tsx | 9 +++------ src/actionbar/FormActionBar.tsx | 14 ++++++------- src/actionbar/GraphActionBar.tsx | 2 +- src/actionbar/TreeActionBar.tsx | 30 ++++++++++++++-------------- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/actionbar/ActionButton.tsx b/src/actionbar/ActionButton.tsx index 93828824a..51f06992c 100644 --- a/src/actionbar/ActionButton.tsx +++ b/src/actionbar/ActionButton.tsx @@ -1,11 +1,10 @@ -import React from "react"; import ButtonWithTooltip from "@/common/ButtonWithTooltip"; import { LoadingOutlined } from "@ant-design/icons"; import { ButtonProps } from "antd"; type Props = ButtonProps & { tooltip: string; - onClick: any; + onClick?: any; icon: any; disabled?: boolean; label?: string; diff --git a/src/actionbar/DashboardActionBar.tsx b/src/actionbar/DashboardActionBar.tsx index 00d122df7..520bd0edb 100644 --- a/src/actionbar/DashboardActionBar.tsx +++ b/src/actionbar/DashboardActionBar.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import { useContext } from "react"; import { DashboardActionContext, DashboardActionContextType, @@ -11,6 +11,7 @@ import { BorderOuterOutlined, } from "@ant-design/icons"; import { useLocale } from "@gisce/react-formiga-components"; +import { ActionBarSeparator } from "./FormActionBar"; function DashboardActionBar() { const { isLoading, dashboardRef, moveItemsEnabled, setMoveItemsEnabled } = @@ -33,7 +34,7 @@ function DashboardActionBar() { setMoveItemsEnabled(!moveItemsEnabled); }} /> - {separator()} + } tooltip={t("configDashboard")} @@ -56,8 +57,4 @@ function DashboardActionBar() { ); } -function separator() { - return
; -} - export default DashboardActionBar; diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index 8b39a9e65..b7c083d45 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -197,8 +197,8 @@ function FormActionBar({ toolbar }: { toolbar: any }) { {formIsLoading && ( <> - {separator()} - {separator()} + + )} @@ -237,7 +237,7 @@ function FormActionBar({ toolbar }: { toolbar: any }) { }) } /> - {separator()} + } tooltip={t("showLogs")} @@ -250,7 +250,7 @@ function FormActionBar({ toolbar }: { toolbar: any }) { disabled={mustDisableButtons || currentId === undefined} onClick={() => tryAction(() => (formRef.current as any).fetchValues())} /> - {separator()} + - {separator()} + } @@ -278,7 +278,7 @@ function FormActionBar({ toolbar }: { toolbar: any }) { onClick={() => tryAction(onNextClick)} /> - {separator()} + } placement="bottomRight" @@ -379,7 +379,7 @@ function FormActionBar({ toolbar }: { toolbar: any }) { ); } -const separator = () =>
; +export const ActionBarSeparator = () =>
; const saveDocument = async ({ onFormSave, diff --git a/src/actionbar/GraphActionBar.tsx b/src/actionbar/GraphActionBar.tsx index 54d347e58..c6dde444c 100644 --- a/src/actionbar/GraphActionBar.tsx +++ b/src/actionbar/GraphActionBar.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import { useContext } from "react"; import { Space } from "antd"; import ChangeViewButton from "./ChangeViewButton"; import { diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index c05627d99..17951a7c2 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -32,6 +32,7 @@ import { mergeParams } from "@/helpers/searchHelper"; import { useFeatureIsEnabled } from "@/context/ConfigContext"; import { ErpFeatureKeys } from "@/models/erpFeature"; import { useHotkeys } from "react-hotkeys-hook"; +import { ActionBarSeparator } from "./FormActionBar"; type Props = { parentContext?: any; @@ -205,13 +206,19 @@ function TreeActionBar(props: Props) { }); } + const finalDomain = (() => { + const domain = searchTreeRef?.current?.getDomain(); + const finalValues = mergeParams(domain || [], searchParams || []); + return finalValues; + })(); + return ( {treeIsLoading && ( <> - {separator()} - {separator()} + + )} {treeExpandable ? null : ( @@ -246,7 +253,7 @@ function TreeActionBar(props: Props) { badgeNumber={searchParams?.length} /> )} - {separator()} + } @@ -270,7 +277,7 @@ function TreeActionBar(props: Props) { loading={removingItem} onClick={tryDelete} /> - {separator()} + )} {!treeExpandable && ( <> - {separator()} + )} - {separator()} + } placement="bottomRight" @@ -351,7 +358,7 @@ function TreeActionBar(props: Props) { /> {advancedExportEnabled && ( <> - {separator()} + setExportModalVisible(false)} model={currentModel!} - domain={mergeParams( - searchTreeRef?.current?.getDomain() || [], - searchParams || [], - )} + domain={finalDomain} limit={limit} totalRegisters={totalItems || 0} selectedRegistersToExport={selectedRowItems} @@ -440,8 +444,4 @@ function TreeActionBar(props: Props) { ); } -function separator() { - return
; -} - export default TreeActionBar; From 1600b5cdc314746b0a56ada4d8e513a95d3e122a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Wed, 22 Jan 2025 10:23:12 +0100 Subject: [PATCH 02/21] feat: extract toolbar behaviour to hooks https://github.com/gisce/webclient/issues/1646 --- src/actionbar/FormActionBar.tsx | 105 ++++-------------------- src/actionbar/TreeActionBar.tsx | 100 ++++++----------------- src/hooks/useFormToolbarButtons.ts | 126 +++++++++++++++++++++++++++++ src/hooks/useTreeToolbarButtons.ts | 96 ++++++++++++++++++++++ 4 files changed, 261 insertions(+), 166 deletions(-) create mode 100644 src/hooks/useFormToolbarButtons.ts create mode 100644 src/hooks/useTreeToolbarButtons.ts diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index b7c083d45..e5a19704c 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -27,18 +27,15 @@ import { TabManagerContext, TabManagerContextType, } from "@/context/TabManagerContext"; -import { - ContentRootContext, - ContentRootContextType, -} from "@/context/ContentRootContext"; import AttachmentsButton from "./AttachmentsButton"; import { Attachment } from "./AttachmentsButtonWrapper"; import { useNextPrevious } from "./useNextPrevious"; +import { + saveDocument, + useFormToolbarButtons, +} from "@/hooks/useFormToolbarButtons"; function FormActionBar({ toolbar }: { toolbar: any }) { - const contentRootContext = useContext( - ContentRootContext, - ) as ContentRootContextType; const tabManagerContext = useContext( TabManagerContext, ) as TabManagerContextType; @@ -73,12 +70,17 @@ function FormActionBar({ toolbar }: { toolbar: any }) { isActive, } = useActionViewContext(); - const { processAction } = contentRootContext || {}; - const { openRelate, openDefaultActionForModel } = tabManagerContext || {}; + const { openDefaultActionForModel } = tabManagerContext || {}; const mustDisableButtons = formIsSaving || removingItem || formIsLoading || duplicatingItem; + const { actionButtonProps, printButtonProps, relateButtonProps } = + useFormToolbarButtons({ + toolbar, + mustDisableButtons, + }); + const tryAction = useCallback( (action: () => void) => { if (formHasChanges) { @@ -147,19 +149,6 @@ function FormActionBar({ toolbar }: { toolbar: any }) { } }, [currentId, currentModel, formRef, goToResourceId, setDuplicatingItem]); - const runAction = useCallback( - (actionData: any) => { - processAction?.({ - actionData, - values: (formRef.current as any).getValues(), - fields: (formRef.current as any).getFields(), - context: (formRef.current as any).getContext(), - onRefreshParentValues: () => (formRef.current as any).fetchValues(), - }); - }, - [formRef, processAction], - ); - useHotkeys( "pagedown", () => isActive && tryAction(onNextClick), @@ -279,64 +268,9 @@ function FormActionBar({ toolbar }: { toolbar: any }) { /> - } - placement="bottomRight" - disabled={mustDisableButtons} - onRetrieveData={async () => [ - { label: t("actions"), items: toolbar?.action }, - ]} - onItemClick={async (action: any) => { - if (action) { - const result = await saveDocument({ onFormSave }); - if (result.succeed) runAction(action); - } - }} - /> - } - disabled={mustDisableButtons} - placement="bottomRight" - onRetrieveData={async () => [ - { label: t("reports"), items: toolbar?.print }, - ]} - onItemClick={async (report: any) => { - if (report) { - const result = await saveDocument({ onFormSave }); - if (result.succeed) { - runAction({ - ...report, - datas: { - ...(report.datas || {}), - ids: [result.currentId as number], - }, - }); - } - } - }} - /> - } - disabled={mustDisableButtons} - placement="bottomRight" - onRetrieveData={async () => [ - { label: t("related"), items: toolbar?.relate }, - ]} - onItemClick={async (relate: any) => { - if (relate) { - const result = await saveDocument({ onFormSave }); - if (result.succeed) { - openRelate({ - relateData: relate, - values: (formRef.current as any).getValues(), - fields: (formRef.current as any).getFields(), - action_id: relate.id, - action_type: relate.type, - }); - } - } - }} - /> + } {...actionButtonProps} /> + } {...printButtonProps} /> + } {...relateButtonProps} />
; -const saveDocument = async ({ - onFormSave, -}: { - onFormSave?: () => Promise<{ succeed: boolean; id: number }>; -}): Promise<{ succeed: boolean; currentId?: number }> => { - const result = await onFormSave?.(); - return result?.succeed - ? { succeed: true, currentId: result.id } - : { succeed: false, currentId: undefined }; -}; - const getAttachmentActionPayload = (res_model: string, res_id: number) => ({ model: "ir.attachment", domain: [ diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index 17951a7c2..99f1377aa 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -20,10 +20,6 @@ import { useLocale, DropdownButton } from "@gisce/react-formiga-components"; import showConfirmDialog from "@/ui/ConfirmDialog"; import ConnectionProvider from "@/ConnectionProvider"; import showErrorDialog from "@/ui/ActionErrorDialog"; -import { - ContentRootContext, - ContentRootContextType, -} from "@/context/ContentRootContext"; import ButtonWithBadge from "./ButtonWithBadge"; import { showLogInfo } from "@/helpers/logInfoHelper"; import SearchBar from "./SearchBar"; @@ -33,6 +29,10 @@ import { useFeatureIsEnabled } from "@/context/ConfigContext"; import { ErpFeatureKeys } from "@/models/erpFeature"; import { useHotkeys } from "react-hotkeys-hook"; import { ActionBarSeparator } from "./FormActionBar"; +import { + useTreeToolbarButtons, + useRunTreeAction, +} from "@/hooks/useTreeToolbarButtons"; type Props = { parentContext?: any; @@ -74,13 +74,17 @@ function TreeActionBar(props: Props) { ErpFeatureKeys.FEATURE_ADVANCED_EXPORT, ); const { t } = useLocale(); - const contentRootContext = useContext( - ContentRootContext, - ) as ContentRootContextType; - const { processAction } = contentRootContext || {}; const [exportModalVisible, setExportModalVisible] = useState(false); const isFirstMount = useRef(true); + const { actionButtonProps, printButtonProps } = useTreeToolbarButtons({ + toolbar, + disabled: treeIsLoading, + parentContext, + }); + + const runAction = useRunTreeAction(); + useHotkeys( "ctrl+l,command+l", () => { @@ -187,25 +191,6 @@ function TreeActionBar(props: Props) { } } - function runAction(actionData: any) { - processAction?.({ - actionData, - values: { - active_id: selectedRowItems?.map((item) => item.id)[0], - active_ids: selectedRowItems?.map((item) => item.id), - }, - fields: {}, - context: { - ...parentContext, - active_id: selectedRowItems?.map((item) => item.id)[0], - active_ids: selectedRowItems?.map((item) => item.id), - }, - onRefreshParentValues: () => { - searchTreeRef?.current?.refreshResults(); - }, - }); - } - const finalDomain = (() => { const domain = searchTreeRef?.current?.getDomain(); const finalValues = mergeParams(domain || [], searchParams || []); @@ -316,46 +301,8 @@ function TreeActionBar(props: Props) { )} - } - placement="bottomRight" - disabled={ - !(selectedRowItems && selectedRowItems?.length > 0) || treeIsLoading - } - onRetrieveData={async () => [ - { label: t("actions"), items: toolbar?.action || [] }, - ]} - onItemClick={(action: any) => { - if (!action) { - return; - } - - runAction(action); - }} - /> - } - placement="bottomRight" - disabled={ - !(selectedRowItems && selectedRowItems?.length > 0) || treeIsLoading - } - onRetrieveData={async () => { - return [{ label: t("reports"), items: toolbar?.print || [] }]; - }} - onItemClick={(report: any) => { - if (!report) { - return; - } - - runAction({ - ...report, - datas: { - ...(report.datas || {}), - ids: selectedRowItems!.map((item) => item.id), - }, - }); - }} - /> + } {...actionButtonProps} /> + } {...printButtonProps} /> {advancedExportEnabled && ( <> @@ -408,16 +355,19 @@ function TreeActionBar(props: Props) { idsToExport = results?.map((item) => item.id) || []; } - runAction({ - id: -1, - model: currentModel, - report_name: "printscreen.list", - type: "ir.actions.report.xml", - datas: { + runAction( + { + id: -1, model: currentModel, - ids: idsToExport, + report_name: "printscreen.list", + type: "ir.actions.report.xml", + datas: { + model: currentModel, + ids: idsToExport, + }, }, - }); + parentContext, + ); return; } diff --git a/src/hooks/useFormToolbarButtons.ts b/src/hooks/useFormToolbarButtons.ts new file mode 100644 index 000000000..7844b90aa --- /dev/null +++ b/src/hooks/useFormToolbarButtons.ts @@ -0,0 +1,126 @@ +import { useCallback, useContext } from "react"; +import { useLocale } from "@gisce/react-formiga-components"; +import { useActionViewContext } from "@/context/ActionViewContext"; +import { + ContentRootContext, + ContentRootContextType, +} from "@/context/ContentRootContext"; +import { + TabManagerContext, + TabManagerContextType, +} from "@/context/TabManagerContext"; + +interface UseFormToolbarButtonsProps { + toolbar: any; + mustDisableButtons?: boolean; +} + +interface SaveDocumentResult { + succeed: boolean; + currentId?: number; +} + +export const useFormToolbarButtons = ({ + toolbar, + mustDisableButtons = false, +}: UseFormToolbarButtonsProps) => { + const { t } = useLocale(); + const contentRootContext = useContext( + ContentRootContext, + ) as ContentRootContextType; + const tabManagerContext = useContext( + TabManagerContext, + ) as TabManagerContextType; + + const { formRef, onFormSave } = useActionViewContext(); + const { processAction } = contentRootContext || {}; + const { openRelate } = tabManagerContext || {}; + + const runAction = useCallback( + (actionData: any) => { + processAction?.({ + actionData, + values: (formRef.current as any).getValues(), + fields: (formRef.current as any).getFields(), + context: (formRef.current as any).getContext(), + onRefreshParentValues: () => (formRef.current as any).fetchValues(), + }); + }, + [formRef, processAction], + ); + + const actionButtonProps = { + disabled: mustDisableButtons, + placement: "bottomRight" as const, + onRetrieveData: async () => [ + { label: t("actions"), items: toolbar?.action }, + ], + onItemClick: async (action: any) => { + if (action) { + const result = await saveDocument({ onFormSave }); + if (result.succeed) runAction(action); + } + }, + }; + + const printButtonProps = { + disabled: mustDisableButtons, + placement: "bottomRight" as const, + onRetrieveData: async () => [ + { label: t("reports"), items: toolbar?.print }, + ], + onItemClick: async (report: any) => { + if (report) { + const result = await saveDocument({ onFormSave }); + if (result.succeed) { + runAction({ + ...report, + datas: { + ...(report.datas || {}), + ids: [result.currentId as number], + }, + }); + } + } + }, + }; + + const relateButtonProps = { + disabled: mustDisableButtons, + placement: "bottomRight" as const, + onRetrieveData: async () => [ + { label: t("related"), items: toolbar?.relate }, + ], + onItemClick: async (relate: any) => { + if (relate) { + const result = await saveDocument({ onFormSave }); + if (result.succeed) { + openRelate({ + relateData: relate, + values: (formRef.current as any).getValues(), + fields: (formRef.current as any).getFields(), + action_id: relate.id, + action_type: relate.type, + }); + } + } + }, + }; + + return { + actionButtonProps, + printButtonProps, + relateButtonProps, + }; +}; + +export const saveDocument = async ({ + onFormSave, +}: { + onFormSave?: () => Promise<{ succeed: boolean; id: number }>; +}): Promise => { + const result = await onFormSave?.(); + return result?.succeed + ? { succeed: true, currentId: result.id } + : { succeed: false, currentId: undefined }; +}; diff --git a/src/hooks/useTreeToolbarButtons.ts b/src/hooks/useTreeToolbarButtons.ts new file mode 100644 index 000000000..407e141d2 --- /dev/null +++ b/src/hooks/useTreeToolbarButtons.ts @@ -0,0 +1,96 @@ +import { useCallback, useContext } from "react"; +import { useLocale } from "@gisce/react-formiga-components"; +import { useActionViewContext } from "@/context/ActionViewContext"; +import { + ContentRootContext, + ContentRootContextType, +} from "@/context/ContentRootContext"; + +interface UseTreeToolbarButtonsProps { + toolbar: any; + disabled?: boolean; + parentContext?: any; +} + +export const useRunTreeAction = () => { + const contentRootContext = useContext( + ContentRootContext, + ) as ContentRootContextType; + const { processAction } = contentRootContext || {}; + const { selectedRowItems, searchTreeRef } = useActionViewContext(); + + return useCallback( + (actionData: any, context: any = {}) => { + processAction?.({ + actionData, + values: { + active_id: selectedRowItems?.map((item) => item.id)[0], + active_ids: selectedRowItems?.map((item) => item.id), + }, + fields: {}, + context: { + ...context, + active_id: selectedRowItems?.map((item) => item.id)[0], + active_ids: selectedRowItems?.map((item) => item.id), + }, + onRefreshParentValues: () => { + searchTreeRef?.current?.refreshResults(); + }, + }); + }, + [processAction, selectedRowItems, searchTreeRef], + ); +}; + +export const useTreeToolbarButtons = ({ + toolbar, + disabled = false, + parentContext = {}, +}: UseTreeToolbarButtonsProps) => { + const { t } = useLocale(); + const { selectedRowItems } = useActionViewContext(); + const runAction = useRunTreeAction(); + + const actionButtonProps = { + placement: "bottomRight" as const, + disabled: !selectedRowItems?.length || disabled, + onRetrieveData: async () => [ + { label: t("actions"), items: toolbar?.action || [] }, + ], + onItemClick: (action: any) => { + if (!action) { + return; + } + runAction(action, parentContext); + }, + }; + + const printButtonProps = { + placement: "bottomRight" as const, + disabled: !selectedRowItems?.length || disabled, + onRetrieveData: async () => [ + { label: t("reports"), items: toolbar?.print || [] }, + ], + onItemClick: (report: any) => { + if (!report) { + return; + } + + runAction( + { + ...report, + datas: { + ...(report.datas || {}), + ids: selectedRowItems!.map((item) => item.id), + }, + }, + parentContext, + ); + }, + }; + + return { + actionButtonProps, + printButtonProps, + }; +}; From 4e8627e23bf9cf0c233cab5fb8bc3aa1d70f6b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Wed, 22 Jan 2025 10:51:39 +0100 Subject: [PATCH 03/21] feat: improve actionbar components with react best practices https://github.com/gisce/webclient/issues/1646 --- src/actionbar/ActionBarSeparator.tsx | 6 + src/actionbar/DashboardActionBar.tsx | 2 +- src/actionbar/FormActionBar.tsx | 164 +++++++----- src/actionbar/TreeActionBar.tsx | 372 ++++++++++++++------------- 4 files changed, 309 insertions(+), 235 deletions(-) create mode 100644 src/actionbar/ActionBarSeparator.tsx diff --git a/src/actionbar/ActionBarSeparator.tsx b/src/actionbar/ActionBarSeparator.tsx new file mode 100644 index 000000000..ff5dcf334 --- /dev/null +++ b/src/actionbar/ActionBarSeparator.tsx @@ -0,0 +1,6 @@ +import { memo } from "react"; + +export const ActionBarSeparator = memo(() => ( +
+)); +ActionBarSeparator.displayName = "ActionBarSeparator"; diff --git a/src/actionbar/DashboardActionBar.tsx b/src/actionbar/DashboardActionBar.tsx index 520bd0edb..ad5414576 100644 --- a/src/actionbar/DashboardActionBar.tsx +++ b/src/actionbar/DashboardActionBar.tsx @@ -11,7 +11,7 @@ import { BorderOuterOutlined, } from "@ant-design/icons"; import { useLocale } from "@gisce/react-formiga-components"; -import { ActionBarSeparator } from "./FormActionBar"; +import { ActionBarSeparator } from "./ActionBarSeparator"; function DashboardActionBar() { const { isLoading, dashboardRef, moveItemsEnabled, setMoveItemsEnabled } = diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index e5a19704c..f0db3373a 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -1,4 +1,4 @@ -import { useContext, useCallback } from "react"; +import { useContext, useCallback, memo, useMemo } from "react"; import { Space, Spin } from "antd"; import { SaveOutlined, @@ -34,8 +34,9 @@ import { saveDocument, useFormToolbarButtons, } from "@/hooks/useFormToolbarButtons"; +import { ActionBarSeparator } from "./ActionBarSeparator"; -function FormActionBar({ toolbar }: { toolbar: any }) { +function FormActionBarComponent({ toolbar }: { toolbar: any }) { const tabManagerContext = useContext( TabManagerContext, ) as TabManagerContextType; @@ -72,8 +73,10 @@ function FormActionBar({ toolbar }: { toolbar: any }) { const { openDefaultActionForModel } = tabManagerContext || {}; - const mustDisableButtons = - formIsSaving || removingItem || formIsLoading || duplicatingItem; + const mustDisableButtons = useMemo( + () => formIsSaving || removingItem || formIsLoading || duplicatingItem, + [formIsSaving, removingItem, formIsLoading, duplicatingItem], + ); const { actionButtonProps, printButtonProps, relateButtonProps } = useFormToolbarButtons({ @@ -149,6 +152,59 @@ function FormActionBar({ toolbar }: { toolbar: any }) { } }, [currentId, currentModel, formRef, goToResourceId, setDuplicatingItem]); + const handleChangeView = useCallback( + (view: any) => { + setPreviousView?.(currentView); + setFormHasChanges?.(false); + setCurrentView?.(view); + }, + [currentView, setPreviousView, setFormHasChanges, setCurrentView], + ); + + const handleRefresh = useCallback(() => { + tryAction(() => (formRef.current as any).fetchValues()); + }, [tryAction, formRef]); + + const handleAddNewAttachment = useCallback(async () => { + const result = await saveDocument({ onFormSave }); + if (result.succeed) { + openDefaultActionForModel?.({ + ...getAttachmentActionPayload( + currentModel as string, + result.currentId as number, + ), + initialViewType: "form", + }); + } + }, [currentModel, onFormSave, openDefaultActionForModel]); + + const handleListAllAttachments = useCallback(async () => { + const result = await saveDocument({ onFormSave }); + if (result.succeed) { + openDefaultActionForModel?.({ + ...getAttachmentActionPayload( + currentModel as string, + result.currentId as number, + ), + initialViewType: "tree", + }); + } + }, [currentModel, onFormSave, openDefaultActionForModel]); + + const handleViewAttachmentDetails = useCallback( + async (attachment: Attachment) => { + const result = await saveDocument({ onFormSave }); + if (result.succeed) { + openDefaultActionForModel?.({ + model: "ir.attachment", + res_id: attachment.id, + initialViewType: "form", + }); + } + }, + [onFormSave, openDefaultActionForModel], + ); + useHotkeys( "pagedown", () => isActive && tryAction(onNextClick), @@ -237,36 +293,24 @@ function FormActionBar({ toolbar }: { toolbar: any }) { icon={} tooltip={t("refresh")} disabled={mustDisableButtons || currentId === undefined} - onClick={() => tryAction(() => (formRef.current as any).fetchValues())} + onClick={handleRefresh} /> { - setPreviousView?.(currentView); - setFormHasChanges?.(false); - setCurrentView?.(view); - }} + onChangeView={handleChangeView} disabled={mustDisableButtons} formHasChanges={formHasChanges} /> - - } - tooltip={t("previous")} - disabled={mustDisableButtons} - onClick={() => tryAction(onPreviousClick)} - /> - } - tooltip={t("next")} - disabled={mustDisableButtons} - onClick={() => tryAction(onNextClick)} - /> - + } {...actionButtonProps} /> } {...printButtonProps} /> @@ -274,46 +318,48 @@ function FormActionBar({ toolbar }: { toolbar: any }) { { - const result = await saveDocument({ onFormSave }); - if (result.succeed) { - openDefaultActionForModel({ - ...getAttachmentActionPayload( - currentModel as string, - result.currentId as number, - ), - initialViewType: "form", - }); - } - }} - onListAllAttachments={async () => { - const result = await saveDocument({ onFormSave }); - if (result.succeed) { - openDefaultActionForModel({ - ...getAttachmentActionPayload( - currentModel as string, - result.currentId as number, - ), - initialViewType: "tree", - }); - } - }} - onViewAttachmentDetails={async (attachment: Attachment) => { - const result = await saveDocument({ onFormSave }); - if (result.succeed) { - openDefaultActionForModel({ - model: "ir.attachment", - res_id: attachment.id, - initialViewType: "form", - }); - } - }} + onAddNewAttachment={handleAddNewAttachment} + onListAllAttachments={handleListAllAttachments} + onViewAttachmentDetails={handleViewAttachmentDetails} /> ); } -export const ActionBarSeparator = () =>
; +const FormActionBar = memo(FormActionBarComponent); + +const NavigationButtons = memo( + ({ + disabled, + onPreviousClick, + onNextClick, + tryAction, + }: { + disabled: boolean; + onPreviousClick: () => void; + onNextClick: () => void; + tryAction: (action: () => void) => void; + }) => { + const { t } = useLocale(); + return ( + + } + tooltip={t("previous")} + disabled={disabled} + onClick={() => tryAction(onPreviousClick)} + /> + } + tooltip={t("next")} + disabled={disabled} + onClick={() => tryAction(onNextClick)} + /> + + ); + }, +); +NavigationButtons.displayName = "NavigationButtons"; const getAttachmentActionPayload = (res_model: string, res_id: number) => ({ model: "ir.attachment", diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index 99f1377aa..14ec8ddc8 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -1,4 +1,12 @@ -import { useContext, useEffect, useState, useRef } from "react"; +import { + useContext, + useEffect, + useState, + useRef, + memo, + useCallback, + useMemo, +} from "react"; import { Space, Spin } from "antd"; import ChangeViewButton from "./ChangeViewButton"; import { @@ -28,11 +36,11 @@ import { mergeParams } from "@/helpers/searchHelper"; import { useFeatureIsEnabled } from "@/context/ConfigContext"; import { ErpFeatureKeys } from "@/models/erpFeature"; import { useHotkeys } from "react-hotkeys-hook"; -import { ActionBarSeparator } from "./FormActionBar"; import { useTreeToolbarButtons, useRunTreeAction, } from "@/hooks/useTreeToolbarButtons"; +import { ActionBarSeparator } from "./ActionBarSeparator"; type Props = { parentContext?: any; @@ -40,7 +48,11 @@ type Props = { toolbar?: any; }; -function TreeActionBar(props: Props) { +function TreeActionBarComponent({ + parentContext = {}, + treeExpandable, + toolbar, +}: Props) { const { availableViews, currentView, @@ -69,7 +81,6 @@ function TreeActionBar(props: Props) { isInfiniteTree, } = useContext(ActionViewContext) as ActionViewContextType; - const { parentContext = {}, treeExpandable, toolbar } = props; const advancedExportEnabled = useFeatureIsEnabled( ErpFeatureKeys.FEATURE_ADVANCED_EXPORT, ); @@ -85,31 +96,121 @@ function TreeActionBar(props: Props) { const runAction = useRunTreeAction(); - useHotkeys( - "ctrl+l,command+l", - () => { - if (!isActive) { - return; + const hasNameSearch = useMemo( + () => + searchTreeNameSearch !== undefined && + searchTreeNameSearch.trim().length > 0, + [searchTreeNameSearch], + ); + + const finalDomain = useMemo(() => { + const domain = searchTreeRef?.current?.getDomain(); + return mergeParams(domain || [], searchParams || []); + }, [searchTreeRef, searchParams]); + + const handleDuplicate = useCallback(async () => { + try { + setDuplicatingItem?.(true); + const currentId = selectedRowItems![0].id; + const newId = await ConnectionProvider.getHandler().duplicate({ + id: currentId, + model: currentModel!, + context: { ...parentContext }, + }); + if (newId) { + searchTreeRef?.current?.refreshResults(); } - if (previousView) { - setPreviousView?.(currentView); - setCurrentView?.(previousView); + } catch (e) { + showErrorDialog(e); + } finally { + setDuplicatingItem?.(false); + } + }, [ + currentModel, + parentContext, + searchTreeRef, + selectedRowItems, + setDuplicatingItem, + ]); + + const handleRemove = useCallback(async () => { + try { + setRemovingItem?.(true); + await ConnectionProvider.getHandler().deleteObjects({ + model: currentModel!, + ids: selectedRowItems!.map((item) => item.id), + context: { ...parentContext }, + }); + setCurrentId?.(undefined); + setCurrentItemIndex?.(undefined); + searchTreeRef?.current?.refreshResults(); + } catch (e) { + showErrorDialog(e); + } finally { + setRemovingItem?.(false); + } + }, [ + currentModel, + parentContext, + searchTreeRef, + selectedRowItems, + setCurrentId, + setCurrentItemIndex, + setRemovingItem, + ]); + + const handleChangeView = useCallback( + (newView: any) => { + setPreviousView?.(currentView); + setCurrentView?.(newView); + }, + [currentView, setPreviousView, setCurrentView], + ); + + const handleRefresh = useCallback(() => { + searchTreeRef?.current?.refreshResults(); + }, [searchTreeRef]); + + const handleSearch = useCallback( + (searchString?: string) => { + if (searchString && searchString.trim().length > 0) { + setSearchTreeNameSearch?.(searchString); + } else { + setSearchTreeNameSearch?.(undefined); + if (!isInfiniteTree) { + searchTreeRef?.current?.refreshResults(); + } } }, - { enableOnFormTags: true, preventDefault: true }, - [previousView, currentView, isActive], + [isInfiniteTree, searchTreeRef, setSearchTreeNameSearch], ); - useHotkeys( - "ctrl+f,command+f", - () => { - if (!isActive) { + const handleExportAction = useCallback( + (itemClicked: any) => { + if (itemClicked.id === "print_screen") { + let idsToExport = selectedRowItems?.map((item) => item.id) || []; + if (idsToExport.length === 0) { + idsToExport = results?.map((item) => item.id) || []; + } + + runAction( + { + id: -1, + model: currentModel, + report_name: "printscreen.list", + type: "ir.actions.report.xml", + datas: { + model: currentModel, + ids: idsToExport, + }, + }, + parentContext, + ); return; } - setSearchVisible?.(!searchVisible); + setExportModalVisible(true); }, - { enableOnFormTags: true, preventDefault: true }, - [searchVisible], + [currentModel, parentContext, results, runAction, selectedRowItems], ); useEffect(() => { @@ -118,84 +219,48 @@ function TreeActionBar(props: Props) { isFirstMount.current = false; return; } - searchTreeRef?.current?.refreshResults(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInfiniteTree, searchTreeNameSearch]); + }, [isInfiniteTree, searchTreeNameSearch, searchTreeRef]); - const hasNameSearch: boolean = - searchTreeNameSearch !== undefined && - searchTreeNameSearch.trim().length > 0; + useHotkeys( + "ctrl+l,command+l", + () => { + if (!isActive) return; + if (previousView) { + setPreviousView?.(currentView); + setCurrentView?.(previousView); + } + }, + { enableOnFormTags: true, preventDefault: true }, + [previousView, currentView, isActive, setPreviousView, setCurrentView], + ); - function tryDuplicate() { + useHotkeys( + "ctrl+f,command+f", + () => { + if (!isActive) return; + setSearchVisible?.(!searchVisible); + }, + { enableOnFormTags: true, preventDefault: true }, + [searchVisible, isActive, setSearchVisible], + ); + + const tryDuplicate = useCallback(() => { showConfirmDialog({ confirmMessage: t("confirmDuplicate"), t, - onOk: () => { - duplicate(); - }, + onOk: handleDuplicate, }); - } + }, [handleDuplicate, t]); - function tryDelete() { + const tryDelete = useCallback(() => { showConfirmDialog({ confirmMessage: t("confirmRemove"), t, - onOk: () => { - remove(); - }, + onOk: handleRemove, }); - } - - async function remove() { - try { - setRemovingItem?.(true); - - await ConnectionProvider.getHandler().deleteObjects({ - model: currentModel!, - ids: selectedRowItems!.map((item) => item.id), - context: { ...parentContext }, - }); - - setCurrentId?.(undefined); - setCurrentItemIndex?.(undefined); - - searchTreeRef?.current?.refreshResults(); - } catch (e) { - showErrorDialog(e); - } finally { - setRemovingItem?.(false); - } - } - - async function duplicate() { - try { - setDuplicatingItem?.(true); - - const currentId = selectedRowItems![0].id; - - const newId = await ConnectionProvider.getHandler().duplicate({ - id: currentId, - model: currentModel!, - context: { ...parentContext }, - }); - - if (newId) { - searchTreeRef?.current?.refreshResults(); - } - } catch (e) { - showErrorDialog(e); - } finally { - setDuplicatingItem?.(false); - } - } - - const finalDomain = (() => { - const domain = searchTreeRef?.current?.getDomain(); - const finalValues = mergeParams(domain || [], searchParams || []); - return finalValues; - })(); + }, [handleRemove, t]); return ( @@ -206,38 +271,25 @@ function TreeActionBar(props: Props) { )} - {treeExpandable ? null : ( + {!treeExpandable && ( <> { - if (searchString && searchString.trim().length > 0) { - setSearchTreeNameSearch?.(searchString); - } else { - setSearchTreeNameSearch?.(undefined); - if (!isInfiniteTree) { - searchTreeRef?.current?.refreshResults(); - } - } - }} + onSearch={handleSearch} + /> + + } + tooltip={t("advanced_search")} + type={searchVisible ? "primary" : "default"} + onClick={() => setSearchVisible?.(!searchVisible)} + disabled={duplicatingItem || removingItem || treeIsLoading} + badgeNumber={searchParams?.length} /> - {!treeExpandable && ( - - } - tooltip={t("advanced_search")} - type={searchVisible ? "primary" : "default"} - onClick={() => { - setSearchVisible?.(!searchVisible); - }} - disabled={duplicatingItem || removingItem || treeIsLoading} - badgeNumber={searchParams?.length} - /> - )} 0) || treeIsLoading } - loading={false} - onClick={() => { - showLogInfo(currentModel!, selectedRowItems![0].id, t); - }} + onClick={() => showLogInfo(currentModel!, selectedRowItems![0].id, t)} /> } tooltip={t("refresh")} disabled={duplicatingItem || removingItem || treeIsLoading} - loading={false} - onClick={() => { - searchTreeRef?.current?.refreshResults(); - }} + onClick={handleRefresh} /> {!treeExpandable && ( <> @@ -291,10 +337,7 @@ function TreeActionBar(props: Props) { { - setPreviousView?.(currentView); - setCurrentView?.(newView); - }} + onChangeView={handleChangeView} previousView={previousView} disabled={treeIsLoading} /> @@ -308,29 +351,7 @@ function TreeActionBar(props: Props) { ( - - - - - - - )} - /> - } + icon={} onRetrieveData={async () => [ { label: t("export"), @@ -346,33 +367,7 @@ function TreeActionBar(props: Props) { ], }, ]} - onItemClick={(itemClicked: any) => { - if (itemClicked.id === "print_screen") { - let idsToExport = - selectedRowItems?.map((item) => item.id) || []; - - if (idsToExport.length === 0) { - idsToExport = results?.map((item) => item.id) || []; - } - - runAction( - { - id: -1, - model: currentModel, - report_name: "printscreen.list", - type: "ir.actions.report.xml", - datas: { - model: currentModel, - ids: idsToExport, - }, - }, - parentContext, - ); - return; - } - - setExportModalVisible(true); - }} + onItemClick={handleExportAction} disabled={ duplicatingItem || removingItem || treeIsLoading || hasNameSearch } @@ -394,4 +389,31 @@ function TreeActionBar(props: Props) { ); } +const TreeActionBar = memo(TreeActionBarComponent); export default TreeActionBar; + +const ExportIcon = memo(() => ( + ( + + + + + + + )} + /> +)); + +ExportIcon.displayName = "ExportIcon"; From 00810279660f877940e0b3547477b2299ee90278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= Date: Wed, 22 Jan 2025 10:59:23 +0100 Subject: [PATCH 04/21] feat: add new localization strings and improve One2many top bar https://github.com/gisce/webclient/issues/1646 - Added new localization strings for creating, searching, and navigating items in Catalan, English, and Spanish. - Enhanced the One2many input components to utilize the toolbar from the current view, improving user experience and consistency across the application. --- src/locales/ca_ES.ts | 6 + src/locales/en_US.ts | 6 + src/locales/es_ES.ts | 6 + src/widgets/base/one2many/One2manyInput.tsx | 1 + .../base/one2many/One2manyInputInfinite.tsx | 1 + src/widgets/base/one2many/One2manyTopBar.tsx | 228 +++++++++++------- 6 files changed, 164 insertions(+), 84 deletions(-) diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index 983500578..a7e24c088 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -109,4 +109,10 @@ export default { not: "No", loading: "Carregant...", pendingToCalculate: "Pendent de calcular", + createNewItem: "Crear nou element", + searchExistingItem: "Cercar element existent", + toggleViewMode: "Canviar mode de vista", + previousItem: "Element anterior", + nextItem: "Element següent", + unlink: "Desvincular", }; diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 8726291b1..345af3b6e 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -105,4 +105,10 @@ export default { not: "Not", loading: "Loading...", pendingToCalculate: "Pending to calculate", + createNewItem: "Create new item", + searchExistingItem: "Search existing item", + toggleViewMode: "Toggle view mode", + previousItem: "Previous item", + nextItem: "Next item", + unlink: "Unlink", }; diff --git a/src/locales/es_ES.ts b/src/locales/es_ES.ts index 951cd0209..291ce140d 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -111,4 +111,10 @@ export default { not: "No", loading: "Cargando...", pendingToCalculate: "Pendiente de calcular", + createNewItem: "Crear nuevo elemento", + searchExistingItem: "Buscar elemento existente", + toggleViewMode: "Cambiar modo de vista", + previousItem: "Elemento anterior", + nextItem: "Elemento siguiente", + unlink: "Desvincular", }; diff --git a/src/widgets/base/one2many/One2manyInput.tsx b/src/widgets/base/one2many/One2manyInput.tsx index b8493f3e9..8364b7a4b 100644 --- a/src/widgets/base/one2many/One2manyInput.tsx +++ b/src/widgets/base/one2many/One2manyInput.tsx @@ -693,6 +693,7 @@ const One2manyInput: React.FC = ( selectedRowKeys={selectedRowKeys} showCreateButton={views.get("form")?.fields !== undefined} showToggleButton={views.size > 1} + toolbar={views.get(currentView)?.toolbar} /> {content()} = ( selectedRowKeys={selectedRowKeys} showCreateButton={showCreateButton} showToggleButton={showToggleButton} + toolbar={views.get(currentView)?.toolbar} /> {currentView === "tree" && ( { +function One2manyTopBarComponent(props: One2manyTopBarProps) { const { title: titleString, readOnly, @@ -50,87 +52,17 @@ export const One2manyTopBar = (props: One2manyTopBarProps) => { showCreateButton, showToggleButton, } = props; - const { token } = useToken(); - - function separator() { - return
; - } - - function title() { - return ( -
-
- {titleString} -
-
- ); - } - - function deleteButton() { - return ( - - : } - onClick={onDelete} - danger={!isMany2Many} - type={isMany2Many ? "default" : "primary"} - disabled={ - totalItems === 0 || - readOnly || - (mode !== "form" && selectedRowKeys.length === 0) - } - /> - - ); - } - - function index() { - let itemToShow = "_"; - if (totalItems === 0) { - itemToShow = "_"; - } else { - itemToShow = (currentItemIndex + 1).toString(); - } - return ( - - ({itemToShow}/{totalItems}) - - ); - } - function itemBrowser() { - return ( - <> - {separator()} - } - onClick={onPreviousItem} - /> - {index()} - } - onClick={onNextItem} - /> - - ); - } + const { token } = useToken(); + const { t } = useLocale(); return (
- {title()} + <div className="flex-none h-8 pl-2"> {mode !== "graph" && showCreateButton && ( <ButtonWithTooltip - tooltip={"Create new item"} + tooltip={t("createNewItem")} icon={<FileAddOutlined />} disabled={readOnly} onClick={onCreateItem} @@ -138,22 +70,38 @@ export const One2manyTopBar = (props: One2manyTopBarProps) => { )} {isMany2Many && showCreateButton && ( <> - {separator()} + <Separator /> <ButtonWithTooltip - tooltip={"Search existing item"} + tooltip={t("searchExistingItem")} icon={<SearchOutlined />} disabled={readOnly} onClick={onSearchItem} /> </> )} - {mode !== "graph" && separator()} - {mode !== "graph" && deleteButton()} - {mode === "form" && itemBrowser()} - {separator()} + {mode !== "graph" && <Separator />} + {mode !== "graph" && ( + <DeleteButton + isMany2Many={isMany2Many} + totalItems={totalItems} + readOnly={readOnly} + mode={mode} + selectedRowKeys={selectedRowKeys} + onDelete={onDelete} + /> + )} + {mode === "form" && ( + <ItemBrowser + currentItemIndex={currentItemIndex} + totalItems={totalItems} + onPreviousItem={onPreviousItem} + onNextItem={onNextItem} + /> + )} + <Separator /> {showToggleButton && ( <ButtonWithTooltip - tooltip={"Toggle view mode"} + tooltip={t("toggleViewMode")} icon={<AlignLeftOutlined />} onClick={onToggleViewMode} /> @@ -161,4 +109,116 @@ export const One2manyTopBar = (props: One2manyTopBarProps) => { </div> </div> ); -}; +} + +const Title = memo(({ title, token }: { title: string; token: any }) => ( + <div + className="flex flex-grow h-8 text-white" + style={{ + borderRadius: token.borderRadius, + backgroundColor: token.colorPrimaryActive, + }} + > + <div className="flex flex-col items-center justify-center h-full"> + <span className="pl-2 font-bold">{title}</span> + </div> + </div> +)); +Title.displayName = "Title"; + +const Separator = memo(() => <div className="inline-block w-3" />); +Separator.displayName = "Separator"; + +const ItemIndex = memo( + ({ + currentItemIndex, + totalItems, + }: { + currentItemIndex: number; + totalItems: number; + }) => { + const itemToShow = + totalItems === 0 ? "_" : (currentItemIndex + 1).toString(); + return ( + <span className="pl-1 pr-1"> + ({itemToShow}/{totalItems}) + </span> + ); + }, +); +ItemIndex.displayName = "ItemIndex"; + +const ItemBrowser = memo( + ({ + currentItemIndex, + totalItems, + onPreviousItem, + onNextItem, + }: { + currentItemIndex: number; + totalItems: number; + onPreviousItem: () => void; + onNextItem: () => void; + }) => { + const { t } = useLocale(); + return ( + <> + <Separator /> + <ButtonWithTooltip + tooltip={t("previousItem")} + icon={<LeftOutlined />} + onClick={onPreviousItem} + /> + <ItemIndex + currentItemIndex={currentItemIndex} + totalItems={totalItems} + /> + <ButtonWithTooltip + tooltip={t("nextItem")} + icon={<RightOutlined />} + onClick={onNextItem} + /> + </> + ); + }, +); +ItemBrowser.displayName = "ItemBrowser"; + +const DeleteButton = memo( + ({ + isMany2Many, + totalItems, + readOnly, + mode, + selectedRowKeys, + onDelete, + }: { + isMany2Many: boolean; + totalItems: number; + readOnly: boolean; + mode: ViewType; + selectedRowKeys: string[]; + onDelete: () => void; + }) => { + const { t } = useLocale(); + return ( + <Badge count={selectedRowKeys.length}> + <ButtonWithTooltip + tooltip={isMany2Many ? t("unlink") : t("delete")} + icon={isMany2Many ? <ApiOutlined /> : <DeleteOutlined />} + onClick={onDelete} + danger={!isMany2Many} + type={isMany2Many ? "default" : "primary"} + disabled={ + totalItems === 0 || + readOnly || + (mode !== "form" && selectedRowKeys.length === 0) + } + /> + </Badge> + ); + }, +); +DeleteButton.displayName = "DeleteButton"; + +export const One2manyTopBar = memo(One2manyTopBarComponent); From 4d82eacefa3b682de7f1a9536fb8ba4a0caafccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Wed, 22 Jan 2025 13:11:24 +0100 Subject: [PATCH 05/21] feat: improve form handling and toolbar integration in One2many components https://github.com/gisce/webclient/issues/1646 - Added formRef to improve form submission handling in the FormActionBar and useFormToolbarButtons hooks. - Refactored One2manyForm to support forwarding refs, enabling better integration with form functionalities. - Updated One2manyInput and One2manyInputInfinite to utilize formRef for improved form management. - Enhanced One2manyTopBar to integrate formRef and streamline toolbar button actions. These changes improve the overall user experience and maintainability of the One2many components. --- src/actionbar/FormActionBar.tsx | 1 + src/hooks/useFormToolbarButtons.ts | 22 +++-- src/widgets/base/one2many/One2manyForm.tsx | 95 ++++++++++--------- src/widgets/base/one2many/One2manyInput.tsx | 4 + .../base/one2many/One2manyInputInfinite.tsx | 4 + src/widgets/base/one2many/One2manyTopBar.tsx | 53 ++++++++++- 6 files changed, 124 insertions(+), 55 deletions(-) diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index f0db3373a..b4c4660df 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -82,6 +82,7 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { useFormToolbarButtons({ toolbar, mustDisableButtons, + formRef, }); const tryAction = useCallback( diff --git a/src/hooks/useFormToolbarButtons.ts b/src/hooks/useFormToolbarButtons.ts index 7844b90aa..0975b66de 100644 --- a/src/hooks/useFormToolbarButtons.ts +++ b/src/hooks/useFormToolbarButtons.ts @@ -1,6 +1,5 @@ -import { useCallback, useContext } from "react"; +import { useCallback, useContext, RefObject } from "react"; import { useLocale } from "@gisce/react-formiga-components"; -import { useActionViewContext } from "@/context/ActionViewContext"; import { ContentRootContext, ContentRootContextType, @@ -13,6 +12,7 @@ import { interface UseFormToolbarButtonsProps { toolbar: any; mustDisableButtons?: boolean; + formRef: RefObject<any>; } interface SaveDocumentResult { @@ -23,6 +23,7 @@ interface SaveDocumentResult { export const useFormToolbarButtons = ({ toolbar, mustDisableButtons = false, + formRef, }: UseFormToolbarButtonsProps) => { const { t } = useLocale(); const contentRootContext = useContext( @@ -32,18 +33,21 @@ export const useFormToolbarButtons = ({ TabManagerContext, ) as TabManagerContextType; - const { formRef, onFormSave } = useActionViewContext(); const { processAction } = contentRootContext || {}; const { openRelate } = tabManagerContext || {}; + const onFormSave = useCallback(async () => { + return await formRef.current?.submitForm(); + }, [formRef]); + const runAction = useCallback( (actionData: any) => { processAction?.({ actionData, - values: (formRef.current as any).getValues(), - fields: (formRef.current as any).getFields(), - context: (formRef.current as any).getContext(), - onRefreshParentValues: () => (formRef.current as any).fetchValues(), + values: formRef.current?.getValues(), + fields: formRef.current?.getFields(), + context: formRef.current?.getContext(), + onRefreshParentValues: () => formRef.current?.fetchValues(), }); }, [formRef, processAction], @@ -97,8 +101,8 @@ export const useFormToolbarButtons = ({ if (result.succeed) { openRelate({ relateData: relate, - values: (formRef.current as any).getValues(), - fields: (formRef.current as any).getFields(), + values: formRef.current?.getValues(), + fields: formRef.current?.getFields(), action_id: relate.id, action_type: relate.type, }); diff --git a/src/widgets/base/one2many/One2manyForm.tsx b/src/widgets/base/one2many/One2manyForm.tsx index b9a07f9cb..f93a91e72 100644 --- a/src/widgets/base/one2many/One2manyForm.tsx +++ b/src/widgets/base/one2many/One2manyForm.tsx @@ -4,7 +4,7 @@ import { One2manyContext, One2manyContextType, } from "@/context/One2manyContext"; -import { useContext } from "react"; +import { useContext, forwardRef } from "react"; import { One2manyItem } from "./One2manyInput"; import { filterDuplicateItems } from "@/helpers/one2manyHelper"; import { useLocale } from "@gisce/react-formiga-components"; @@ -18,51 +18,58 @@ export type One2manyFormProps = { onChange: (items: One2manyItem[]) => void; }; -export const One2manyForm = ({ - formView, - items, - context, - relation, - readOnly, - onChange, -}: One2manyFormProps) => { - const { itemIndex } = useContext(One2manyContext) as One2manyContextType; +export const One2manyForm = forwardRef( + ( + { + formView, + items, + context, + relation, + readOnly, + onChange, + }: One2manyFormProps, + ref, + ) => { + const { itemIndex } = useContext(One2manyContext) as One2manyContextType; + const { t } = useLocale(); - const { t } = useLocale(); + if (items.length === 0) { + return t("noCurrentEntries"); + } - if (items.length === 0) { - return t("noCurrentEntries"); - } + return ( + <Form + ref={ref} + formView={formView} + values={items[itemIndex]?.values} + parentContext={context} + model={relation} + id={items[itemIndex]?.id} + submitMode={"values"} + onFieldsChange={(values: any) => { + const currentItemId = items[itemIndex]?.id; - return ( - <Form - formView={formView} - values={items[itemIndex]?.values} - parentContext={context} - model={relation} - id={items[itemIndex]?.id} - submitMode={"values"} - onFieldsChange={(values: any) => { - const currentItemId = items[itemIndex]?.id; + const updatedItems = items.map((item) => { + if (item.id === currentItemId) { + return { + ...item, + operation: + item.operation === "original" + ? "pendingUpdate" + : item.operation, + values: { ...values, id: currentItemId }, + treeValues: { ...values, id: currentItemId }, + }; + } + return item; + }); - const updatedItems = items.map((item) => { - if (item.id === currentItemId) { - return { - ...item, - operation: - item.operation === "original" - ? "pendingUpdate" - : item.operation, - values: { ...values, id: currentItemId }, - treeValues: { ...values, id: currentItemId }, - }; - } - return item; - }); + onChange(filterDuplicateItems(updatedItems)); + }} + readOnly={readOnly} + /> + ); + }, +); - onChange(filterDuplicateItems(updatedItems)); - }} - readOnly={readOnly} - /> - ); -}; +One2manyForm.displayName = "One2manyForm"; diff --git a/src/widgets/base/one2many/One2manyInput.tsx b/src/widgets/base/one2many/One2manyInput.tsx index 8364b7a4b..2a7e1804f 100644 --- a/src/widgets/base/one2many/One2manyInput.tsx +++ b/src/widgets/base/one2many/One2manyInput.tsx @@ -107,6 +107,7 @@ const One2manyInput: React.FC<One2manyInputProps> = ( const [sorter, setSorter] = useState<any>(); const originalSortItemIds = useRef<number[]>(); const [colorsForResults, setColorsForResults] = useState<any>(undefined); + const formRef = useRef<any>(); const { readOnly, @@ -573,6 +574,7 @@ const One2manyInput: React.FC<One2manyInputProps> = ( return ( <Form + ref={formRef} formView={views.get("form")} values={itemsToShow[itemIndex]?.values} parentContext={{ ...getContext?.(), ...context }} @@ -694,6 +696,8 @@ const One2manyInput: React.FC<One2manyInputProps> = ( showCreateButton={views.get("form")?.fields !== undefined} showToggleButton={views.size > 1} toolbar={views.get(currentView)?.toolbar} + context={{ ...getContext?.(), ...context }} + formRef={formRef} /> {content()} <FormModal diff --git a/src/widgets/base/one2many/One2manyInputInfinite.tsx b/src/widgets/base/one2many/One2manyInputInfinite.tsx index fa15c4072..e99826602 100644 --- a/src/widgets/base/one2many/One2manyInputInfinite.tsx +++ b/src/widgets/base/one2many/One2manyInputInfinite.tsx @@ -72,6 +72,7 @@ export const One2manyInput: React.FC<One2manyInputInfiniteProps> = ( getContext, fetchValues: fetchParentFormValues, } = formContext || {}; + const formRef = useRef<any>(); const showToggleButton = views.size > 1; const showCreateButton = views.get("form")?.fields !== undefined; @@ -265,6 +266,8 @@ export const One2manyInput: React.FC<One2manyInputInfiniteProps> = ( showCreateButton={showCreateButton} showToggleButton={showToggleButton} toolbar={views.get(currentView)?.toolbar} + context={{ ...getContext?.(), ...context }} + formRef={formRef} /> {currentView === "tree" && ( <One2manyTree @@ -293,6 +296,7 @@ export const One2manyInput: React.FC<One2manyInputInfiniteProps> = ( )} {currentView === "form" && ( <One2manyForm + ref={formRef} items={items} formView={views.get("form")} context={context} diff --git a/src/widgets/base/one2many/One2manyTopBar.tsx b/src/widgets/base/one2many/One2manyTopBar.tsx index a4edc45e7..ed73026c3 100644 --- a/src/widgets/base/one2many/One2manyTopBar.tsx +++ b/src/widgets/base/one2many/One2manyTopBar.tsx @@ -1,6 +1,6 @@ -import { memo } from "react"; +import { memo, RefObject } from "react"; import ButtonWithTooltip from "@/common/ButtonWithTooltip"; -import { useLocale } from "@gisce/react-formiga-components"; +import { useLocale, DropdownButton } from "@gisce/react-formiga-components"; import { FileAddOutlined, DeleteOutlined, @@ -9,9 +9,14 @@ import { AlignLeftOutlined, SearchOutlined, ApiOutlined, + ThunderboltOutlined, + PrinterOutlined, + EnterOutlined, } from "@ant-design/icons"; import { ViewType } from "@/types"; import { theme, Badge } from "antd"; +import { useFormToolbarButtons } from "@/hooks/useFormToolbarButtons"; +import { useTreeToolbarButtons } from "@/hooks/useTreeToolbarButtons"; const { useToken } = theme; type One2manyTopBarProps = { @@ -32,6 +37,8 @@ type One2manyTopBarProps = { showToggleButton: boolean; showCreateButton: boolean; toolbar?: any; + context?: any; + formRef: RefObject<any>; }; function One2manyTopBarComponent(props: One2manyTopBarProps) { @@ -51,11 +58,30 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { selectedRowKeys, showCreateButton, showToggleButton, + toolbar, + context, + formRef, } = props; const { token } = useToken(); const { t } = useLocale(); + const { actionButtonProps, printButtonProps, relateButtonProps } = + useFormToolbarButtons({ + toolbar, + mustDisableButtons: readOnly, + formRef, + }); + + const { + actionButtonProps: treeActionButtonProps, + printButtonProps: treePrintButtonProps, + } = useTreeToolbarButtons({ + toolbar, + disabled: readOnly, + parentContext: context, + }); + return ( <div className="flex mb-2"> <Title title={titleString} token={token} /> @@ -106,6 +132,29 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { onClick={onToggleViewMode} /> )} + {toolbar && ( + <> + <Separator /> + <DropdownButton + icon={<ThunderboltOutlined />} + {...(mode === "form" ? actionButtonProps : treeActionButtonProps)} + /> + <Separator /> + <DropdownButton + icon={<PrinterOutlined />} + {...(mode === "form" ? printButtonProps : treePrintButtonProps)} + /> + {mode === "form" && ( + <> + <Separator /> + <DropdownButton + icon={<EnterOutlined />} + {...relateButtonProps} + /> + </> + )} + </> + )} </div> </div> ); From 0419afe1f03a80878b339fc65029e11d75f31d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Thu, 23 Jan 2025 16:42:03 +0100 Subject: [PATCH 06/21] feat: allow use of hooks from one2many's --- src/actionbar/FormActionBar.tsx | 23 ++++++++-------- src/actionbar/TreeActionBar.tsx | 15 +++++++---- src/context/ContentRootContext.tsx | 4 ++- src/hooks/useFormToolbarButtons.ts | 6 +++-- src/hooks/useTreeToolbarButtons.ts | 26 ++++++++++++------- src/widgets/base/one2many/One2manyInput.tsx | 3 +++ .../base/one2many/One2manyInputInfinite.tsx | 3 +++ src/widgets/base/one2many/One2manyTopBar.tsx | 5 ++++ 8 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index b4c4660df..3fd803c31 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -78,13 +78,6 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { [formIsSaving, removingItem, formIsLoading, duplicatingItem], ); - const { actionButtonProps, printButtonProps, relateButtonProps } = - useFormToolbarButtons({ - toolbar, - mustDisableButtons, - formRef, - }); - const tryAction = useCallback( (action: () => void) => { if (formHasChanges) { @@ -96,6 +89,18 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { [formHasChanges, t], ); + const handleRefresh = useCallback(() => { + tryAction(() => (formRef.current as any).fetchValues()); + }, [tryAction, formRef]); + + const { actionButtonProps, printButtonProps, relateButtonProps } = + useFormToolbarButtons({ + toolbar, + mustDisableButtons, + formRef, + onRefreshParentValues: handleRefresh, + }); + const handleRemove = useCallback(async () => { try { setRemovingItem?.(true); @@ -162,10 +167,6 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { [currentView, setPreviousView, setFormHasChanges, setCurrentView], ); - const handleRefresh = useCallback(() => { - tryAction(() => (formRef.current as any).fetchValues()); - }, [tryAction, formRef]); - const handleAddNewAttachment = useCallback(async () => { const result = await saveDocument({ onFormSave }); if (result.succeed) { diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index 14ec8ddc8..02e5b772f 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -88,13 +88,22 @@ function TreeActionBarComponent({ const [exportModalVisible, setExportModalVisible] = useState(false); const isFirstMount = useRef(true); + const handleRefresh = useCallback(() => { + searchTreeRef?.current?.refreshResults(); + }, [searchTreeRef]); + const { actionButtonProps, printButtonProps } = useTreeToolbarButtons({ toolbar, disabled: treeIsLoading, parentContext, + selectedRowItems, + onRefreshParentValues: handleRefresh, }); - const runAction = useRunTreeAction(); + const runAction = useRunTreeAction({ + selectedRowItems, + onRefreshParentValues: handleRefresh, + }); const hasNameSearch = useMemo( () => @@ -167,10 +176,6 @@ function TreeActionBarComponent({ [currentView, setPreviousView, setCurrentView], ); - const handleRefresh = useCallback(() => { - searchTreeRef?.current?.refreshResults(); - }, [searchTreeRef]); - const handleSearch = useCallback( (searchString?: string) => { if (searchString && searchString.trim().length > 0) { diff --git a/src/context/ContentRootContext.tsx b/src/context/ContentRootContext.tsx index 985059f45..37bc69c51 100644 --- a/src/context/ContentRootContext.tsx +++ b/src/context/ContentRootContext.tsx @@ -172,7 +172,9 @@ const ContentRootProvider = ( onRefreshParentValues?: any; }) { const { type } = actionData; - onRefreshParentValues.current.push(onRefreshParentValuesFn); + if (onRefreshParentValuesFn) { + onRefreshParentValues.current.push(onRefreshParentValuesFn); + } if (type === "ir.actions.report.xml") { return await generateReport({ diff --git a/src/hooks/useFormToolbarButtons.ts b/src/hooks/useFormToolbarButtons.ts index 0975b66de..f5892a226 100644 --- a/src/hooks/useFormToolbarButtons.ts +++ b/src/hooks/useFormToolbarButtons.ts @@ -13,6 +13,7 @@ interface UseFormToolbarButtonsProps { toolbar: any; mustDisableButtons?: boolean; formRef: RefObject<any>; + onRefreshParentValues?: () => void; } interface SaveDocumentResult { @@ -24,6 +25,7 @@ export const useFormToolbarButtons = ({ toolbar, mustDisableButtons = false, formRef, + onRefreshParentValues, }: UseFormToolbarButtonsProps) => { const { t } = useLocale(); const contentRootContext = useContext( @@ -47,10 +49,10 @@ export const useFormToolbarButtons = ({ values: formRef.current?.getValues(), fields: formRef.current?.getFields(), context: formRef.current?.getContext(), - onRefreshParentValues: () => formRef.current?.fetchValues(), + onRefreshParentValues, }); }, - [formRef, processAction], + [formRef, processAction, onRefreshParentValues], ); const actionButtonProps = { diff --git a/src/hooks/useTreeToolbarButtons.ts b/src/hooks/useTreeToolbarButtons.ts index 407e141d2..7343620f3 100644 --- a/src/hooks/useTreeToolbarButtons.ts +++ b/src/hooks/useTreeToolbarButtons.ts @@ -1,6 +1,5 @@ import { useCallback, useContext } from "react"; import { useLocale } from "@gisce/react-formiga-components"; -import { useActionViewContext } from "@/context/ActionViewContext"; import { ContentRootContext, ContentRootContextType, @@ -10,14 +9,21 @@ interface UseTreeToolbarButtonsProps { toolbar: any; disabled?: boolean; parentContext?: any; + selectedRowItems?: any[]; + onRefreshParentValues?: () => void; } -export const useRunTreeAction = () => { +export const useRunTreeAction = ({ + selectedRowItems, + onRefreshParentValues, +}: { + selectedRowItems?: any[]; + onRefreshParentValues?: () => void; +}) => { const contentRootContext = useContext( ContentRootContext, ) as ContentRootContextType; const { processAction } = contentRootContext || {}; - const { selectedRowItems, searchTreeRef } = useActionViewContext(); return useCallback( (actionData: any, context: any = {}) => { @@ -33,12 +39,10 @@ export const useRunTreeAction = () => { active_id: selectedRowItems?.map((item) => item.id)[0], active_ids: selectedRowItems?.map((item) => item.id), }, - onRefreshParentValues: () => { - searchTreeRef?.current?.refreshResults(); - }, + onRefreshParentValues, }); }, - [processAction, selectedRowItems, searchTreeRef], + [processAction, selectedRowItems, onRefreshParentValues], ); }; @@ -46,10 +50,14 @@ export const useTreeToolbarButtons = ({ toolbar, disabled = false, parentContext = {}, + selectedRowItems = [], + onRefreshParentValues, }: UseTreeToolbarButtonsProps) => { const { t } = useLocale(); - const { selectedRowItems } = useActionViewContext(); - const runAction = useRunTreeAction(); + const runAction = useRunTreeAction({ + selectedRowItems, + onRefreshParentValues, + }); const actionButtonProps = { placement: "bottomRight" as const, diff --git a/src/widgets/base/one2many/One2manyInput.tsx b/src/widgets/base/one2many/One2manyInput.tsx index 2a7e1804f..ab98a8534 100644 --- a/src/widgets/base/one2many/One2manyInput.tsx +++ b/src/widgets/base/one2many/One2manyInput.tsx @@ -698,6 +698,9 @@ const One2manyInput: React.FC<One2manyInputProps> = ( toolbar={views.get(currentView)?.toolbar} context={{ ...getContext?.(), ...context }} formRef={formRef} + onRefreshParentValues={() => { + fetchParentFormValues?.({ forceRefresh: true }); + }} /> {content()} <FormModal diff --git a/src/widgets/base/one2many/One2manyInputInfinite.tsx b/src/widgets/base/one2many/One2manyInputInfinite.tsx index e99826602..f3142be84 100644 --- a/src/widgets/base/one2many/One2manyInputInfinite.tsx +++ b/src/widgets/base/one2many/One2manyInputInfinite.tsx @@ -268,6 +268,9 @@ export const One2manyInput: React.FC<One2manyInputInfiniteProps> = ( toolbar={views.get(currentView)?.toolbar} context={{ ...getContext?.(), ...context }} formRef={formRef} + onRefreshParentValues={() => { + fetchParentFormValues?.({ forceRefresh: true }); + }} /> {currentView === "tree" && ( <One2manyTree diff --git a/src/widgets/base/one2many/One2manyTopBar.tsx b/src/widgets/base/one2many/One2manyTopBar.tsx index ed73026c3..9fdaa96cf 100644 --- a/src/widgets/base/one2many/One2manyTopBar.tsx +++ b/src/widgets/base/one2many/One2manyTopBar.tsx @@ -39,6 +39,7 @@ type One2manyTopBarProps = { toolbar?: any; context?: any; formRef: RefObject<any>; + onRefreshParentValues?: () => void; }; function One2manyTopBarComponent(props: One2manyTopBarProps) { @@ -61,6 +62,7 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { toolbar, context, formRef, + onRefreshParentValues, } = props; const { token } = useToken(); @@ -71,6 +73,7 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { toolbar, mustDisableButtons: readOnly, formRef, + onRefreshParentValues, }); const { @@ -80,6 +83,8 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { toolbar, disabled: readOnly, parentContext: context, + selectedRowItems: selectedRowKeys.map((key) => ({ id: key })), + onRefreshParentValues, }); return ( From b91ffeb018d1cca3501a18fe3728d0e84f6cac51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Thu, 23 Jan 2025 16:57:19 +0100 Subject: [PATCH 07/21] Merge branch 'v2' into feat/add-toolbar-buttons-to-x2many --- src/actionbar/DashboardActionBar.tsx | 3 +++ src/locales/ca_ES.ts | 3 +++ src/locales/en_US.ts | 3 +++ src/locales/es_ES.ts | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/actionbar/DashboardActionBar.tsx b/src/actionbar/DashboardActionBar.tsx index ad5414576..6209375de 100644 --- a/src/actionbar/DashboardActionBar.tsx +++ b/src/actionbar/DashboardActionBar.tsx @@ -12,6 +12,7 @@ import { } from "@ant-design/icons"; import { useLocale } from "@gisce/react-formiga-components"; import { ActionBarSeparator } from "./ActionBarSeparator"; +import { ShareUrlButton } from "./ShareUrlButton"; function DashboardActionBar() { const { isLoading, dashboardRef, moveItemsEnabled, setMoveItemsEnabled } = @@ -53,6 +54,8 @@ function DashboardActionBar() { dashboardRef?.current.refresh(); }} /> + <ActionBarSeparator /> + <ShareUrlButton /> </Space> ); } diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index a7e24c088..447bd8d51 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -115,4 +115,7 @@ export default { previousItem: "Element anterior", nextItem: "Element següent", unlink: "Desvincular", + share: "Compartir URL", + copyToClipboard: "Copiar al porta-retalls", + urlCopiedToClipboard: "URL copiada al porta-retalls", }; diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 345af3b6e..86572832a 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -111,4 +111,7 @@ export default { previousItem: "Previous item", nextItem: "Next item", unlink: "Unlink", + share: "Compartir URL", + urlCopiedToClipboard: "URL copied to clipboard", + copyToClipboard: "Copy to clipboard", }; diff --git a/src/locales/es_ES.ts b/src/locales/es_ES.ts index 291ce140d..08c12144e 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -117,4 +117,7 @@ export default { previousItem: "Elemento anterior", nextItem: "Elemento siguiente", unlink: "Desvincular", + share: "Compartir URL", + urlCopiedToClipboard: "URL copiada al portapapeles", + copyToClipboard: "Copiar al portapapeles", }; From 6c0976970cffd19b0f16ba316b0aeabd2edde6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Thu, 23 Jan 2025 17:03:12 +0100 Subject: [PATCH 08/21] fix: bad conflict resolution --- src/actionbar/GraphActionBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actionbar/GraphActionBar.tsx b/src/actionbar/GraphActionBar.tsx index 9a9e44c94..50db7af55 100644 --- a/src/actionbar/GraphActionBar.tsx +++ b/src/actionbar/GraphActionBar.tsx @@ -11,7 +11,7 @@ import ButtonWithBadge from "./ButtonWithBadge"; import { ReloadOutlined, FilterOutlined } from "@ant-design/icons"; import { View } from "@/types"; import { ShareUrlButton } from "./ShareUrlButton"; -import { ActionBarSeparator } from "./FormActionBar"; +import { ActionBarSeparator } from "./ActionBarSeparator"; function GraphActionBar({ refreshGraph }: { refreshGraph: () => void }) { const { t } = useLocale(); From b9f3d3066f8c694b00502d665cac16fe2f7cc5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Thu, 23 Jan 2025 17:22:26 +0100 Subject: [PATCH 09/21] fix: adjust sharebutton for formactionbar --- src/actionbar/FormActionBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index 6ebde27af..52b3d0924 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -326,7 +326,7 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { onViewAttachmentDetails={handleViewAttachmentDetails} /> <ActionBarSeparator /> - <ShareUrlButton /> + <ShareUrlButton res_id={currentId} /> </Space> ); } From c68ec432113c6cd85931ee2b81b43327dbbb91c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Fri, 24 Jan 2025 09:24:31 +0100 Subject: [PATCH 10/21] fix: remove unused function --- src/views/ActionView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/ActionView.tsx b/src/views/ActionView.tsx index 8d59d4db4..bdccc7bf3 100644 --- a/src/views/ActionView.tsx +++ b/src/views/ActionView.tsx @@ -419,8 +419,6 @@ function ActionView(props: Props, ref: any) { } } - function content() {} - function onNewClicked() { if (currentId === undefined && currentView!.type === "form") { (formRef.current as any).clearAndReload(); From 9fd9d6a01c396f5a390140a958a280c334286c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Fri, 24 Jan 2025 16:23:24 +0100 Subject: [PATCH 11/21] fix: adjust value key filtering in URL sharing - Added "parent_id" to the allowed values keys in URL parameters - Updated useUrlFromCurrentTab to include form values in the actionRawData. - Modified RootView to merge filtered values with global values - Ensured getPlainValues in Form handles potential null values for getValues and getFields. --- src/helpers/shareUrlHelper.ts | 2 +- src/hooks/useUrlFromCurrentTab.ts | 10 +++++++++- src/views/RootView.tsx | 11 ++++++----- src/widgets/views/Form.tsx | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/helpers/shareUrlHelper.ts b/src/helpers/shareUrlHelper.ts index 80d88ed95..75bb60b7e 100644 --- a/src/helpers/shareUrlHelper.ts +++ b/src/helpers/shareUrlHelper.ts @@ -2,7 +2,7 @@ import { ActionInfo, ActionRawData } from "@/types"; const OPEN_ACTION_PATH = "action"; // Parameters to exclude from the URL -const ALLOWED_VALUES_KEYS = ["active_id", "active_ids", "id"]; +const ALLOWED_VALUES_KEYS = ["active_id", "active_ids", "id", "parent_id"]; const IGNORED_PARAMS = ["target", "context", "domain", "fields"]; export const createShareOpenUrl = (action: ActionInfo) => { diff --git a/src/hooks/useUrlFromCurrentTab.ts b/src/hooks/useUrlFromCurrentTab.ts index 099e0d8ca..2d1b2437f 100644 --- a/src/hooks/useUrlFromCurrentTab.ts +++ b/src/hooks/useUrlFromCurrentTab.ts @@ -13,7 +13,8 @@ export function useUrlFromCurrentTab({ }: { currentTab?: Tab; }): UseUrlFromCurrentTabResult { - const { currentView, searchParams, currentId } = useActionViewContext(); + const { currentView, searchParams, currentId, formRef } = + useActionViewContext(); const { currentTab: currentTabContext } = useTabs(); const currentTab = currentTabProps || currentTabContext; @@ -33,6 +34,13 @@ export function useUrlFromCurrentTab({ ...(initialView && { initialView }), ...(searchParams && { searchParams }), ...(currentId && { res_id: currentId }), + actionRawData: { + ...currentTab.action.actionRawData, + values: { + ...currentTab.action.values, + ...(formRef.current?.getPlainValues() || {}), + }, + }, }; const shareUrl = createShareOpenUrl(finalActionData); diff --git a/src/views/RootView.tsx b/src/views/RootView.tsx index 45f556350..0f6e4a51a 100644 --- a/src/views/RootView.tsx +++ b/src/views/RootView.tsx @@ -83,9 +83,10 @@ function RootView(props: RootViewProps, ref: any) { context: rootContext, }); - let values: Record<string, any> = filterAllowedValues( - actionRawData?.values, - ); + let values: Record<string, any> = { + ...(filterAllowedValues(actionRawData?.values) || {}), + ...globalValues, + }; const finalIdToRead: number | undefined = res_id || values.active_id || values.id; @@ -112,7 +113,7 @@ function RootView(props: RootViewProps, ref: any) { parseContext({ context: actionRawData.context, fields, - values: { ...globalValues, ...(values || {}) }, + values, }); } else { parsedContext = {}; @@ -133,7 +134,7 @@ function RootView(props: RootViewProps, ref: any) { ) { return await ConnectionProvider.getHandler().evalDomain({ domain: actionRawData.domain, - values: { ...(values || {}), ...globalValues }, + values, context: { ...rootContext, ...parsedContext }, fields, }); diff --git a/src/widgets/views/Form.tsx b/src/widgets/views/Form.tsx index 705802729..387b26fcd 100644 --- a/src/widgets/views/Form.tsx +++ b/src/widgets/views/Form.tsx @@ -287,8 +287,8 @@ function Form(props: FormProps, ref: any) { } function getPlainValues() { - const values: any = getValues(); - const fields: any = getFields(); + const values: any = getValues() || {}; + const fields: any = getFields() || {}; const reformattedValues: { [key: string]: any } = {}; Object.keys(values).forEach((key) => { From 1b5d7dd4b6b5476af5a4ba1d184a1fb161a89876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Fri, 24 Jan 2025 16:55:06 +0100 Subject: [PATCH 12/21] fix: improve value merging logic in useUrlFromCurrentTab - Updated the logic to prioritize form values over action values for non-tree views. - For tree views with active_ids, merged form values with action values. - Simplified the final action data structure by directly assigning merged values. --- src/hooks/useUrlFromCurrentTab.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/hooks/useUrlFromCurrentTab.ts b/src/hooks/useUrlFromCurrentTab.ts index 2d1b2437f..dc00fbd8d 100644 --- a/src/hooks/useUrlFromCurrentTab.ts +++ b/src/hooks/useUrlFromCurrentTab.ts @@ -29,6 +29,24 @@ export function useUrlFromCurrentTab({ }; const { action_id } = currentTab.action; + + let finalValues = currentTab.action.values || {}; + const formValues = formRef.current?.getPlainValues() || {}; + + // For tree view with active_ids, action values have priority over form values + if (initialView.type === "tree" && finalValues.active_ids) { + finalValues = { + ...formValues, + ...finalValues, + }; + } else { + // For all other cases, form values (if any) have priority over action values + finalValues = { + ...finalValues, + ...formValues, + }; + } + const finalActionData: ActionInfo = { ...currentTab.action, ...(initialView && { initialView }), @@ -36,10 +54,7 @@ export function useUrlFromCurrentTab({ ...(currentId && { res_id: currentId }), actionRawData: { ...currentTab.action.actionRawData, - values: { - ...currentTab.action.values, - ...(formRef.current?.getPlainValues() || {}), - }, + values: finalValues, }, }; From c79c3e0f124142cbee88a75dab8dbbcc3382fc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Sat, 25 Jan 2025 11:53:45 +0100 Subject: [PATCH 13/21] fix: improve and export parameter filtering --- src/helpers/shareUrlHelper.ts | 35 +++++++++++++++++++++++++++-------- src/index.ts | 3 +++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/helpers/shareUrlHelper.ts b/src/helpers/shareUrlHelper.ts index 75bb60b7e..a80f44dc7 100644 --- a/src/helpers/shareUrlHelper.ts +++ b/src/helpers/shareUrlHelper.ts @@ -3,7 +3,18 @@ import { ActionInfo, ActionRawData } from "@/types"; const OPEN_ACTION_PATH = "action"; // Parameters to exclude from the URL const ALLOWED_VALUES_KEYS = ["active_id", "active_ids", "id", "parent_id"]; -const IGNORED_PARAMS = ["target", "context", "domain", "fields"]; +const ALLOWED_PARAMETERS = [ + "model", + "views", + "title", + "initialView", + "action_id", + "action_type", + "res_id", + "limit", + "actionRawData", + "searchParams", +]; export const createShareOpenUrl = (action: ActionInfo) => { const url = new URL(window.location.origin); @@ -15,13 +26,10 @@ export const createShareOpenUrl = (action: ActionInfo) => { action?.actionRawData && filterActionRawData(action.actionRawData), }; - // Add all non-null properties from action to URL - Object.entries(finalAction).forEach(([key, value]) => { - if ( - !IGNORED_PARAMS.includes(key) && - value && - (!Array.isArray(value) || value.length > 0) - ) { + // Filter allowed parameters and add them to URL + const allowedParams = filterAllowedParameters(finalAction); + Object.entries(allowedParams).forEach(([key, value]) => { + if (value && (!Array.isArray(value) || value.length > 0)) { url.searchParams.set(key, convertToString(value)); } }); @@ -84,6 +92,17 @@ const filterActionRawData = (actionRawData: ActionRawData) => { return Object.keys(filteredData).length > 0 ? filteredData : undefined; }; +export const filterAllowedParameters = (parameters: any) => { + if (!parameters || typeof parameters !== "object") { + return {}; + } + return Object.fromEntries( + Object.entries(parameters).filter(([key]) => + ALLOWED_PARAMETERS.includes(key), + ), + ); +}; + export const filterAllowedValues = (values: any) => { if (!values || typeof values !== "object") { return {}; diff --git a/src/index.ts b/src/index.ts index 4b2071491..c7777c069 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,6 +91,7 @@ import { ErpAllFeatureKeys, ErpFeatureKeys } from "./models/erpFeature"; import type { ErpFeaturesMap } from "./models/erpFeature"; import { GraphCard } from "./widgets/views/Graph"; import dayjs from "./helpers/dayjs"; +import { filterAllowedParameters , filterAllowedValues } from "./helpers/shareUrlHelper"; export { Button, @@ -183,4 +184,6 @@ export { Spinner, Carousel, ColorPicker, + filterAllowedParameters, + filterAllowedValues, }; From 28fff0ae1917962fec186f86124b74ccc5494074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Sat, 25 Jan 2025 18:30:18 +0100 Subject: [PATCH 14/21] fix: transform initialView to initialViewId --- src/helpers/shareUrlHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/helpers/shareUrlHelper.ts b/src/helpers/shareUrlHelper.ts index a80f44dc7..ddf24431a 100644 --- a/src/helpers/shareUrlHelper.ts +++ b/src/helpers/shareUrlHelper.ts @@ -7,7 +7,7 @@ const ALLOWED_PARAMETERS = [ "model", "views", "title", - "initialView", + "initialViewId", "action_id", "action_type", "res_id", @@ -34,6 +34,9 @@ export const createShareOpenUrl = (action: ActionInfo) => { } }); + action.initialView?.id && + url.searchParams.set("initialViewId", action.initialView?.id.toString()); + return url.toString(); }; From bf072e59a9f8bd86a0a8c9d239c8757b5d46af60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Sun, 26 Jan 2025 09:08:05 +0100 Subject: [PATCH 15/21] fix: also clear url when closing all tabs --- src/context/TabManagerContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/context/TabManagerContext.tsx b/src/context/TabManagerContext.tsx index 8a9564653..58c30b789 100644 --- a/src/context/TabManagerContext.tsx +++ b/src/context/TabManagerContext.tsx @@ -79,6 +79,7 @@ const TabManagerProvider = (props: TabManagerProviderProps): any => { useEffect(() => { if (noTabs) { document.title = title; + window.history.replaceState({}, "", "/"); } }, [noTabs, title]); From b46cf2f19f50476393d070b55f88b1fe304fad75 Mon Sep 17 00:00:00 2001 From: semantic-release-bot <semantic-release-bot@martynus.net> Date: Tue, 28 Jan 2025 10:50:15 +0000 Subject: [PATCH 16/21] chore(release): 2.58.1 [skip ci] ## [2.58.1](https://github.com/gisce/react-ooui/compare/v2.58.0...v2.58.1) (2025-01-28) ### Bug Fixes * adjust value key filtering in URL sharing ([9fd9d6a](https://github.com/gisce/react-ooui/commit/9fd9d6a01c396f5a390140a958a280c334286c88)) * also clear url when closing all tabs ([bf072e5](https://github.com/gisce/react-ooui/commit/bf072e59a9f8bd86a0a8c9d239c8757b5d46af60)) * clear title when no tabs ([68d42f0](https://github.com/gisce/react-ooui/commit/68d42f0020d880f74e8f91254fe5566f67180b3d)) * improve and export parameter filtering ([c79c3e0](https://github.com/gisce/react-ooui/commit/c79c3e0f124142cbee88a75dab8dbbcc3382fc96)) * improve value merging logic in useUrlFromCurrentTab ([1b5d7dd](https://github.com/gisce/react-ooui/commit/1b5d7dd4b6b5476af5a4ba1d184a1fb161a89876)) * transform initialView to initialViewId ([28fff0a](https://github.com/gisce/react-ooui/commit/28fff0ae1917962fec186f86124b74ccc5494074)) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3aa61bbcf..c17e495c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gisce/react-ooui", - "version": "2.58.0", + "version": "2.58.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gisce/react-ooui", - "version": "2.58.0", + "version": "2.58.1", "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/plots": "^1.0.9", diff --git a/package.json b/package.json index 48a51c5cc..7a1cbd299 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gisce/react-ooui", - "version": "2.58.0", + "version": "2.58.1", "engines": { "node": "20.5.0" }, From 625187475593e866eb5ef44e1d938432645076b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Tue, 28 Jan 2025 13:43:30 +0100 Subject: [PATCH 17/21] feat: fetch toolbar if needed and if view of one2many is hardcoded https://github.com/gisce/webclient/issues/1646 --- src/widgets/base/one2many/One2many.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/widgets/base/one2many/One2many.tsx b/src/widgets/base/one2many/One2many.tsx index 25437defa..b4478386f 100644 --- a/src/widgets/base/one2many/One2many.tsx +++ b/src/widgets/base/one2many/One2many.tsx @@ -2,7 +2,7 @@ import { useContext, useState } from "react"; import { One2many as One2manyOoui } from "@gisce/ooui"; import Field from "@/common/Field"; import { Spin, Alert } from "antd"; -import { Views, ViewType } from "@/types"; +import { FormView, TreeView, Views, ViewType } from "@/types"; import ConnectionProvider from "@/ConnectionProvider"; import One2manyProvider from "@/context/One2manyContext"; import { One2manyInput } from "@/widgets/base/one2many/One2manyInput"; @@ -36,9 +36,21 @@ export const One2many = (props: Props) => { }, [ooui]); const getViewData = async (type: ViewType) => { + const getViewPromise = ConnectionProvider.getHandler().getView({ + model: relation, + type, + context: { ...getContext?.(), ...context }, + }); + if (oouiViews && oouiViews[type]) { - return oouiViews[type]; + const view = oouiViews[type]; + if (!view.toolbar && (type === "form" || type === "tree")) { + const viewWithToolbar: TreeView | FormView = await getViewPromise; + return { ...view, toolbar: viewWithToolbar.toolbar }; + } + return view; } + return await ConnectionProvider.getHandler().getView({ model: relation, type, From 7470e76b082dc621e49a7b5631c80b91679adbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Tue, 28 Jan 2025 17:32:39 +0100 Subject: [PATCH 18/21] fix: temporally disable toolbar in one2many's until we decide which design path follow --- src/views/actionViews/FormActionView.tsx | 1 - src/widgets/base/one2many/One2manyTopBar.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/views/actionViews/FormActionView.tsx b/src/views/actionViews/FormActionView.tsx index 165d57c08..dc5102a0e 100644 --- a/src/views/actionViews/FormActionView.tsx +++ b/src/views/actionViews/FormActionView.tsx @@ -2,7 +2,6 @@ import FormActionBar from "@/actionbar/FormActionBar"; import { FormView } from "@/types"; import TitleHeader from "@/ui/TitleHeader"; import Form from "@/widgets/views/Form"; -import React from "react"; export type FormActionViewProps = { formView?: FormView; diff --git a/src/widgets/base/one2many/One2manyTopBar.tsx b/src/widgets/base/one2many/One2manyTopBar.tsx index a8d841c65..495083bfb 100644 --- a/src/widgets/base/one2many/One2manyTopBar.tsx +++ b/src/widgets/base/one2many/One2manyTopBar.tsx @@ -137,7 +137,7 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { onClick={onToggleViewMode} /> )} - {toolbar && ( + {/* {toolbar && ( <> <Separator /> <DropdownButton @@ -159,7 +159,7 @@ function One2manyTopBarComponent(props: One2manyTopBarProps) { </> )} </> - )} + )} */} </div> </div> ); From 5a5909361e552d5af697a567099be4a02dfcf829 Mon Sep 17 00:00:00 2001 From: semantic-release-bot <semantic-release-bot@martynus.net> Date: Tue, 28 Jan 2025 16:34:37 +0000 Subject: [PATCH 19/21] chore(release): 2.59.0 [skip ci] # [2.59.0](https://github.com/gisce/react-ooui/compare/v2.58.1...v2.59.0) (2025-01-28) ### Bug Fixes * adjust sharebutton for formactionbar ([b9f3d30](https://github.com/gisce/react-ooui/commit/b9f3d3066f8c694b00502d665cac16fe2f7cc5ec)) * bad conflict resolution ([6c09769](https://github.com/gisce/react-ooui/commit/6c0976970cffd19b0f16ba316b0aeabd2edde6e5)) * improve action separator and treeactions bar ([c520994](https://github.com/gisce/react-ooui/commit/c520994cbe0f1df581d1f7ae11eb82dc02bacd22)) * remove unused function ([c68ec43](https://github.com/gisce/react-ooui/commit/c68ec432113c6cd85931ee2b81b43327dbbb91c7)) * temporally disable toolbar in one2many's until we decide which design path follow ([7470e76](https://github.com/gisce/react-ooui/commit/7470e76b082dc621e49a7b5631c80b91679adbdb)) ### Features * add new localization strings and improve One2many top bar ([0081027](https://github.com/gisce/react-ooui/commit/00810279660f877940e0b3547477b2299ee90278)) * allow use of hooks from one2many's ([0419afe](https://github.com/gisce/react-ooui/commit/0419afe1f03a80878b339fc65029e11d75f31d25)) * extract toolbar behaviour to hooks ([1600b5c](https://github.com/gisce/react-ooui/commit/1600b5cdc314746b0a56ada4d8e513a95d3e122a)) * fetch toolbar if needed and if view of one2many is hardcoded ([6251874](https://github.com/gisce/react-ooui/commit/625187475593e866eb5ef44e1d938432645076b8)) * improve actionbar components with react best practices ([4e8627e](https://github.com/gisce/react-ooui/commit/4e8627e23bf9cf0c233cab5fb8bc3aa1d70f6b60)) * improve form handling and toolbar integration in One2many components ([4d82eac](https://github.com/gisce/react-ooui/commit/4d82eacefa3b682de7f1a9536fb8ba4a0caafccd)) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c17e495c8..09ab7b874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gisce/react-ooui", - "version": "2.58.1", + "version": "2.59.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gisce/react-ooui", - "version": "2.58.1", + "version": "2.59.0", "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/plots": "^1.0.9", diff --git a/package.json b/package.json index 7a1cbd299..8fb292a54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gisce/react-ooui", - "version": "2.58.1", + "version": "2.59.0", "engines": { "node": "20.5.0" }, From fd9d10fcc95325300c4068ed2dd4a4c5d70149cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev> Date: Tue, 28 Jan 2025 17:48:40 +0100 Subject: [PATCH 20/21] fix: restore bad conflict resolution --- src/actionbar/FormActionBar.tsx | 38 ++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index 52b3d0924..eb4c02d73 100644 --- a/src/actionbar/FormActionBar.tsx +++ b/src/actionbar/FormActionBar.tsx @@ -210,15 +210,25 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { useHotkeys( "pagedown", - () => isActive && tryAction(onNextClick), + async () => { + if (!isActive) return; + const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); + if (!canWeClose) return; + onNextClick(); + }, { enableOnFormTags: true, preventDefault: true }, - [isActive, tryAction, onNextClick], + [isActive, onNextClick, formRef], ); useHotkeys( "pageup", - () => isActive && tryAction(onPreviousClick), + async () => { + if (!isActive) return; + const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); + if (!canWeClose) return; + onPreviousClick(); + }, { enableOnFormTags: true, preventDefault: true }, - [isActive, tryAction, onPreviousClick], + [isActive, onPreviousClick, formRef], ); useHotkeys( "ctrl+s,command+s", @@ -228,14 +238,22 @@ function FormActionBarComponent({ toolbar }: { toolbar: any }) { ); useHotkeys( "ctrl+l,command+l", - () => { - if (isActive && previousView) { - setPreviousView?.(currentView); - setCurrentView?.(previousView); - } + async () => { + if (!isActive || !previousView) return; + const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); + if (!canWeClose) return; + setPreviousView?.(currentView); + setCurrentView?.(previousView); }, { enableOnFormTags: true, preventDefault: true }, - [isActive, previousView, currentView, setPreviousView, setCurrentView], + [ + isActive, + previousView, + currentView, + setPreviousView, + setCurrentView, + formRef, + ], ); if (!currentView) return null; From f89c7b1b036cd0a099decdd5e0e7f3ad1ede7727 Mon Sep 17 00:00:00 2001 From: semantic-release-bot <semantic-release-bot@martynus.net> Date: Tue, 28 Jan 2025 16:50:17 +0000 Subject: [PATCH 21/21] chore(release): 2.59.1 [skip ci] ## [2.59.1](https://github.com/gisce/react-ooui/compare/v2.59.0...v2.59.1) (2025-01-28) ### Bug Fixes * restore bad conflict resolution ([fd9d10f](https://github.com/gisce/react-ooui/commit/fd9d10fcc95325300c4068ed2dd4a4c5d70149cf)) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09ab7b874..3b09092fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gisce/react-ooui", - "version": "2.59.0", + "version": "2.59.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gisce/react-ooui", - "version": "2.59.0", + "version": "2.59.1", "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/plots": "^1.0.9", diff --git a/package.json b/package.json index 8fb292a54..c79dc776d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gisce/react-ooui", - "version": "2.59.0", + "version": "2.59.1", "engines": { "node": "20.5.0" },