diff --git a/package-lock.json b/package-lock.json index 285d88b38..09ab7b874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gisce/react-ooui", - "version": "2.59.0-rc.1", + "version": "2.59.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gisce/react-ooui", - "version": "2.59.0-rc.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 78565050f..8fb292a54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gisce/react-ooui", - "version": "2.59.0-rc.1", + "version": "2.59.0", "engines": { "node": "20.5.0" }, 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/ActionButton.tsx b/src/actionbar/ActionButton.tsx index cdd600d65..51f06992c 100644 --- a/src/actionbar/ActionButton.tsx +++ b/src/actionbar/ActionButton.tsx @@ -1,4 +1,3 @@ -import React from "react"; import ButtonWithTooltip from "@/common/ButtonWithTooltip"; import { LoadingOutlined } from "@ant-design/icons"; import { ButtonProps } from "antd"; diff --git a/src/actionbar/DashboardActionBar.tsx b/src/actionbar/DashboardActionBar.tsx index cc90c0493..6209375de 100644 --- a/src/actionbar/DashboardActionBar.tsx +++ b/src/actionbar/DashboardActionBar.tsx @@ -11,12 +11,8 @@ import { BorderOuterOutlined, } from "@ant-design/icons"; import { useLocale } from "@gisce/react-formiga-components"; -import { - ActionViewContext, - ActionViewContextType, -} from "@/context/ActionViewContext"; +import { ActionBarSeparator } from "./ActionBarSeparator"; import { ShareUrlButton } from "./ShareUrlButton"; -import { ActionBarSeparator } from "./FormActionBar"; function DashboardActionBar() { const { isLoading, dashboardRef, moveItemsEnabled, setMoveItemsEnabled } = diff --git a/src/actionbar/FormActionBar.tsx b/src/actionbar/FormActionBar.tsx index d6f92e227..52b3d0924 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, @@ -27,19 +27,17 @@ 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"; +import { ActionBarSeparator } from "./ActionBarSeparator"; import { ShareUrlButton } from "./ShareUrlButton"; -function FormActionBar({ toolbar }: { toolbar: any }) { - const contentRootContext = useContext( - ContentRootContext, - ) as ContentRootContextType; +function FormActionBarComponent({ toolbar }: { toolbar: any }) { const tabManagerContext = useContext( TabManagerContext, ) as TabManagerContextType; @@ -74,11 +72,12 @@ function FormActionBar({ toolbar }: { toolbar: any }) { isActive, } = useActionViewContext(); - const { processAction } = contentRootContext || {}; - const { openRelate, openDefaultActionForModel } = tabManagerContext || {}; + const { openDefaultActionForModel } = tabManagerContext || {}; - const mustDisableButtons = - formIsSaving || removingItem || formIsLoading || duplicatingItem; + const mustDisableButtons = useMemo( + () => formIsSaving || removingItem || formIsLoading || duplicatingItem, + [formIsSaving, removingItem, formIsLoading, duplicatingItem], + ); const tryAction = useCallback( (action: () => void) => { @@ -91,6 +90,18 @@ function FormActionBar({ 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); @@ -148,40 +159,66 @@ 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(), + const handleChangeView = useCallback( + (view: any) => { + setPreviousView?.(currentView); + setFormHasChanges?.(false); + setCurrentView?.(view); + }, + [currentView, setPreviousView, setFormHasChanges, setCurrentView], + ); + + 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", + }); + } }, - [formRef, processAction], + [onFormSave, openDefaultActionForModel], ); useHotkeys( "pagedown", - async () => { - if (!isActive) return; - const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); - if (!canWeClose) return; - onNextClick(); - }, + () => isActive && tryAction(onNextClick), { enableOnFormTags: true, preventDefault: true }, - [isActive, onNextClick, formRef], + [isActive, tryAction, onNextClick], ); useHotkeys( "pageup", - async () => { - if (!isActive) return; - const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); - if (!canWeClose) return; - onPreviousClick(); - }, + () => isActive && tryAction(onPreviousClick), { enableOnFormTags: true, preventDefault: true }, - [isActive, onPreviousClick, formRef], + [isActive, tryAction, onPreviousClick], ); useHotkeys( "ctrl+s,command+s", @@ -191,22 +228,14 @@ function FormActionBar({ toolbar }: { toolbar: any }) { ); useHotkeys( "ctrl+l,command+l", - async () => { - if (!isActive || !previousView) return; - const canWeClose = await (formRef.current as any).cancelUnsavedChanges(); - if (!canWeClose) return; - setPreviousView?.(currentView); - setCurrentView?.(previousView); + () => { + if (isActive && previousView) { + setPreviousView?.(currentView); + setCurrentView?.(previousView); + } }, { enableOnFormTags: true, preventDefault: true }, - [ - isActive, - previousView, - currentView, - setPreviousView, - setCurrentView, - formRef, - ], + [isActive, previousView, currentView, setPreviousView, setCurrentView], ); if (!currentView) return null; @@ -267,132 +296,34 @@ 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)} - /> - - - } - 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 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} /> @@ -400,18 +331,40 @@ function FormActionBar({ toolbar }: { toolbar: any }) { ); } -export const ActionBarSeparator = () =>
; +const FormActionBar = memo(FormActionBarComponent); -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 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/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(); diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index fc93f30e8..e01f5cd3f 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -1,4 +1,12 @@ -import { useContext, useEffect, useState, useRef, useMemo } from "react"; +import { + useContext, + useEffect, + useState, + useRef, + memo, + useCallback, + useMemo, +} from "react"; import { Space, Spin } from "antd"; import ChangeViewButton from "./ChangeViewButton"; import { @@ -20,10 +28,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"; @@ -32,8 +36,12 @@ import { mergeParams } from "@/helpers/searchHelper"; import { useFeatureIsEnabled } from "@/context/ConfigContext"; import { ErpFeatureKeys } from "@/models/erpFeature"; import { useHotkeys } from "react-hotkeys-hook"; +import { + useTreeToolbarButtons, + useRunTreeAction, +} from "@/hooks/useTreeToolbarButtons"; +import { ActionBarSeparator } from "./ActionBarSeparator"; import { ShareUrlButton } from "./ShareUrlButton"; -import { ActionBarSeparator } from "./FormActionBar"; type Props = { parentContext?: any; @@ -41,7 +49,11 @@ type Props = { toolbar?: any; }; -function TreeActionBar(props: Props) { +function TreeActionBarComponent({ + parentContext = {}, + treeExpandable, + toolbar, +}: Props) { const { availableViews, currentView, @@ -70,148 +82,191 @@ function TreeActionBar(props: Props) { isInfiniteTree, } = useContext(ActionViewContext) as ActionViewContextType; - const { parentContext = {}, treeExpandable, toolbar } = props; const advancedExportEnabled = useFeatureIsEnabled( 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); - useHotkeys( - "ctrl+l,command+l", - () => { - if (!isActive) { - return; - } - if (previousView) { - setPreviousView?.(currentView); - setCurrentView?.(previousView); - } - }, - { enableOnFormTags: true, preventDefault: true }, - [previousView, currentView, isActive], - ); - - useHotkeys( - "ctrl+f,command+f", - () => { - if (!isActive) { - return; - } - setSearchVisible?.(!searchVisible); - }, - { enableOnFormTags: true, preventDefault: true }, - [searchVisible], - ); + const handleRefresh = useCallback(() => { + searchTreeRef?.current?.refreshResults(); + }, [searchTreeRef]); - useEffect(() => { - if (isInfiniteTree && searchTreeNameSearch === undefined) { - if (isFirstMount.current) { - isFirstMount.current = false; - return; - } + const { actionButtonProps, printButtonProps } = useTreeToolbarButtons({ + toolbar, + disabled: treeIsLoading, + parentContext, + selectedRowItems, + onRefreshParentValues: handleRefresh, + }); - searchTreeRef?.current?.refreshResults(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInfiniteTree, searchTreeNameSearch]); + const runAction = useRunTreeAction({ + selectedRowItems, + onRefreshParentValues: handleRefresh, + }); - const hasNameSearch: boolean = - searchTreeNameSearch !== undefined && - searchTreeNameSearch.trim().length > 0; + const hasNameSearch = useMemo( + () => + searchTreeNameSearch !== undefined && + searchTreeNameSearch.trim().length > 0, + [searchTreeNameSearch], + ); - function tryDuplicate() { - showConfirmDialog({ - confirmMessage: t("confirmDuplicate"), - t, - onOk: () => { - duplicate(); - }, - }); - } + const finalDomain = useMemo(() => { + const domain = searchTreeRef?.current?.getDomain(); + return mergeParams(domain || [], searchParams || []); + }, [searchTreeRef, searchParams]); - function tryDelete() { - showConfirmDialog({ - confirmMessage: t("confirmRemove"), - t, - onOk: () => { - remove(); - }, - }); - } + 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(); + } + } catch (e) { + showErrorDialog(e); + } finally { + setDuplicatingItem?.(false); + } + }, [ + currentModel, + parentContext, + searchTreeRef, + selectedRowItems, + setDuplicatingItem, + ]); - async function remove() { + 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, + ]); - async function duplicate() { - try { - setDuplicatingItem?.(true); + const handleChangeView = useCallback( + (newView: any) => { + setPreviousView?.(currentView); + setCurrentView?.(newView); + }, + [currentView, setPreviousView, setCurrentView], + ); - const currentId = selectedRowItems![0].id; + const handleSearch = useCallback( + (searchString?: string) => { + if (searchString && searchString.trim().length > 0) { + setSearchTreeNameSearch?.(searchString); + } else { + setSearchTreeNameSearch?.(undefined); + if (!isInfiniteTree) { + searchTreeRef?.current?.refreshResults(); + } + } + }, + [isInfiniteTree, searchTreeRef, setSearchTreeNameSearch], + ); - const newId = await ConnectionProvider.getHandler().duplicate({ - id: currentId, - model: currentModel!, - context: { ...parentContext }, - }); + 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) || []; + } - if (newId) { - searchTreeRef?.current?.refreshResults(); + runAction( + { + id: -1, + model: currentModel, + report_name: "printscreen.list", + type: "ir.actions.report.xml", + datas: { + model: currentModel, + ids: idsToExport, + }, + }, + parentContext, + ); + return; } - } catch (e) { - showErrorDialog(e); - } finally { - setDuplicatingItem?.(false); + setExportModalVisible(true); + }, + [currentModel, parentContext, results, runAction, selectedRowItems], + ); + + useEffect(() => { + if (isInfiniteTree && searchTreeNameSearch === undefined) { + if (isFirstMount.current) { + isFirstMount.current = false; + return; + } + searchTreeRef?.current?.refreshResults(); } - } + }, [isInfiniteTree, searchTreeNameSearch, searchTreeRef]); - 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(); - }, + useHotkeys( + "ctrl+l,command+l", + () => { + if (!isActive) return; + if (previousView) { + setPreviousView?.(currentView); + setCurrentView?.(previousView); + } + }, + { enableOnFormTags: true, preventDefault: true }, + [previousView, currentView, isActive, setPreviousView, setCurrentView], + ); + + 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: handleDuplicate, }); - } + }, [handleDuplicate, t]); - const finalDomain = (() => { - const domain = searchTreeRef?.current?.getDomain(); - const finalValues = mergeParams(domain || [], searchParams || []); - return finalValues; - })(); + const tryDelete = useCallback(() => { + showConfirmDialog({ + confirmMessage: t("confirmRemove"), + t, + onOk: handleRemove, + }); + }, [handleRemove, t]); return ( @@ -222,38 +277,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 && ( <> @@ -307,84 +343,21 @@ function TreeActionBar(props: Props) { { - setPreviousView?.(currentView); - setCurrentView?.(newView); - }} + onChangeView={handleChangeView} previousView={previousView} disabled={treeIsLoading} /> )} - } - 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 && ( <> ( - - - - - - - )} - /> - } + icon={} onRetrieveData={async () => [ { label: t("export"), @@ -400,30 +373,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, - }, - }); - return; - } - - setExportModalVisible(true); - }} + onItemClick={handleExportAction} disabled={ duplicatingItem || removingItem || treeIsLoading || hasNameSearch } @@ -447,4 +397,31 @@ function TreeActionBar(props: Props) { ); } +const TreeActionBar = memo(TreeActionBarComponent); export default TreeActionBar; + +const ExportIcon = memo(() => ( + ( + + + + + + + )} + /> +)); + +ExportIcon.displayName = "ExportIcon"; 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]); diff --git a/src/hooks/useFormToolbarButtons.ts b/src/hooks/useFormToolbarButtons.ts new file mode 100644 index 000000000..f5892a226 --- /dev/null +++ b/src/hooks/useFormToolbarButtons.ts @@ -0,0 +1,132 @@ +import { useCallback, useContext, RefObject } from "react"; +import { useLocale } from "@gisce/react-formiga-components"; +import { + ContentRootContext, + ContentRootContextType, +} from "@/context/ContentRootContext"; +import { + TabManagerContext, + TabManagerContextType, +} from "@/context/TabManagerContext"; + +interface UseFormToolbarButtonsProps { + toolbar: any; + mustDisableButtons?: boolean; + formRef: RefObject; + onRefreshParentValues?: () => void; +} + +interface SaveDocumentResult { + succeed: boolean; + currentId?: number; +} + +export const useFormToolbarButtons = ({ + toolbar, + mustDisableButtons = false, + formRef, + onRefreshParentValues, +}: UseFormToolbarButtonsProps) => { + const { t } = useLocale(); + const contentRootContext = useContext( + ContentRootContext, + ) as ContentRootContextType; + const tabManagerContext = useContext( + TabManagerContext, + ) as TabManagerContextType; + + 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?.getValues(), + fields: formRef.current?.getFields(), + context: formRef.current?.getContext(), + onRefreshParentValues, + }); + }, + [formRef, processAction, onRefreshParentValues], + ); + + 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?.getValues(), + fields: formRef.current?.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..7343620f3 --- /dev/null +++ b/src/hooks/useTreeToolbarButtons.ts @@ -0,0 +1,104 @@ +import { useCallback, useContext } from "react"; +import { useLocale } from "@gisce/react-formiga-components"; +import { + ContentRootContext, + ContentRootContextType, +} from "@/context/ContentRootContext"; + +interface UseTreeToolbarButtonsProps { + toolbar: any; + disabled?: boolean; + parentContext?: any; + selectedRowItems?: any[]; + onRefreshParentValues?: () => void; +} + +export const useRunTreeAction = ({ + selectedRowItems, + onRefreshParentValues, +}: { + selectedRowItems?: any[]; + onRefreshParentValues?: () => void; +}) => { + const contentRootContext = useContext( + ContentRootContext, + ) as ContentRootContextType; + const { processAction } = contentRootContext || {}; + + 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, + }); + }, + [processAction, selectedRowItems, onRefreshParentValues], + ); +}; + +export const useTreeToolbarButtons = ({ + toolbar, + disabled = false, + parentContext = {}, + selectedRowItems = [], + onRefreshParentValues, +}: UseTreeToolbarButtonsProps) => { + const { t } = useLocale(); + const runAction = useRunTreeAction({ + selectedRowItems, + onRefreshParentValues, + }); + + 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, + }; +}; diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index e89064c04..447bd8d51 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -109,6 +109,12 @@ 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", 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 2525f6c2c..86572832a 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -105,6 +105,12 @@ 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", 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 5e0e903b0..08c12144e 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -111,6 +111,12 @@ 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", share: "Compartir URL", urlCopiedToClipboard: "URL copiada al portapapeles", copyToClipboard: "Copiar al portapapeles", 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(); 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/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, 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 ( +
{ + const currentItemId = items[itemIndex]?.id; - return ( - { - 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 b8493f3e9..ab98a8534 100644 --- a/src/widgets/base/one2many/One2manyInput.tsx +++ b/src/widgets/base/one2many/One2manyInput.tsx @@ -107,6 +107,7 @@ const One2manyInput: React.FC = ( const [sorter, setSorter] = useState(); const originalSortItemIds = useRef(); const [colorsForResults, setColorsForResults] = useState(undefined); + const formRef = useRef(); const { readOnly, @@ -573,6 +574,7 @@ const One2manyInput: React.FC = ( return ( = ( selectedRowKeys={selectedRowKeys} showCreateButton={views.get("form")?.fields !== undefined} showToggleButton={views.size > 1} + toolbar={views.get(currentView)?.toolbar} + context={{ ...getContext?.(), ...context }} + formRef={formRef} + onRefreshParentValues={() => { + fetchParentFormValues?.({ forceRefresh: true }); + }} /> {content()} = ( getContext, fetchValues: fetchParentFormValues, } = formContext || {}; + const formRef = useRef(); const showToggleButton = views.size > 1; const showCreateButton = views.get("form")?.fields !== undefined; @@ -264,6 +265,12 @@ export const One2manyInput: React.FC = ( selectedRowKeys={selectedRowKeys} showCreateButton={showCreateButton} showToggleButton={showToggleButton} + toolbar={views.get(currentView)?.toolbar} + context={{ ...getContext?.(), ...context }} + formRef={formRef} + onRefreshParentValues={() => { + fetchParentFormValues?.({ forceRefresh: true }); + }} /> {currentView === "tree" && ( = ( )} {currentView === "form" && ( ; + onRefreshParentValues?: () => void; }; -export const One2manyTopBar = (props: One2manyTopBarProps) => { +function One2manyTopBarComponent(props: One2manyTopBarProps) { const { title: titleString, readOnly, @@ -49,88 +59,41 @@ export const One2manyTopBar = (props: One2manyTopBarProps) => { selectedRowKeys, showCreateButton, showToggleButton, + toolbar, + context, + formRef, + onRefreshParentValues, } = props; - const { token } = useToken(); - function separator() { - return
; - } - - function title() { - return ( -
-
- {titleString} -
-
- ); - } + const { token } = useToken(); + const { t } = useLocale(); - function deleteButton() { - return ( - - : } - onClick={onDelete} - danger={!isMany2Many} - type={isMany2Many ? "default" : "primary"} - disabled={ - totalItems === 0 || - readOnly || - (mode !== "form" && selectedRowKeys.length === 0) - } - /> - - ); - } + const { actionButtonProps, printButtonProps, relateButtonProps } = + useFormToolbarButtons({ + toolbar, + mustDisableButtons: readOnly, + formRef, + onRefreshParentValues, + }); - 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 { + actionButtonProps: treeActionButtonProps, + printButtonProps: treePrintButtonProps, + } = useTreeToolbarButtons({ + toolbar, + disabled: readOnly, + parentContext: context, + selectedRowItems: selectedRowKeys.map((key) => ({ id: key })), + onRefreshParentValues, + }); 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,27 +101,178 @@ 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} /> )} + {/* {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> ); -}; +} + +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);