From 41b390da6190577b294700f46a582207da317c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20G=C3=BCell=20Segarra?= Date: Thu, 26 Sep 2024 09:42:16 +0200 Subject: [PATCH] feat: infinite tree (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement user view prefs (remote columns for one2manytree) * fix: adjustment in useNetworkRequest and development mode * fix: remove console.log * fix: try to debug weird bug * fix: debug weird bug * Revert "fix: debug weird bug" This reverts commit 1235374b56987989b25a34d5152f37e48afdbd9d. * Revert "fix: try to debug weird bug" This reverts commit 51ae0e60d4f97c8ee8d82f09b29c59a04bdd1f31. * feat: allow sort columns in one2many infinite trees * feat(inifite-tree): initial wip * fix: work * feat: adjust method not exported * feat: refactor for reusing localstorage and remotestorage column state hooks * feat: search tree infinite does mostly work * feat: more work * feat: adjust context props * feat: improve state restoring in infinite table * feat: adapt one2many to use new select all rows approach * feat: more adjustments in tree * fix: adjust namesearch prop * fix: adjust treeactionview switcher * feat: work on side filter * feat: more work on side filtering * feat: wip in side search drawer * fix: improve side drawer * feat: more adjsutmetns * feat: more work on side drawer * feat: improvements in filter * fix: adjust tree infinite from attr and implement name_Search * fix: avoid two calls and adjust selection when filtering * fix: select all ids with domain and mergedParams * fix: adjust name search selection * fix: improvements in search tree infinite * fix: glitch * fix: adjust tree aggregates * update gisce/react-formiga-table to v1.7.0 (#603) * chore(release): 2.21.0 [skip ci] # [2.21.0](https://github.com/gisce/react-ooui/compare/v2.20.0...v2.21.0) (2024-09-17) ### Features * infinite one2many's improvements, sort and persist state ([#590](https://github.com/gisce/react-ooui/issues/590)) ([cbc4479](https://github.com/gisce/react-ooui/commit/cbc4479a82733d1e02ac286e6aaf7219aed06b7e)), closes [#591](https://github.com/gisce/react-ooui/issues/591) [#592](https://github.com/gisce/react-ooui/issues/592) * fix: adjust fixed height in graph components in forms (#598) https://github.com/gisce/webclient/issues/1220 * chore(release): 2.21.1 [skip ci] ## [2.21.1](https://github.com/gisce/react-ooui/compare/v2.21.0...v2.21.1) (2024-09-23) ### Bug Fixes * adjust fixed height in graph components in forms ([#598](https://github.com/gisce/react-ooui/issues/598)) ([d8cb903](https://github.com/gisce/react-ooui/commit/d8cb903b88bd9523de57760e2fb39111f14ad536)) * fix: allow clear in selection fields (#602) * chore(release): 2.21.2 [skip ci] ## [2.21.2](https://github.com/gisce/react-ooui/compare/v2.21.1...v2.21.2) (2024-09-25) ### Bug Fixes * allow clear in selection fields ([#602](https://github.com/gisce/react-ooui/issues/602)) ([e73fd0b](https://github.com/gisce/react-ooui/commit/e73fd0b2711e46779c85b6de2a9ec02fcafd0ece)) * feat: update gisce/react-formiga-table to v1.7.0 https://github.com/gisce/react-formiga-table/releases/tag/v1.7.0 --------- Co-authored-by: semantic-release-bot Co-authored-by: Marc Güell Segarra Co-authored-by: mguellsegarra <5711443+mguellsegarra@users.noreply.github.com> * update gisce/ooui to v2.11.0 (#604) * chore(release): 2.21.0 [skip ci] # [2.21.0](https://github.com/gisce/react-ooui/compare/v2.20.0...v2.21.0) (2024-09-17) ### Features * infinite one2many's improvements, sort and persist state ([#590](https://github.com/gisce/react-ooui/issues/590)) ([cbc4479](https://github.com/gisce/react-ooui/commit/cbc4479a82733d1e02ac286e6aaf7219aed06b7e)), closes [#591](https://github.com/gisce/react-ooui/issues/591) [#592](https://github.com/gisce/react-ooui/issues/592) * fix: adjust fixed height in graph components in forms (#598) https://github.com/gisce/webclient/issues/1220 * chore(release): 2.21.1 [skip ci] ## [2.21.1](https://github.com/gisce/react-ooui/compare/v2.21.0...v2.21.1) (2024-09-23) ### Bug Fixes * adjust fixed height in graph components in forms ([#598](https://github.com/gisce/react-ooui/issues/598)) ([d8cb903](https://github.com/gisce/react-ooui/commit/d8cb903b88bd9523de57760e2fb39111f14ad536)) * fix: allow clear in selection fields (#602) * chore(release): 2.21.2 [skip ci] ## [2.21.2](https://github.com/gisce/react-ooui/compare/v2.21.1...v2.21.2) (2024-09-25) ### Bug Fixes * allow clear in selection fields ([#602](https://github.com/gisce/react-ooui/issues/602)) ([e73fd0b](https://github.com/gisce/react-ooui/commit/e73fd0b2711e46779c85b6de2a9ec02fcafd0ece)) * feat: update gisce/ooui to v2.11.0 https://github.com/gisce/ooui/releases/tag/v2.11.0 --------- Co-authored-by: semantic-release-bot Co-authored-by: Marc Güell Segarra Co-authored-by: mguellsegarra <5711443+mguellsegarra@users.noreply.github.com> --------- Co-authored-by: Gisce Co-authored-by: semantic-release-bot Co-authored-by: mguellsegarra <5711443+mguellsegarra@users.noreply.github.com> --- package-lock.json | 47 +- package.json | 6 +- src/context/ActionViewContext.tsx | 82 ++- src/helpers/o2m-columnStorageHelper.ts | 13 + src/helpers/tree-columnStorageHelper.ts | 11 + src/helpers/treeHelper.tsx | 29 + src/hooks/useAvailableHeight.ts | 34 ++ src/hooks/useEffectDebugger.ts | 12 +- src/hooks/useFetchTreeViews.ts | 67 +++ src/hooks/usePrevious.ts | 9 + src/locales/ca_ES.ts | 4 + src/locales/en_US.ts | 4 + src/locales/es_ES.ts | 4 + src/types/index.ts | 1 + src/ui/FloatingDrawer.tsx | 156 ++++++ src/ui/GenericErrorDialog.tsx | 21 +- src/ui/TitleHeader.tsx | 5 +- src/views/actionViews/TreeActionView.tsx | 92 ++- .../base/one2many/AggregatesFooter.tsx | 35 ++ src/widgets/base/one2many/One2manyFooter.tsx | 21 - .../base/one2many/One2manyInputInfinite.tsx | 15 +- src/widgets/base/one2many/One2manyTree.tsx | 45 +- src/widgets/base/one2many/useOne2manyTree.ts | 44 +- .../one2many/useOne2manyTreeAggregates.ts | 4 +- .../base/one2many/useTreeAggregates.ts | 68 ++- ...torage.ts => useTreeColumnLocalStorage.ts} | 22 +- ...orage.ts => useTreeColumnRemoteStorage.ts} | 22 +- ...lumnStorage.ts => useTreeColumnStorage.ts} | 21 +- ...eFetch.ts => useTreeColumnStorageFetch.ts} | 17 +- src/widgets/views/SearchTreeHeader.tsx | 66 +++ src/widgets/views/SearchTreeInfinite.tsx | 522 ++++++++++++++++++ src/widgets/views/Tree/treeComponents.tsx | 2 +- .../views/searchFilter/DateRangePicker.tsx | 1 + src/widgets/views/searchFilter/PairFields.tsx | 17 +- .../views/searchFilter/SideSearchFilter.tsx | 267 +++++++++ 35 files changed, 1571 insertions(+), 215 deletions(-) create mode 100644 src/helpers/o2m-columnStorageHelper.ts create mode 100644 src/helpers/tree-columnStorageHelper.ts create mode 100644 src/hooks/useAvailableHeight.ts create mode 100644 src/hooks/useFetchTreeViews.ts create mode 100644 src/hooks/usePrevious.ts create mode 100644 src/ui/FloatingDrawer.tsx create mode 100644 src/widgets/base/one2many/AggregatesFooter.tsx delete mode 100644 src/widgets/base/one2many/One2manyFooter.tsx rename src/widgets/base/one2many/{useOne2manyColumnLocalStorage.ts => useTreeColumnLocalStorage.ts} (52%) rename src/widgets/base/one2many/{useOne2manyColumnRemoteStorage.ts => useTreeColumnRemoteStorage.ts} (78%) rename src/widgets/base/one2many/{useOne2manyColumnStorage.ts => useTreeColumnStorage.ts} (68%) rename src/widgets/base/one2many/{useOne2manyColumnStorageFetch.ts => useTreeColumnStorageFetch.ts} (64%) create mode 100644 src/widgets/views/SearchTreeHeader.tsx create mode 100644 src/widgets/views/SearchTreeInfinite.tsx create mode 100644 src/widgets/views/searchFilter/SideSearchFilter.tsx diff --git a/package-lock.json b/package-lock.json index 9f3c5951..68a13cfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,16 @@ "dependencies": { "@ant-design/plots": "^1.0.9", "@gisce/fiber-diagram": "2.1.1", - "@gisce/ooui": "2.10.2", + "@gisce/ooui": "2.11.0", "@gisce/react-formiga-components": "1.8.0", - "@gisce/react-formiga-table": "1.6.0", + "@gisce/react-formiga-table": "1.7.0", "@monaco-editor/react": "^4.4.5", "@tabler/icons-react": "^2.11.0", + "@types/deep-equal": "^1.0.4", "antd": "5.13.1", "buffer": "^6.0.3", "file-type-buffer-browser": "git+ssh://git@github.com/mguellsegarra/file-type-buffer-browser.git", + "framer-motion": "^11.5.5", "interweave": "^13.0.0", "md5": "^2.3.0", "nanoid": "^5.0.4", @@ -3368,9 +3370,9 @@ } }, "node_modules/@gisce/ooui": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@gisce/ooui/-/ooui-2.10.2.tgz", - "integrity": "sha512-eYbLUxhw5KAi4ne3fgcJY7zQ6gxWCnfswsros6yA4f1O/IVwllpBwC1szKpEkZuQrPmax19eLqFNDLi5ktf3fQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@gisce/ooui/-/ooui-2.11.0.tgz", + "integrity": "sha512-9BZ4p9/JIobkw//YcdBJ3v7sbCTPajeIeSvb3M8r2yDByImDuMvFWat5tzMYjmucRTJXs9v3qu9DW+2JDBTMsw==", "dependencies": { "@gisce/conscheck": "1.0.9", "html-entities": "^2.3.3", @@ -3407,9 +3409,9 @@ } }, "node_modules/@gisce/react-formiga-table": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@gisce/react-formiga-table/-/react-formiga-table-1.6.0.tgz", - "integrity": "sha512-Q6LJxxSm0XA6NQWZJoXPbNdktgoFO9+xoSvPAvfJ5knJAfwM/0oGAwkx++Wbcn8LUyP3b51CfZ26VzJKEzxgwQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@gisce/react-formiga-table/-/react-formiga-table-1.7.0.tgz", + "integrity": "sha512-UJMD5qBTgkJLzYGHKDjOT/eEY1ebt0QEx5xOqqFUVtEma9w50sVtLXvJEXAHfsQOBSvnm79jT9bOPzqGGHTSWA==", "dependencies": { "ag-grid-community": "^31.2.1", "ag-grid-react": "^31.2.1", @@ -9154,6 +9156,11 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", + "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==" + }, "node_modules/@types/detect-port": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", @@ -14346,6 +14353,30 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "11.5.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.5.tgz", + "integrity": "sha512-4srkT940jYA3bdQRglxod0KoqDvcghYri1A6bTjT02IXvq/EAd6A0tgUnJc5Q2ahhf8n959aLD3yO+XmLmE8OQ==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", diff --git a/package.json b/package.json index e3e40473..709c1f71 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,16 @@ "dependencies": { "@ant-design/plots": "^1.0.9", "@gisce/fiber-diagram": "2.1.1", - "@gisce/ooui": "2.10.2", + "@gisce/ooui": "2.11.0", "@gisce/react-formiga-components": "1.8.0", - "@gisce/react-formiga-table": "1.6.0", + "@gisce/react-formiga-table": "1.7.0", "@monaco-editor/react": "^4.4.5", "@tabler/icons-react": "^2.11.0", + "@types/deep-equal": "^1.0.4", "antd": "5.13.1", "buffer": "^6.0.3", "file-type-buffer-browser": "git+ssh://git@github.com/mguellsegarra/file-type-buffer-browser.git", + "framer-motion": "^11.5.5", "interweave": "^13.0.0", "md5": "^2.3.0", "nanoid": "^5.0.4", diff --git a/src/context/ActionViewContext.tsx b/src/context/ActionViewContext.tsx index 4557cafc..bcd4cfa4 100644 --- a/src/context/ActionViewContext.tsx +++ b/src/context/ActionViewContext.tsx @@ -1,27 +1,45 @@ import { DEFAULT_SEARCH_LIMIT } from "@/models/constants"; import { View } from "@/types"; -import { createContext, useEffect, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; -export type ActionViewContextType = { +type ActionViewProviderProps = { title: string; - availableViews: View[]; currentView: View; setCurrentView: (view: View) => void; + availableViews: View[]; + formRef: any; + searchTreeRef: any; + onNewClicked: () => void; + currentId?: number; + setCurrentId: (id?: number) => void; + setCurrentItemIndex: (value?: number) => void; + currentItemIndex?: number; + results?: any[]; + setResults: (value: any[]) => void; + currentModel: string; + sorter: any; + setSorter: (sorter: any) => void; + totalItems: number; + setTotalItems: (totalItems: number) => void; + selectedRowItems?: any[]; + setSelectedRowItems: (value: any[]) => void; + setSearchTreeNameSearch: (searchString?: string) => void; + searchTreeNameSearch?: string; + goToResourceId: (ids: number[], openInSameTab?: boolean) => Promise; + limit?: number; + isActive: boolean; + children: React.ReactNode; +}; + +export type ActionViewContextType = Omit< + ActionViewProviderProps, + "children" +> & { formIsSaving?: boolean; setFormIsSaving?: (value: boolean) => void; formHasChanges?: boolean; setFormHasChanges?: (value: boolean) => void; onFormSave?: () => Promise<{ succeed: boolean; id: number }>; - formRef?: any; - searchTreeRef?: any; - onNewClicked: () => void; - currentId?: number; - setCurrentId?: (id?: number) => void; - currentItemIndex?: number; - setCurrentItemIndex?: (value?: number) => void; - results?: any[]; - setResults?: (value: any[]) => void; - currentModel?: string; removingItem?: boolean; setRemovingItem?: (value: boolean) => void; formIsLoading?: boolean; @@ -32,39 +50,26 @@ export type ActionViewContextType = { setGraphIsLoading?: (value: boolean) => void; attachments?: any; setAttachments?: (value: any) => void; - selectedRowItems?: any[]; - setSelectedRowItems?: (value: any[]) => void; duplicatingItem?: boolean; setDuplicatingItem?: (value: boolean) => void; searchParams?: any[]; setSearchParams?: (value: any[]) => void; searchVisible?: boolean; setSearchVisible?: (value: boolean) => void; - sorter: any; - setSorter: (sorter: any) => void; - totalItems: number; - setTotalItems: (totalItems: number) => void; - searchTreeNameSearch?: string; - setSearchTreeNameSearch?: (searchString?: string) => void; previousView?: View; setPreviousView?: (view: View) => void; - goToResourceId?: (ids: number[], openInSameTab?: boolean) => Promise; searchValues?: any; setSearchValues?: (value: any) => void; - limit?: number; setLimit?: (value: number) => void; setTitle?: (value: string) => void; - isActive: boolean; + treeFirstVisibleRow: number; + setTreeFirstVisibleRow: (totalItems: number) => void; }; export const ActionViewContext = createContext( null, ); -type ActionViewProviderProps = ActionViewContextType & { - children: React.ReactNode; -}; - const ActionViewProvider = (props: ActionViewProviderProps): any => { const { children, @@ -107,6 +112,8 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { const [graphIsLoading, setGraphIsLoading] = useState(true); const [previousView, setPreviousView] = useState(); const [searchValues, setSearchValues] = useState({}); + const [treeFirstVisibleRow, setTreeFirstVisibleRow] = useState(0); + const [limit, setLimit] = useState( limitProps !== undefined ? limitProps : DEFAULT_SEARCH_LIMIT, ); @@ -207,6 +214,8 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { setLimit, setTitle, isActive, + setTreeFirstVisibleRow, + treeFirstVisibleRow, }} > {children} @@ -214,4 +223,21 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { ); }; +export const useActionViewContext = ( + isRoot: boolean, +): ActionViewContextType => { + const actionViewContext = useContext( + ActionViewContext, + ) as ActionViewContextType; + if (!isRoot) { + return {} as ActionViewContextType; + } + if (!actionViewContext) { + throw new Error( + "useActionViewContext must be used within a ActionViewProvider", + ); + } + return actionViewContext; +}; + export default ActionViewProvider; diff --git a/src/helpers/o2m-columnStorageHelper.ts b/src/helpers/o2m-columnStorageHelper.ts new file mode 100644 index 00000000..395303ea --- /dev/null +++ b/src/helpers/o2m-columnStorageHelper.ts @@ -0,0 +1,13 @@ +export type One2manyTreeDataForHash = { + parentViewId?: number; + treeViewId?: number; + one2ManyFieldName: string; +}; + +export type O2mDataForHashWithModel = One2manyTreeDataForHash & { + model: string; +}; + +export const getKey = (dataForHash: O2mDataForHashWithModel) => { + return `columnState-${dataForHash.parentViewId}-${dataForHash.treeViewId}-${dataForHash.one2ManyFieldName}-${dataForHash.model}`; +}; diff --git a/src/helpers/tree-columnStorageHelper.ts b/src/helpers/tree-columnStorageHelper.ts new file mode 100644 index 00000000..4a92a7fc --- /dev/null +++ b/src/helpers/tree-columnStorageHelper.ts @@ -0,0 +1,11 @@ +export type TreeDataForHash = { + treeViewId?: number; + model: string; +}; + +export const getKey = (dataForHash: TreeDataForHash) => { + if (!dataForHash.treeViewId || !dataForHash.model) { + return undefined; + } + return `columnState-${dataForHash.treeViewId}-${dataForHash.model}`; +}; diff --git a/src/helpers/treeHelper.tsx b/src/helpers/treeHelper.tsx index ba5bf20c..4f614bfa 100644 --- a/src/helpers/treeHelper.tsx +++ b/src/helpers/treeHelper.tsx @@ -6,6 +6,7 @@ import { Reference, } from "@gisce/ooui"; import { TreeView, Column } from "@/types"; +import { SortDirection } from "@gisce/react-formiga-table"; const getTree = (treeView: TreeView): TreeOoui => { const xml = treeView.arch; @@ -218,6 +219,32 @@ function hasActualValues(obj: Record): boolean { return false; } +const getOrderFromSortFields = (sortFields?: Record) => { + if (!sortFields) { + return undefined; + } + return Object.keys(sortFields) + .map((field) => { + const direction = sortFields[field]; + return `${field} ${direction}`; + }) + .join(", "); +}; + +function extractTreeXmlAttribute( + archString: string, + attributeName: string, +): string | null { + const regex = new RegExp(`]*\\s+${attributeName}="([^"]+)"`, "i"); + const match = archString.match(regex); + + if (match && match[1]) { + return match[1]; + } + + return null; +} + export { getTableColumns, getTableItems, @@ -228,4 +255,6 @@ export { getStatusMap, sortResults, hasActualValues, + getOrderFromSortFields, + extractTreeXmlAttribute, }; diff --git a/src/hooks/useAvailableHeight.ts b/src/hooks/useAvailableHeight.ts new file mode 100644 index 00000000..f9c77d43 --- /dev/null +++ b/src/hooks/useAvailableHeight.ts @@ -0,0 +1,34 @@ +import React, { useState, useEffect, RefObject, useMemo } from "react"; + +export const useAvailableHeight = ({ + elementRef, + offset = 0, + dependencies = [], +}: { + elementRef: RefObject; + offset?: number; + dependencies?: React.DependencyList; +}): number => { + const [availableHeight, setAvailableHeight] = useState(0); + + useEffect(() => { + const updateHeight = () => { + if (elementRef.current) { + const windowHeight = window.innerHeight; + const boundingRect = elementRef.current.getBoundingClientRect(); + const availableHeight = windowHeight - boundingRect.top; + setAvailableHeight(availableHeight); + } + }; + + updateHeight(); + window.addEventListener("resize", updateHeight); + + return () => window.removeEventListener("resize", updateHeight); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef, ...dependencies]); + + const memoizedHeight = useMemo(() => availableHeight, [availableHeight]); + + return memoizedHeight - offset; +}; diff --git a/src/hooks/useEffectDebugger.ts b/src/hooks/useEffectDebugger.ts index 278c2479..0a856b62 100644 --- a/src/hooks/useEffectDebugger.ts +++ b/src/hooks/useEffectDebugger.ts @@ -1,12 +1,5 @@ -import { useRef, useEffect } from "react"; - -const usePrevious = (value: any, initialValue: any) => { - const ref = useRef(initialValue); - useEffect(() => { - ref.current = value; - }); - return ref.current; -}; +import { useEffect } from "react"; +import { usePrevious } from "./usePrevious"; const useEffectDebugger = ( effectHook: any, @@ -37,6 +30,7 @@ const useEffectDebugger = ( console.log("[use-effect-debugger] ", changedDeps); } + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(effectHook, dependencies); }; diff --git a/src/hooks/useFetchTreeViews.ts b/src/hooks/useFetchTreeViews.ts new file mode 100644 index 00000000..2d584988 --- /dev/null +++ b/src/hooks/useFetchTreeViews.ts @@ -0,0 +1,67 @@ +import { useCallback, useState } from "react"; +import { ConnectionProvider, FormView, TreeView } from ".."; +import { useNetworkRequest } from "@/hooks/useNetworkRequest"; +import { showErrorDialog } from "@/ui/GenericErrorDialog"; +import useDeepCompareEffect from "use-deep-compare-effect"; + +export type UseFetchTreeViewsOpts = { + model: string; + formViewProps?: FormView; + treeViewProps?: TreeView; + context?: any; +}; + +export const useFetchTreeViews = ({ + model, + formViewProps, + treeViewProps, + context, +}: UseFetchTreeViewsOpts) => { + const [loading, setLoading] = useState(true); + const [treeView, setTreeView] = useState(); + const [formView, setFormView] = useState(); + + const [fetchGetViewRequest, cancelGetViewRequest] = useNetworkRequest( + ConnectionProvider.getHandler().getView, + ); + + useDeepCompareEffect(() => { + fetchViewData(); + return () => { + cancelGetViewRequest(); + }; + }, [context, formViewProps, model, treeViewProps]); + + const fetchViewData = useCallback(async () => { + setLoading(true); + try { + const fetchPromises: Array> = []; + + if (!formViewProps) { + fetchPromises.push( + fetchGetViewRequest({ model, type: "form", context }), + ); + } + if (!treeViewProps) { + fetchPromises.push( + fetchGetViewRequest({ model, type: "tree", context }), + ); + } + + const results = await Promise.all(fetchPromises); + + const formViewIndex = 0; + const treeViewIndex = formViewProps ? 0 : 1; + + setFormView(formViewProps || (results[formViewIndex] as FormView)); + setTreeView(treeViewProps || (results[treeViewIndex] as TreeView)); + } catch (error) { + console.error("Error fetching view data:", error); + showErrorDialog(error); + } finally { + setLoading(false); + } + }, [context, fetchGetViewRequest, formViewProps, model, treeViewProps]); + + return { loading, treeView, formView }; +}; diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts new file mode 100644 index 00000000..95e0874c --- /dev/null +++ b/src/hooks/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "react"; + +export const usePrevious = (value: any, initialValue?: any) => { + const ref = useRef(initialValue); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index c770f46c..8a7b4eae 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -101,4 +101,8 @@ export default { openInNewTab: "Obrir en una nova pestanya", confirmDuplicate: "Estàs segur de volguer duplicar els registre/s seleccionats?", + confirmSelectAllRegisters: + "Estàs segur de volguer seleccionar tots els {totalRecords} registres?", + filter: "Filtrar", + applyFilters: "Aplicar filtres", }; diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 59480457..7a761ca4 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -97,4 +97,8 @@ export default { openInSameWindow: "Open in the current tab", openInNewTab: "Open in a new tab", confirmDuplicate: "Are you sure you want to duplicate the selected item/s?", + confirmSelectAllRegisters: + "Are you sure you want to select all {totalRecords} registers?", + filter: "Filter", + applyFilters: "Apply filters", }; diff --git a/src/locales/es_ES.ts b/src/locales/es_ES.ts index 6af2166a..71047308 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -103,4 +103,8 @@ export default { openInNewTab: "Abrir en una nueva pestaña", confirmDuplicate: "Estás seguro de querer duplicar el registro/s seleccionado/s?", + confirmSelectAllRegisters: + "Estás seguro de querer seleccionar todos los {totalRecords} registros?", + filter: "Filtrar", + applyFilters: "Aplicar filtros", }; diff --git a/src/types/index.ts b/src/types/index.ts index bd61df5e..86bb0a6b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -87,6 +87,7 @@ type SearchRequest = { context?: any; attrs?: any; order?: number | string | null; + name_search?: string; }; type SearchAllIdsRequest = SearchCountRequest & { diff --git a/src/ui/FloatingDrawer.tsx b/src/ui/FloatingDrawer.tsx new file mode 100644 index 00000000..301e8e04 --- /dev/null +++ b/src/ui/FloatingDrawer.tsx @@ -0,0 +1,156 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Typography, Button, Layout, theme } from "antd"; +import { CloseOutlined } from "@ant-design/icons"; + +const { useToken } = theme; +const { Title } = Typography; +const { Header, Content, Footer } = Layout; + +interface FloatingDrawerProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + title?: string; + footer?: React.ReactNode; +} + +export const FloatingDrawer: React.FC = ({ + isOpen, + onClose, + children, + title, + footer, +}) => { + const [showDrawer, setShowDrawer] = useState(isOpen); + const drawerRef = useRef(null); + const { token } = useToken(); + + const handleOverlayClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + onClose(); + }, + [onClose], + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }, + [onClose], + ); + + useEffect(() => { + if (isOpen) { + setShowDrawer(true); + document.addEventListener("keydown", handleKeyDown); + document.body.style.overflow = "hidden"; + } else { + document.removeEventListener("keydown", handleKeyDown); + document.body.style.overflow = ""; + } + + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.body.style.overflow = ""; + }; + }, [isOpen, handleKeyDown]); + + const handleAnimationComplete = () => { + if (!isOpen) { + setShowDrawer(false); + } + }; + + if (!showDrawer) return null; + + const headerFooterStyle: React.CSSProperties = { + height: "64px", // Fixed height for header and footer + display: "flex", + alignItems: "center", // Vertically center content + background: token.colorPrimaryBg, + padding: "0 16px", + borderBottom: "1px solid #f0f0f0", + }; + + return ( + + {isOpen && ( + <> + + +
+ + {title} + +
+ + {children} + + {footer && ( +
+ {footer} +
+ )} +
+ + )} +
+ ); +}; diff --git a/src/ui/GenericErrorDialog.tsx b/src/ui/GenericErrorDialog.tsx index 1818ae95..04d4a146 100644 --- a/src/ui/GenericErrorDialog.tsx +++ b/src/ui/GenericErrorDialog.tsx @@ -1,5 +1,6 @@ -import { Modal } from "antd"; +import { App, Modal } from "antd"; import { ExclamationCircleOutlined } from "@ant-design/icons"; +import { useCallback } from "react"; const { error } = Modal; @@ -18,3 +19,21 @@ export const showErrorExceptionDialog = (error: any) => { : JSON.stringify(error); showErrorDialog(messageContent); }; + +export const useShowErrorDialog = () => { + const { modal } = App.useApp(); + return useCallback( + (error: any) => { + const messageContent = error.message + ? JSON.stringify(error.message) + : JSON.stringify(error); + modal.error({ + title: "Error", + icon: , + centered: true, + content: messageContent, + }); + }, + [modal], + ); +}; diff --git a/src/ui/TitleHeader.tsx b/src/ui/TitleHeader.tsx index b919c194..2d3277c3 100644 --- a/src/ui/TitleHeader.tsx +++ b/src/ui/TitleHeader.tsx @@ -14,10 +14,11 @@ const { Title, Text } = Typography; type Props = { title?: string; children?: any; + showSummary?: boolean; }; function TitleHeader(props: Props) { - const { title: titleProps, children } = props; + const { title: titleProps, children, showSummary = true } = props; const { title, currentView, @@ -103,7 +104,7 @@ function TitleHeader(props: Props) { {titleProps || title} - {getSummary()} + {showSummary && getSummary()} {children} diff --git a/src/views/actionViews/TreeActionView.tsx b/src/views/actionViews/TreeActionView.tsx index 1aebb769..feca55c3 100644 --- a/src/views/actionViews/TreeActionView.tsx +++ b/src/views/actionViews/TreeActionView.tsx @@ -1,12 +1,14 @@ import TreeActionBar from "@/actionbar/TreeActionBar"; import { FormView, TreeView, View } from "@/types"; import TitleHeader from "@/ui/TitleHeader"; -import SearchTree from "@/widgets/views/SearchTree"; -import React, { useContext } from "react"; +import { Fragment, useCallback, useContext, useMemo } from "react"; import { ActionViewContext, ActionViewContextType, } from "@/context/ActionViewContext"; +import { SearchTreeInfinite } from "@/widgets/views/SearchTreeInfinite"; +import SearchTree from "@/widgets/views/SearchTree"; +import { extractTreeXmlAttribute } from "@/helpers/treeHelper"; export type TreeActionViewProps = { formView?: FormView; @@ -42,46 +44,80 @@ export const TreeActionView = (props: TreeActionViewProps) => { searchTreeNameSearch, } = props; + const isInfiniteTree = useMemo(() => { + if (!treeView?.arch) { + return false; + } + return extractTreeXmlAttribute(treeView?.arch, "infinite"); + }, [treeView]); + const { currentView, setPreviousView } = useContext( ActionViewContext, ) as ActionViewContextType; + const onRowClicked = useCallback( + (event: any) => { + const { id } = event; + setCurrentId(id); + const itemIndex = results.findIndex((item: any) => { + return item.id === id; + }); + setPreviousView?.(currentView); + setCurrentItemIndex(itemIndex); + const formView = availableViews.find( + (v) => v.type === "form", + ) as FormView; + setCurrentView(formView); + }, + [ + availableViews, + currentView, + results, + setCurrentId, + setCurrentItemIndex, + setCurrentView, + setPreviousView, + ], + ); + if (!visible) { return null; } return ( - <> - + + - { - const { id } = event; - setCurrentId(id); - const itemIndex = results.findIndex((item: any) => { - return item.id === id; - }); - setPreviousView?.(currentView); - setCurrentItemIndex(itemIndex); - const formView = availableViews.find( - (v) => v.type === "form", - ) as FormView; - setCurrentView(formView); - }} - /> - + {isInfiniteTree && ( + + )} + {!isInfiniteTree && ( + + )} + ); }; diff --git a/src/widgets/base/one2many/AggregatesFooter.tsx b/src/widgets/base/one2many/AggregatesFooter.tsx new file mode 100644 index 00000000..5aece3bb --- /dev/null +++ b/src/widgets/base/one2many/AggregatesFooter.tsx @@ -0,0 +1,35 @@ +import { TreeAggregates } from "./useTreeAggregates"; +import { LoadingOutlined } from "@ant-design/icons"; + +export const AggregatesFooter = ({ + aggregates, + isLoading, +}: { + aggregates: TreeAggregates; + isLoading: boolean; +}) => { + const summary = + aggregates && + Object.keys(aggregates).map((fieldKey) => { + const fieldAggregates = aggregates[fieldKey]; + const fieldSummary = fieldAggregates.map((aggregate) => { + return `${aggregate.label}: ${aggregate.amount}`; + }); + return fieldSummary.join(", "); + }); + + return ( +
+ {isLoading && } + {!isLoading && summary && summary.join(", ")} +
+ ); +}; diff --git a/src/widgets/base/one2many/One2manyFooter.tsx b/src/widgets/base/one2many/One2manyFooter.tsx deleted file mode 100644 index ddae2235..00000000 --- a/src/widgets/base/one2many/One2manyFooter.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { TreeAggregates } from "./useTreeAggregates"; - -export const One2manyFooter = ({ - aggregates, -}: { - aggregates: TreeAggregates; -}) => { - if (!aggregates) { - return null; - } - - const summary = Object.keys(aggregates).map((fieldKey) => { - const fieldAggregates = aggregates[fieldKey]; - const fieldSummary = fieldAggregates.map((aggregate) => { - return `${aggregate.label}: ${aggregate.amount}`; - }); - return fieldSummary.join(", "); - }); - - return
{summary.join(", ")}
; -}; diff --git a/src/widgets/base/one2many/One2manyInputInfinite.tsx b/src/widgets/base/one2many/One2manyInputInfinite.tsx index a1b9d545..dec71ffe 100644 --- a/src/widgets/base/one2many/One2manyInputInfinite.tsx +++ b/src/widgets/base/one2many/One2manyInputInfinite.tsx @@ -92,16 +92,18 @@ export const One2manyInput: React.FC = ( setSelectedRowKeys, onChangeFirstVisibleRowIndex, onGetFirstVisibileRowIndex, - onGetSelectedRowKeys, - allRowSelectedMode, - onChangeAllRowSelectedMode, + onSelectionCheckboxClicked, } = useOne2manyTree({ treeView: views.get("tree"), relation, context, + allRowsIds: items + .filter((item) => item.id !== undefined) + .map((item) => item.id!), + gridRef, }); - const aggregates = useOne2manyTreeAggregates({ + const [, aggregates] = useOne2manyTreeAggregates({ ooui: treeOoui, model: relation, items, @@ -272,9 +274,8 @@ export const One2manyInput: React.FC = ( relation={relation} onChangeFirstVisibleRowIndex={onChangeFirstVisibleRowIndex} onGetFirstVisibleRowIndex={onGetFirstVisibileRowIndex} - onGetSelectedRowKeys={onGetSelectedRowKeys} - allRowSelectedMode={allRowSelectedMode} - onAllRowSelectedModeChange={onChangeAllRowSelectedMode} + selectedRowKeys={selectedRowKeys} + onSelectionCheckboxClicked={onSelectionCheckboxClicked} dataForHash={{ parentViewId: props.parentViewId, treeViewId: props.treeViewId, diff --git a/src/widgets/base/one2many/One2manyTree.tsx b/src/widgets/base/one2many/One2manyTree.tsx index 3e013b3e..364a111b 100644 --- a/src/widgets/base/one2many/One2manyTree.tsx +++ b/src/widgets/base/one2many/One2manyTree.tsx @@ -11,15 +11,13 @@ import { COLUMN_COMPONENTS } from "@/widgets/views/Tree/treeComponents"; import useDeepCompareEffect from "use-deep-compare-effect"; import { useDeepCompareMemo } from "use-deep-compare"; import { TreeAggregates } from "./useTreeAggregates"; -import { One2manyFooter } from "./One2manyFooter"; -import { useOne2manyColumnStorageFetch } from "./useOne2manyColumnStorageFetch"; +import { AggregatesFooter } from "./AggregatesFooter"; +import { useTreeColumnStorageFetch } from "./useTreeColumnStorageFetch"; import { Spin, Badge } from "antd"; - -export type One2manyTreeDataForHash = { - parentViewId?: number; - treeViewId?: number; - one2ManyFieldName: string; -}; +import { + One2manyTreeDataForHash, + getKey, +} from "@/helpers/o2m-columnStorageHelper"; export type One2manyTreeProps = { items: One2manyItem[]; @@ -44,11 +42,10 @@ export type One2manyTreeProps = { relation: string; onChangeFirstVisibleRowIndex?: (index: number) => void; onGetFirstVisibleRowIndex?: () => number | undefined; - onGetSelectedRowKeys?: () => any[]; - onAllRowSelectedModeChange?: (allRowSelectedMode: boolean) => void; - allRowSelectedMode?: boolean; + onSelectionCheckboxClicked?: () => void; dataForHash: One2manyTreeDataForHash; aggregates?: TreeAggregates; + selectedRowKeys?: number[]; }; const DEFAULT_HEIGHT = 400; @@ -66,11 +63,10 @@ export const One2manyTree = ({ relation, onChangeFirstVisibleRowIndex, onGetFirstVisibleRowIndex, - onGetSelectedRowKeys, - onAllRowSelectedModeChange, - allRowSelectedMode, + onSelectionCheckboxClicked, dataForHash, aggregates, + selectedRowKeys = [], }: One2manyTreeProps) => { const internalGridRef = useRef(); const tableRef: RefObject = gridRef! || internalGridRef!; @@ -189,10 +185,12 @@ export const One2manyTree = ({ }, []); const { loading, getColumnState, updateColumnState } = - useOne2manyColumnStorageFetch({ - ...dataForHash, - model: relation, - }); + useTreeColumnStorageFetch( + getKey({ + ...dataForHash, + model: relation, + }), + ); if (loading) { return ; @@ -212,11 +210,14 @@ export const One2manyTree = ({ onGetColumnsState={getColumnState} onChangeFirstVisibleRowIndex={onChangeFirstVisibleRowIndex} onGetFirstVisibleRowIndex={onGetFirstVisibleRowIndex} - onGetSelectedRowKeys={onGetSelectedRowKeys} - allRowSelectedMode={allRowSelectedMode} - onAllRowSelectedModeChange={onAllRowSelectedModeChange} + selectedRowKeys={selectedRowKeys} + onSelectionCheckboxClicked={onSelectionCheckboxClicked} totalRows={totalRows} - footer={aggregates && } + footer={ + aggregates && ( + + ) + } hasStatusColumn={ooui.status !== null} statusComponent={(status: any) => } onRowStatus={(record: any) => statusForResults.current?.[record.id]} diff --git a/src/widgets/base/one2many/useOne2manyTree.ts b/src/widgets/base/one2many/useOne2manyTree.ts index ae17d7a8..898148ca 100644 --- a/src/widgets/base/one2many/useOne2manyTree.ts +++ b/src/widgets/base/one2many/useOne2manyTree.ts @@ -1,27 +1,42 @@ import ConnectionProvider from "@/ConnectionProvider"; import { getColorMap, getStatusMap, getTree } from "@/helpers/treeHelper"; import { TreeView } from "@/types"; -import { SortDirection } from "@gisce/react-formiga-table"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { InfiniteTableRef, SortDirection } from "@gisce/react-formiga-table"; +import { useCallback, useMemo, useRef, useState } from "react"; import { useDeepCompareCallback } from "use-deep-compare"; export const useOne2manyTree = ({ treeView, relation, context, + allRowsIds, + gridRef, }: { treeView: TreeView; relation: string; context: any; + allRowsIds: number[]; + gridRef: React.RefObject; }) => { const [selectedRowKeys, setSelectedRowKeys] = useState([]); const firstVisibleRowIndex = useRef(0); - const selectedRowKeysRef = useRef(selectedRowKeys); - const allRowSelectedMode = useRef(false); - const onChangeAllRowSelectedMode = useCallback((value: boolean) => { - allRowSelectedMode.current = value; - }, []); + const onSelectionCheckboxClicked = useCallback(() => { + let mustSelectAll = false; + if (selectedRowKeys.length === 0) { + mustSelectAll = true; + } else { + mustSelectAll = false; + } + + if (mustSelectAll) { + setSelectedRowKeys(allRowsIds); + gridRef.current?.setSelectedRows(allRowsIds); + } else { + setSelectedRowKeys([]); + gridRef.current?.setSelectedRows([]); + } + }, [allRowsIds, gridRef, selectedRowKeys.length]); const onChangeFirstVisibleRowIndex = useCallback((index: number) => { firstVisibleRowIndex.current = index; @@ -31,17 +46,6 @@ export const useOne2manyTree = ({ return firstVisibleRowIndex.current; }, []); - useEffect(() => { - selectedRowKeysRef.current = selectedRowKeys; - }, [selectedRowKeys]); - - const onGetSelectedRowKeys = useCallback(() => { - if (allRowSelectedMode) { - return []; - } - return selectedRowKeysRef.current; - }, []); - const treeOoui = useMemo(() => { return getTree(treeView); }, [treeView]); @@ -105,8 +109,6 @@ export const useOne2manyTree = ({ selectedRowKeys, onChangeFirstVisibleRowIndex, onGetFirstVisibileRowIndex, - onGetSelectedRowKeys, - onChangeAllRowSelectedMode, - allRowSelectedMode: allRowSelectedMode.current, + onSelectionCheckboxClicked, }; }; diff --git a/src/widgets/base/one2many/useOne2manyTreeAggregates.ts b/src/widgets/base/one2many/useOne2manyTreeAggregates.ts index ea465c3c..6c64c620 100644 --- a/src/widgets/base/one2many/useOne2manyTreeAggregates.ts +++ b/src/widgets/base/one2many/useOne2manyTreeAggregates.ts @@ -1,6 +1,6 @@ import { Tree as TreeOoui } from "@gisce/ooui"; import { One2manyItem } from "./One2manyInput"; -import { TreeAggregates, useTreeAggregates } from "./useTreeAggregates"; +import { useTreeAggregates } from "./useTreeAggregates"; export const useOne2manyTreeAggregates = ({ ooui, @@ -12,7 +12,7 @@ export const useOne2manyTreeAggregates = ({ items: One2manyItem[]; selectedRowKeys: any[]; model: string; -}): TreeAggregates => { +}) => { const realItems = items.filter((it) => it.id && it.id > 0); let domain; if (selectedRowKeys.length > 0) { diff --git a/src/widgets/base/one2many/useTreeAggregates.ts b/src/widgets/base/one2many/useTreeAggregates.ts index 7d616014..7b11c538 100644 --- a/src/widgets/base/one2many/useTreeAggregates.ts +++ b/src/widgets/base/one2many/useTreeAggregates.ts @@ -1,9 +1,12 @@ import ConnectionProvider from "@/ConnectionProvider"; import { useNetworkRequest } from "@/hooks/useNetworkRequest"; import { Tree as TreeOoui } from "@gisce/ooui"; -import { useEffect, useState } from "react"; -import { useDeepCompareCallback, useDeepCompareMemo } from "use-deep-compare"; -import useDeepCompareEffect from "use-deep-compare-effect"; +import { useState } from "react"; +import { + useDeepCompareEffect, + useDeepCompareCallback, + useDeepCompareMemo, +} from "use-deep-compare"; const OPERATION_KEYS = ["sum", "count", "max", "min"]; @@ -13,7 +16,7 @@ export type TreeAggregates = Array<{ operation: string; label: string; - amount: number; + amount: number | string; }> > | undefined; @@ -22,18 +25,22 @@ export const useTreeAggregates = ({ ooui, model, domain, + showEmptyValues, }: { - ooui: TreeOoui; + ooui?: TreeOoui; domain?: any[]; model: string; -}) => { + showEmptyValues?: boolean; +}): [boolean, TreeAggregates, boolean] => { const [aggregates, setAggregates] = useState(); + const [loading, setLoading] = useState(false); const [readAggregates, cancelReadAggregates] = useNetworkRequest( ConnectionProvider.getHandler().readAggregates, ); const fieldsAndOpToRetrieve = useDeepCompareMemo(() => { + if (!ooui) return undefined; return ooui.columns .filter((it) => { return Object.keys(it).some((key) => { @@ -60,18 +67,47 @@ export const useTreeAggregates = ({ acc[key] = obj[key as any]; return acc; }, {}); - }, [ooui.columns]); + }, [ooui?.columns]); const fetchData = useDeepCompareCallback(async () => { - if (!domain) { + if (!ooui) { return; } try { + setLoading(true); + + if (!domain && showEmptyValues && fieldsAndOpToRetrieve) { + const emptyAggregates: TreeAggregates = {}; + Object.entries({ ...fieldsAndOpToRetrieve }).forEach( + ([field, operations]) => { + emptyAggregates[field] = operations.map((operation) => { + const fieldDefinition = ooui.columns.find( + (it) => it.id === field, + ); + return { + operation, + label: + (fieldDefinition?.[ + `_${operation}` as keyof typeof fieldDefinition + ] as string) || "", + amount: "-", + }; + }); + }, + ); + setAggregates(emptyAggregates); + return; + } else if (!domain) { + setAggregates(undefined); + return; + } + const retrievedData = await readAggregates({ model, domain, aggregateFields: fieldsAndOpToRetrieve, }); + let result: TreeAggregates; Object.entries(retrievedData).forEach((key) => { const field: string = key[0]; @@ -88,12 +124,18 @@ export const useTreeAggregates = ({ }); setAggregates(result); } catch (err) { + setAggregates(undefined); console.error(err); + } finally { + setLoading(false); } - }, [domain, fieldsAndOpToRetrieve, model, ooui.columns, readAggregates]); + }, [domain, fieldsAndOpToRetrieve, model, ooui?.columns, readAggregates]); useDeepCompareEffect(() => { - if (Object.keys(fieldsAndOpToRetrieve).length === 0) { + if ( + !fieldsAndOpToRetrieve || + Object.keys(fieldsAndOpToRetrieve).length === 0 + ) { return; } fetchData(); @@ -103,5 +145,9 @@ export const useTreeAggregates = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [fieldsAndOpToRetrieve, domain]); - return aggregates; + const hasAggregates = + fieldsAndOpToRetrieve !== undefined && + Object.keys(fieldsAndOpToRetrieve).length > 0; + + return [loading, aggregates, hasAggregates]; }; diff --git a/src/widgets/base/one2many/useOne2manyColumnLocalStorage.ts b/src/widgets/base/one2many/useTreeColumnLocalStorage.ts similarity index 52% rename from src/widgets/base/one2many/useOne2manyColumnLocalStorage.ts rename to src/widgets/base/one2many/useTreeColumnLocalStorage.ts index 65d0550e..7fc820e5 100644 --- a/src/widgets/base/one2many/useOne2manyColumnLocalStorage.ts +++ b/src/widgets/base/one2many/useTreeColumnLocalStorage.ts @@ -1,26 +1,26 @@ import { ColumnState } from "@gisce/react-formiga-table"; -import { One2manyTreeDataForHash } from "./One2manyTree"; import { useDeepCompareCallback } from "use-deep-compare"; -import { getKey } from "./useOne2manyColumnStorage"; -export type DataForHashWithModel = One2manyTreeDataForHash & { model: string }; - -export const useOne2manyColumnLocalStorage = ( - dataForHash: DataForHashWithModel, -) => { +export const useTreeColumnLocalStorage = (key?: string) => { const getColumnState = useDeepCompareCallback(async (): Promise< ColumnState[] | undefined > => { + if (!key) { + return undefined; + } // Get the column state values from the localstorage for the curent model - const columnState = localStorage.getItem(getKey(dataForHash)); + const columnState = localStorage.getItem(key); return columnState ? JSON.parse(columnState) : undefined; - }, [dataForHash]); + }, [key]); const updateColumnState = useDeepCompareCallback( async (state: ColumnState[]) => { - localStorage.setItem(getKey(dataForHash), JSON.stringify(state)); + if (!key) { + return; + } + localStorage.setItem(key, JSON.stringify(state)); }, - [dataForHash], + [key], ); return { getColumnState, updateColumnState }; diff --git a/src/widgets/base/one2many/useOne2manyColumnRemoteStorage.ts b/src/widgets/base/one2many/useTreeColumnRemoteStorage.ts similarity index 78% rename from src/widgets/base/one2many/useOne2manyColumnRemoteStorage.ts rename to src/widgets/base/one2many/useTreeColumnRemoteStorage.ts index 535e142b..a41f725c 100644 --- a/src/widgets/base/one2many/useOne2manyColumnRemoteStorage.ts +++ b/src/widgets/base/one2many/useTreeColumnRemoteStorage.ts @@ -1,16 +1,10 @@ import { ColumnState } from "@gisce/react-formiga-table"; -import { One2manyTreeDataForHash } from "./One2manyTree"; import { useDeepCompareCallback } from "use-deep-compare"; import ConnectionProvider from "@/ConnectionProvider"; import { useNetworkRequest } from "@/hooks/useNetworkRequest"; -import { getKey } from "./useOne2manyColumnStorage"; import { useEffect } from "react"; -export type DataForHashWithModel = One2manyTreeDataForHash & { model: string }; - -export const useOne2manyColumnRemoteStorage = ( - dataForHash: DataForHashWithModel, -) => { +export const useTreeColumnRemoteStorage = (key?: string) => { useEffect(() => { return () => { cancelReadRequest(); @@ -30,17 +24,23 @@ export const useOne2manyColumnRemoteStorage = ( const getColumnState = useDeepCompareCallback(async (): Promise< ColumnState[] | undefined > => { + if (!key) { + throw new Error("Unknown column state key"); + } const state = await read({ - key: getKey(dataForHash), + key, }); if (state === false) { throw new Error("Empty column state"); } return state; - }, [dataForHash]); + }, [key]); const updateColumnState = useDeepCompareCallback( async (state: ColumnState[]) => { + if (!key) { + return; + } // state is an array of objects but we need to remove the properties of each object that are null // to avoid sending them to the backend const stateWithoutNulls = state.map((column) => @@ -50,11 +50,11 @@ export const useOne2manyColumnRemoteStorage = ( ); return save({ - key: getKey(dataForHash), + key, preferences: stateWithoutNulls, }); }, - [dataForHash], + [key], ); return { getColumnState, updateColumnState }; diff --git a/src/widgets/base/one2many/useOne2manyColumnStorage.ts b/src/widgets/base/one2many/useTreeColumnStorage.ts similarity index 68% rename from src/widgets/base/one2many/useOne2manyColumnStorage.ts rename to src/widgets/base/one2many/useTreeColumnStorage.ts index 358c9ed1..ec0f5dbf 100644 --- a/src/widgets/base/one2many/useOne2manyColumnStorage.ts +++ b/src/widgets/base/one2many/useTreeColumnStorage.ts @@ -1,14 +1,11 @@ import { ColumnState } from "@gisce/react-formiga-table"; -import { One2manyTreeDataForHash } from "./One2manyTree"; import { useDeepCompareCallback } from "use-deep-compare"; import { useFeatureIsEnabled } from "@/context/ConfigContext"; import { ErpFeatureKeys } from "@/models/erpFeature"; -import { useOne2manyColumnLocalStorage } from "./useOne2manyColumnLocalStorage"; -import { useOne2manyColumnRemoteStorage } from "./useOne2manyColumnRemoteStorage"; +import { useTreeColumnLocalStorage } from "./useTreeColumnLocalStorage"; +import { useTreeColumnRemoteStorage } from "./useTreeColumnRemoteStorage"; -export type DataForHashWithModel = One2manyTreeDataForHash & { model: string }; - -export const useOne2manyColumnStorage = (dataForHash: DataForHashWithModel) => { +export const useTreeColumnStorage = (key?: string) => { const remoteUserViewPrefsEnabled = useFeatureIsEnabled( ErpFeatureKeys.FEATURE_USERVIEWPREFS, ); @@ -16,12 +13,12 @@ export const useOne2manyColumnStorage = (dataForHash: DataForHashWithModel) => { const { getColumnState: getLocalColumnState, updateColumnState: updateLocalColumnState, - } = useOne2manyColumnLocalStorage(dataForHash); + } = useTreeColumnLocalStorage(key); const { getColumnState: getRemoteColumnState, updateColumnState: updateRemoteColumnState, - } = useOne2manyColumnRemoteStorage(dataForHash); + } = useTreeColumnRemoteStorage(key); const getColumnState = useDeepCompareCallback(async (): Promise< ColumnState[] | undefined @@ -35,7 +32,7 @@ export const useOne2manyColumnStorage = (dataForHash: DataForHashWithModel) => { console.error(err); return getLocalColumnState(); } - }, [dataForHash]); + }, [key]); const updateColumnState = useDeepCompareCallback( async (state: ColumnState[]) => { @@ -55,12 +52,8 @@ export const useOne2manyColumnStorage = (dataForHash: DataForHashWithModel) => { return updateLocalColumnState(columnStatesWithoutSort); } }, - [dataForHash], + [key], ); return { getColumnState, updateColumnState }; }; - -export const getKey = (dataForHash: DataForHashWithModel) => { - return `columnState-${dataForHash.parentViewId}-${dataForHash.treeViewId}-${dataForHash.one2ManyFieldName}-${dataForHash.model}`; -}; diff --git a/src/widgets/base/one2many/useOne2manyColumnStorageFetch.ts b/src/widgets/base/one2many/useTreeColumnStorageFetch.ts similarity index 64% rename from src/widgets/base/one2many/useOne2manyColumnStorageFetch.ts rename to src/widgets/base/one2many/useTreeColumnStorageFetch.ts index c3f052cf..63f049a9 100644 --- a/src/widgets/base/one2many/useOne2manyColumnStorageFetch.ts +++ b/src/widgets/base/one2many/useTreeColumnStorageFetch.ts @@ -1,20 +1,18 @@ import { ColumnState } from "@gisce/react-formiga-table"; -import { One2manyTreeDataForHash } from "./One2manyTree"; -import { useOne2manyColumnStorage } from "./useOne2manyColumnStorage"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useTreeColumnStorage } from "./useTreeColumnStorage"; -export type DataForHashWithModel = One2manyTreeDataForHash & { model: string }; - -export const useOne2manyColumnStorageFetch = ( - dataForHash: DataForHashWithModel, -) => { +export const useTreeColumnStorageFetch = (key?: string) => { const [loading, setLoading] = useState(true); const columnState = useRef(undefined); const { getColumnState: getColumnStateInternal, updateColumnState } = - useOne2manyColumnStorage(dataForHash); + useTreeColumnStorage(key); useEffect(() => { + if (!key) { + return; + } const fetchColumnState = async () => { setLoading(true); try { @@ -27,8 +25,7 @@ export const useOne2manyColumnStorageFetch = ( }; fetchColumnState(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [getColumnStateInternal, key]); const getColumnState = useCallback(() => { return columnState.current; diff --git a/src/widgets/views/SearchTreeHeader.tsx b/src/widgets/views/SearchTreeHeader.tsx new file mode 100644 index 00000000..cde3eb82 --- /dev/null +++ b/src/widgets/views/SearchTreeHeader.tsx @@ -0,0 +1,66 @@ +import { useLocale } from "@gisce/react-formiga-components"; +import { Row, Col, Spin, Typography } from "antd"; +const { Text } = Typography; + +export type SearchTreeHeaderProps = { + totalRows?: number | null; + selectedRowKeys: number[]; + allRowSelectedMode: boolean; +}; + +export const SearchTreeHeader = ({ + totalRows, + selectedRowKeys, + allRowSelectedMode, +}: SearchTreeHeaderProps) => { + const { t } = useLocale(); + + return ( + + + {allRowSelectedMode ? ( + {`${selectedRowKeys.length} ${t("selectedRegisters")}`} + ) : ( + + )} + + + {totalRows === undefined && } + {totalRows !== null && + totalRows !== undefined && + `${t("totalRegisters")}: ${totalRows}`} + + + ); +}; + +const SearchTreeSelectionSummary = ({ + selectedRowKeys, +}: { + selectedRowKeys: number[]; +}) => { + const { t } = useLocale(); + if (selectedRowKeys.length === 1) { + return ( + <> + 1 {t("selectedRegisters")} - (id:{" "} + {selectedRowKeys[0]}) + + ); + } else if (selectedRowKeys.length > 1) { + return ( + <> + {selectedRowKeys.length} {t("selectedRegisters") + " "} + + + ); + } +}; diff --git a/src/widgets/views/SearchTreeInfinite.tsx b/src/widgets/views/SearchTreeInfinite.tsx new file mode 100644 index 00000000..17431d06 --- /dev/null +++ b/src/widgets/views/SearchTreeInfinite.tsx @@ -0,0 +1,522 @@ +import { + Fragment, + RefObject, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; + +import { FormView, TreeView } from "@/types/index"; + +import { useFetchTreeViews } from "@/hooks/useFetchTreeViews"; +import { Badge, Spin } from "antd"; +import { + getColorMap, + getOrderFromSortFields, + getStatusMap, + getTableColumns, + getTableItems, + getTree, +} from "@/helpers/treeHelper"; +import { COLUMN_COMPONENTS } from "./Tree/treeComponents"; +import { useDeepCompareMemo } from "use-deep-compare"; +import { + InfiniteTable, + InfiniteTableRef, + SortDirection, +} from "@gisce/react-formiga-table"; +import ConnectionProvider from "@/ConnectionProvider"; +import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useActionViewContext } from "@/context/ActionViewContext"; +import { mergeSearchFields } from "@/helpers/formHelper"; +import { useTreeColumnStorageFetch } from "../base/one2many/useTreeColumnStorageFetch"; +import { getKey } from "@/helpers/tree-columnStorageHelper"; +import { useTreeAggregates } from "../base/one2many/useTreeAggregates"; +import { AggregatesFooter } from "../base/one2many/AggregatesFooter"; +import { SearchTreeHeader } from "./SearchTreeHeader"; +import { useLocale } from "@gisce/react-formiga-components"; +import showConfirmDialog from "@/ui/ConfirmDialog"; +import { SideSearchFilter } from "./searchFilter/SideSearchFilter"; +import { mergeParams } from "@/helpers/searchHelper"; +import useDeepCompareEffect from "use-deep-compare-effect"; +import deepEqual from "deep-equal"; +import { useShowErrorDialog } from "@/ui/GenericErrorDialog"; + +export const HEIGHT_OFFSET = 10; +export const MAX_ROWS_TO_SELECT = 200; + +type OnRowClickedData = { + id: number; + model: string; + formView: FormView; + treeView: TreeView; +}; + +type SearchTreeInfiniteProps = { + model: string; + formView?: FormView; + treeView?: TreeView; + onRowClicked: (data: OnRowClickedData) => void; + nameSearch?: string; + treeScrollY?: number; + domain?: any; + visible?: boolean; + rootTree?: boolean; + parentContext?: any; + onChangeSelectedRowKeys?: (selectedRowKeys: any) => void; +}; + +function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { + const { + model, + formView: formViewProps, + treeView: treeViewProps, + onRowClicked, + domain = [], + visible = true, + rootTree = false, + parentContext = {}, + onChangeSelectedRowKeys, + nameSearch: nameSearchProps, + } = props; + const colorsForResults = useRef<{ [key: number]: string }>({}); + const statusForResults = useRef<{ [key: number]: string }>(); + const tableRef: RefObject = useRef(null); + const lastAssignedResults = useRef([]); + const showErrorDialog = useShowErrorDialog(); + + const [totalRows, setTotalRows] = useState(); + + const { t } = useLocale(); + + useImperativeHandle(ref, () => ({ + refreshResults: () => { + tableRef?.current?.refresh(); + }, + getFields: () => treeView?.fields, + getDomain: () => domain, + })); + + const containerRef = useRef(null); + const availableHeight = useAvailableHeight({ + elementRef: containerRef, + offset: HEIGHT_OFFSET, + }); + + const { treeView, formView, loading } = useFetchTreeViews({ + model, + formViewProps, + treeViewProps, + context: parentContext, + }); + + const { + setTreeIsLoading, + searchVisible = false, + setSearchVisible, + setSelectedRowItems, + setTreeFirstVisibleRow, + treeFirstVisibleRow, + selectedRowItems, + setSearchParams, + searchValues, + searchParams, + setSearchValues, + searchTreeNameSearch, + setSearchTreeNameSearch, + } = useActionViewContext(rootTree); + + const nameSearch = nameSearchProps || searchTreeNameSearch; + + useEffect(() => { + setSelectedRowItems?.([]); + setSearchParams?.([]); + setSearchValues?.({}); + tableRef.current?.unselectAll(); + tableRef.current?.refresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nameSearch]); + + const treeOoui = useMemo(() => { + if (!treeView) { + return; + } + return getTree(treeView); + }, [treeView]); + + const columns = useDeepCompareMemo(() => { + if (!treeOoui) { + return; + } + return getTableColumns( + treeOoui, + { + ...COLUMN_COMPONENTS, + }, + parentContext, + ); + }, [treeOoui, parentContext]); + + const columnStateKey = useMemo(() => { + return getKey({ + treeViewId: treeView?.view_id, + model, + }); + }, [model, treeView?.view_id]); + + const { + loading: getColumnStateInProgress, + getColumnState, + updateColumnState, + } = useTreeColumnStorageFetch(columnStateKey); + + const mergedParams = useMemo( + () => mergeParams(searchParams || [], domain), + [domain, searchParams], + ); + + const fetchResults = useCallback( + async ({ + startRow, + endRow, + sortFields, + }: { + startRow: number; + endRow: number; + sortFields?: Record; + }) => { + if (!treeOoui) { + return []; + } + + const attrs: any = {}; + if (treeOoui.colors) { + attrs.colors = treeOoui.colors; + } + if (treeOoui.status) { + attrs.status = treeOoui.status; + } + + const { + totalItems: totalItemsPromise, + results, + attrsEvaluated, + } = await ConnectionProvider.getHandler().searchForTree({ + params: nameSearch ? domain : mergedParams, + limit: endRow - startRow, + offset: startRow, + model, + fields: treeView!.field_parent + ? { ...treeView!.fields, [treeView!.field_parent]: {} } + : treeView!.fields, + context: parentContext, + attrs, + order: getOrderFromSortFields(sortFields), + name_search: nameSearch, + }); + + if (results.length === 0) { + lastAssignedResults.current = []; + setTotalRows(0); + return []; + } + + // TODO: maybe we could improve this somehow + Promise.resolve().then(async () => { + totalItemsPromise.then((totalItems) => { + setTotalRows(totalItems); + }); + }); + + const preparedResults = getTableItems(treeOoui, results); + + const colors = getColorMap(attrsEvaluated); + + colorsForResults.current = { + ...colorsForResults.current, + ...colors, + }; + + if (!statusForResults.current && treeOoui.status) { + statusForResults.current = {}; + } + + if (treeOoui.status) { + const status = getStatusMap(attrsEvaluated); + statusForResults.current = { + ...statusForResults.current, + ...status, + }; + } + + lastAssignedResults.current = [...preparedResults]; + return preparedResults; + }, + [ + domain, + mergedParams, + model, + nameSearch, + parentContext, + treeOoui, + treeView, + ], + ); + + const changeSelectedRowKeys = useCallback( + (newSelectedRowKeys: any[]) => { + setSelectedRowItems?.(newSelectedRowKeys.map((id: number) => ({ id }))); + onChangeSelectedRowKeys?.(newSelectedRowKeys); + }, + [onChangeSelectedRowKeys, setSelectedRowItems], + ); + + const onRequestData = useCallback( + async ({ + startRow, + endRow, + sortFields, + }: { + startRow: number; + endRow: number; + sortFields?: Record; + }) => { + try { + setTreeIsLoading?.(true); + setTotalRows(undefined); + const results = await fetchResults({ + startRow, + endRow, + sortFields, + }); + return results; + } catch (error) { + console.error(error); + setTotalRows(null); + showErrorDialog(error); + throw error; + } finally { + setTreeIsLoading?.(false); + } + }, + [fetchResults, setTreeIsLoading, showErrorDialog], + ); + + const onRowStyle = useCallback((record: any) => { + if (colorsForResults.current[record.node?.data?.id]) { + return { color: colorsForResults.current[record.node?.data?.id] }; + } + return undefined; + }, []); + + const selectedRowKeys = useMemo(() => { + return selectedRowItems?.map((item) => item.id) || []; + }, [selectedRowItems]); + + const [loadingAggregates, aggregates, hasAggregates] = useTreeAggregates({ + ooui: treeOoui, + model, + showEmptyValues: true, + domain: + selectedRowKeys?.length > 0 + ? // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + [["id", "in", selectedRowKeys.sort()]] + : undefined, + }); + + const onSelectionCheckboxClicked = useCallback(async () => { + let mustSelectAll = false; + if (selectedRowItems?.length === 0) { + mustSelectAll = true; + } else { + mustSelectAll = false; + } + + const selectAllPromise = async () => { + if (nameSearch) { + setSelectedRowItems?.(lastAssignedResults.current); + return; + } + + if (!totalRows) { + return; + } + + const allRowsResults = await ConnectionProvider.getHandler().searchAllIds( + { + params: nameSearch ? domain : mergedParams, + model, + context: parentContext, + totalItems: totalRows, + }, + ); + setSelectedRowItems?.(allRowsResults.map((id: number) => ({ id }))); + }; + + if (mustSelectAll) { + if (totalRows && totalRows > MAX_ROWS_TO_SELECT) { + showConfirmDialog({ + confirmMessage: t("confirmSelectAllRegisters").replace( + "{totalRecords}", + totalRows.toString(), + ), + t, + onOk: selectAllPromise, + }); + } else { + selectAllPromise(); + } + } else { + setSelectedRowItems?.([]); + } + }, [ + domain, + mergedParams, + model, + nameSearch, + parentContext, + selectedRowItems?.length, + setSelectedRowItems, + t, + totalRows, + ]); + + const firstVisibleRowIndex = useCallback(() => { + return treeFirstVisibleRow; + }, [treeFirstVisibleRow]); + + const footerComp = useMemo(() => { + if (!hasAggregates) { + return null; + } + return ( + + ); + }, [aggregates, loadingAggregates, hasAggregates]); + + const statusComp = useCallback((status: any) => { + return ; + }, []); + + const onRowStatus = useCallback( + (record: any) => statusForResults.current?.[record.id], + [], + ); + + const content = useMemo(() => { + if (!columns || !treeOoui) { + return null; + } + + return ( + + ); + }, [ + availableHeight, + changeSelectedRowKeys, + columns, + firstVisibleRowIndex, + footerComp, + getColumnState, + onRequestData, + onRowClicked, + onRowStatus, + onRowStyle, + onSelectionCheckboxClicked, + selectedRowKeys, + setTreeFirstVisibleRow, + statusComp, + totalRows, + treeOoui, + updateColumnState, + ]); + + const prevSearchParamsRef = useRef(searchParams); + const prevSearchVisibleRef = useRef(searchVisible); + + useDeepCompareEffect(() => { + const searchParamsChanged = !deepEqual( + searchParams, + prevSearchParamsRef.current, + ); + const searchVisibleChangedToFalse = + prevSearchVisibleRef.current && !searchVisible; + + if (searchParamsChanged && searchVisibleChangedToFalse) { + tableRef.current?.refresh(); + } + + prevSearchParamsRef.current = searchParams; + prevSearchVisibleRef.current = searchVisible; + }, [searchParams, searchVisible]); + + return ( + + +
+ {loading || getColumnStateInProgress ? ( + + ) : ( + + {content} + setSearchVisible?.(false)} + fields={{ ...formView?.fields, ...treeView?.fields }} + searchFields={mergeSearchFields([ + formView?.search_fields, + treeView?.search_fields, + ])} + onSubmit={({ params, values }) => { + setSelectedRowItems?.([]); + tableRef.current?.unselectAll(); + setSearchTreeNameSearch?.(undefined); + setSearchParams?.(params); + setSearchValues?.(values); + setSearchVisible?.(false); + }} + searchValues={searchValues} + /> + + )} +
+
+ ); +} + +export const SearchTreeInfinite = forwardRef(SearchTreeInfiniteComp); diff --git a/src/widgets/views/Tree/treeComponents.tsx b/src/widgets/views/Tree/treeComponents.tsx index 8de98773..aab69757 100644 --- a/src/widgets/views/Tree/treeComponents.tsx +++ b/src/widgets/views/Tree/treeComponents.tsx @@ -221,7 +221,7 @@ export const TagsComponent = ({ } finally { setLoading(false); } - }, [context, field, relation, value.items]); + }, [context, field, relation, value?.items]); useEffect(() => { if (value?.items && value?.items.length > 0) { diff --git a/src/widgets/views/searchFilter/DateRangePicker.tsx b/src/widgets/views/searchFilter/DateRangePicker.tsx index 1d8f4bdc..ee1daf62 100644 --- a/src/widgets/views/searchFilter/DateRangePicker.tsx +++ b/src/widgets/views/searchFilter/DateRangePicker.tsx @@ -13,6 +13,7 @@ export const DateRangePicker = (props: WidgetProps) => { allowEmpty={[true, true]} format={"DD/MM/YYYY"} locale={datePickerLocale} + style={{ width: "100%" }} > ); diff --git a/src/widgets/views/searchFilter/PairFields.tsx b/src/widgets/views/searchFilter/PairFields.tsx index f3aca48e..2bbdab47 100644 --- a/src/widgets/views/searchFilter/PairFields.tsx +++ b/src/widgets/views/searchFilter/PairFields.tsx @@ -30,7 +30,7 @@ export function PairFields(props: WidgetProps): React.ReactElement { }; return ( - <> +
{showLabel && (
); } diff --git a/src/widgets/views/searchFilter/SideSearchFilter.tsx b/src/widgets/views/searchFilter/SideSearchFilter.tsx new file mode 100644 index 00000000..215a9e7e --- /dev/null +++ b/src/widgets/views/searchFilter/SideSearchFilter.tsx @@ -0,0 +1,267 @@ +import { + Fragment, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; +import { Form, Alert, Button, FormInstance } from "antd"; +import useDeepCompareEffect from "use-deep-compare-effect"; +import { SearchOutlined, ClearOutlined } from "@ant-design/icons"; + +import { + SearchFilter as SearchFilterOoui, + Container, + Field, +} from "@gisce/ooui"; + +import { SearchField } from "./SearchField"; +import { SearchFields } from "@/types"; + +import { getParamsForFields } from "@/helpers/searchHelper"; +import { useLocale } from "@gisce/react-formiga-components"; +import { FloatingDrawer } from "@/ui/FloatingDrawer"; +import debounce from "lodash.debounce"; +import deepEqual from "deep-equal"; +import { set } from "lodash"; + +type SideSearchFilterBaseProps = { + onSubmit: (values: any) => void; + searchValues?: any; + values?: any; +}; + +type SideSearchFilterContainerProps = SideSearchFilterBaseProps & { + fields: any; + searchFields: SearchFields; + isOpen: boolean; + onClose: () => void; +}; + +export type SideSearchFilterProps = SideSearchFilterBaseProps & { + searchFields?: Container; + onChange?: (values: any) => void; +}; + +// eslint-disable-next-line react/display-name +export const SideSearchFilterComponent = forwardRef( + (props, ref) => { + const { onSubmit, searchValues, searchFields, onChange } = props; + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(searchValues); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchValues]); + + useImperativeHandle(ref, () => ({ + submit: form.submit, + resetFields: form.resetFields, + setFieldsValue: form.setFieldsValue, + })); + + const getRowsAndCols = () => { + if (!searchFields) return; + + const rows = searchFields?.rows; + + const formValues = normalizeValues(form.getFieldsValue()); + + return rows?.map((row, i) => { + return row.map((item, j) => { + const hasValue = formValues[(item as Field).id] !== undefined; + return ( +
+
+ +
+
+ ); + }); + }); + }; + + const rows = getRowsAndCols(); + + const checkFieldsChanges = useCallback(() => { + const touchedValues = form.getFieldsValue(); + onChange?.(touchedValues); + }, [form, onChange]); + + const debouncedCheckFieldsChanges = debounce(checkFieldsChanges, 100); + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + form.submit(); + } + }; + return ( + +
+ {rows} +
+
+ + ); + }, +); + +export const SideSearchFilter = (props: SideSearchFilterContainerProps) => { + const { onSubmit, isOpen, onClose, searchFields, fields, searchValues } = + props; + const sfo = useRef(); + const { t } = useLocale(); + const [parsedSearchFields, setParsedSearchFields] = useState(); + const formRef = useRef(null); + const [searchParams, setSearchParams] = useState(); + + useEffect(() => { + if (!isOpen) { + return; + } + setSearchParams(undefined); + }, [isOpen]); + + useDeepCompareEffect(() => { + if (!isOpen) return; + sfo.current = new SearchFilterOoui(searchFields, fields, 1); + sfo.current.parse(); + setParsedSearchFields(sfo.current._advancedSearchContainer); + }, [fields, searchFields, isOpen]); + + const onFinish = useCallback( + (values: any) => { + const newParams = getParamsForFields( + values, + sfo.current?._advancedSearchContainer, + ); + onSubmit({ params: newParams, values: normalizeValues(values) }); + }, + [onSubmit], + ); + + const handleSubmit = useCallback(() => { + formRef.current?.submit(); + }, []); + + const handleOnChange = useCallback( + (values: any) => { + const convertedValues = normalizeValues(values); + + if (deepEqual(convertedValues, searchValues)) { + setSearchParams([]); + return; + } + const newParams = getParamsForFields( + values, + sfo.current?._advancedSearchContainer, + ); + setSearchParams(newParams); + }, + [searchValues], + ); + + const handleClear = useCallback(() => { + formRef.current?.resetFields(); + formRef?.current?.setFieldsValue({}); + setSearchParams([]); + }, []); + + const paramsToShow = + searchParams || + getParamsForFields(searchValues, sfo.current?._advancedSearchContainer); + + return ( + + } + > + + + ); +}; + +export const SideSearchFooter = ({ + onClear, + onSubmit, + searchParams, +}: { + onClear: () => void; + onSubmit: () => void; + searchParams?: any[]; +}) => { + const { t } = useLocale(); + + return ( +
+ + +
+ ); +}; + +const normalizeValues = (values: any) => { + // values object should be converted: fields that are empty strings should be undefined + return Object.keys(values).reduce((acc: any, key) => { + const value = values[key]; + if (value !== "" && value !== undefined) { + acc[key] = value; + } + return acc; + }, {}); +};