diff --git a/src/actionbar/ShareUrlButton.tsx b/src/actionbar/ShareUrlButton.tsx index 67b70a4cb..b9cf437e5 100644 --- a/src/actionbar/ShareUrlButton.tsx +++ b/src/actionbar/ShareUrlButton.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef } from "react"; import { Button, Input, message, Space, Popover, theme } from "antd"; import { CopyOutlined, CheckOutlined } from "@ant-design/icons"; import { useLocale } from "@gisce/react-formiga-components"; @@ -15,6 +15,7 @@ export type ShareUrlButtonProps = { }; export function ShareUrlButton({ res_id, searchParams }: ShareUrlButtonProps) { + const buttonRef = useRef(null); const { currentView } = useActionViewContext(); const initialView = { id: currentView.view_id, @@ -118,12 +119,14 @@ export function ShareUrlButton({ res_id, searchParams }: ShareUrlButtonProps) { return (
- } - disabled={moreDataNeededForCopying} - tooltip={t("share")} - /> +
+ } + disabled={moreDataNeededForCopying} + tooltip={t("share")} + /> +
); diff --git a/src/actionbar/TreeActionBar.tsx b/src/actionbar/TreeActionBar.tsx index e01f5cd3f..4d790ceae 100644 --- a/src/actionbar/TreeActionBar.tsx +++ b/src/actionbar/TreeActionBar.tsx @@ -79,7 +79,7 @@ function TreeActionBarComponent({ limit, totalItems, isActive, - isInfiniteTree, + treeType, } = useContext(ActionViewContext) as ActionViewContextType; const advancedExportEnabled = useFeatureIsEnabled( @@ -183,12 +183,12 @@ function TreeActionBarComponent({ setSearchTreeNameSearch?.(searchString); } else { setSearchTreeNameSearch?.(undefined); - if (!isInfiniteTree) { + if (treeType !== "infinite") { searchTreeRef?.current?.refreshResults(); } } }, - [isInfiniteTree, searchTreeRef, setSearchTreeNameSearch], + [treeType, searchTreeRef, setSearchTreeNameSearch], ); const handleExportAction = useCallback( @@ -220,14 +220,14 @@ function TreeActionBarComponent({ ); useEffect(() => { - if (isInfiniteTree && searchTreeNameSearch === undefined) { + if (treeType === "infinite" && searchTreeNameSearch === undefined) { if (isFirstMount.current) { isFirstMount.current = false; return; } searchTreeRef?.current?.refreshResults(); } - }, [isInfiniteTree, searchTreeNameSearch, searchTreeRef]); + }, [treeType, searchTreeNameSearch, searchTreeRef]); useHotkeys( "ctrl+l,command+l", diff --git a/src/actionbar/useNextPrevious.ts b/src/actionbar/useNextPrevious.ts index 4dceffa30..b198e7d62 100644 --- a/src/actionbar/useNextPrevious.ts +++ b/src/actionbar/useNextPrevious.ts @@ -6,7 +6,7 @@ import { useShowErrorDialog } from "@/ui/GenericErrorDialog"; export const useNextPrevious = () => { const { - isInfiniteTree, + treeType, totalItems, currentItemIndex, setCurrentId, @@ -109,20 +109,20 @@ export const useNextPrevious = () => { ); const onNextClick = useCallback(() => { - if (isInfiniteTree) { + if (treeType === "infinite") { handleInfiniteNavigation("next"); } else { handleFiniteNavigation("next"); } - }, [isInfiniteTree, handleInfiniteNavigation, handleFiniteNavigation]); + }, [treeType, handleInfiniteNavigation, handleFiniteNavigation]); const onPreviousClick = useCallback(() => { - if (isInfiniteTree) { + if (treeType === "infinite") { handleInfiniteNavigation("previous"); } else { handleFiniteNavigation("previous"); } - }, [isInfiniteTree, handleInfiniteNavigation, handleFiniteNavigation]); + }, [treeType, handleInfiniteNavigation, handleFiniteNavigation]); return { onNextClick, diff --git a/src/context/ActionViewContext.tsx b/src/context/ActionViewContext.tsx index fc09645d6..e9839d373 100644 --- a/src/context/ActionViewContext.tsx +++ b/src/context/ActionViewContext.tsx @@ -1,6 +1,10 @@ import { convertParamsToValues } from "@/helpers/searchHelper"; import { DEFAULT_SEARCH_LIMIT } from "@/models/constants"; import { TreeView, View } from "@/types"; +import { + DEFAULT_TREE_TYPE, + TreeType, +} from "@/views/actionViews/TreeActionView"; import { ColumnState } from "@gisce/react-formiga-table"; import { createContext, useContext, useEffect, useState } from "react"; @@ -24,7 +28,7 @@ type ActionViewProviderProps = { totalItems: number; setTotalItems: (totalItems: number) => void; selectedRowItems?: any[]; - setSelectedRowItems: (value: any[]) => void; + setSelectedRowItems: (value: any[] | ((prevValue: any[]) => any[])) => void; setSearchTreeNameSearch: (searchString?: string) => void; searchTreeNameSearch?: string; goToResourceId: (ids: number[], openInSameTab?: boolean) => Promise; @@ -32,6 +36,8 @@ type ActionViewProviderProps = { isActive: boolean; children: React.ReactNode; initialSearchParams?: any[]; + initialCurrentPage?: number; + initialOrder?: any[]; }; export type ActionViewContextType = Omit< @@ -66,13 +72,17 @@ export type ActionViewContextType = Omit< setLimit?: (value: number) => void; setTitle?: (value: string) => void; treeFirstVisibleRow: number; - setTreeFirstVisibleRow: (totalItems: number) => void; + setTreeFirstVisibleRow: (value: number) => void; + treeFirstVisibleColumn: string | undefined; + setTreeFirstVisibleColumn: (value: string | undefined) => void; searchQuery?: SearchQueryParams; setSearchQuery?: (value: SearchQueryParams) => void; - isInfiniteTree?: boolean; - setIsInfiniteTree?: (value: boolean) => void; - sortState?: ColumnState[]; - setSortState?: (value: ColumnState[] | undefined) => void; + treeType?: TreeType; + setTreeType?: (value: TreeType) => void; + order?: ColumnState[]; + setOrder?: (value: ColumnState[] | undefined) => void; + currentPage?: number; + setCurrentPage?: (value: number) => void; }; export const ActionViewContext = createContext( @@ -116,6 +126,8 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { limit: limitProps, isActive, initialSearchParams, + initialCurrentPage, + initialOrder, } = props; const [formIsSaving, setFormIsSaving] = useState(false); @@ -138,20 +150,30 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { ), ); const [treeFirstVisibleRow, setTreeFirstVisibleRow] = useState(0); + const [treeFirstVisibleColumn, setTreeFirstVisibleColumn] = useState< + string | undefined + >(undefined); const [searchQuery, setSearchQuery] = useState(); - const [isInfiniteTree, setIsInfiniteTree] = useState(false); - const [sortState, setSortState] = useState(); + const [treeType, setTreeType] = useState(DEFAULT_TREE_TYPE); + const [order, setOrder] = useState( + initialOrder as ColumnState[] | undefined, + ); const [limit, setLimit] = useState( limitProps !== undefined ? limitProps : DEFAULT_SEARCH_LIMIT, ); const [title, setTitle] = useState(titleProps); + const [currentPage, setCurrentPage] = useState( + initialCurrentPage || 1, + ); + useEffect(() => { if (results && results.length > 0 && !currentItemIndex) { setCurrentItemIndex?.(0); setCurrentId?.(results[0].id); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [results]); useEffect(() => { @@ -244,12 +266,16 @@ const ActionViewProvider = (props: ActionViewProviderProps): any => { isActive, setTreeFirstVisibleRow, treeFirstVisibleRow, + treeFirstVisibleColumn, + setTreeFirstVisibleColumn, searchQuery, setSearchQuery, - isInfiniteTree, - setIsInfiniteTree, - sortState, - setSortState, + treeType, + setTreeType, + order, + setOrder, + currentPage, + setCurrentPage, }} > {children} @@ -322,12 +348,16 @@ export const useActionViewContext = () => { setTitle: () => {}, treeFirstVisibleRow: 0, setTreeFirstVisibleRow: () => {}, + treeFirstVisibleColumn: undefined, + setTreeFirstVisibleColumn: () => {}, searchQuery: undefined, setSearchQuery: () => {}, - isInfiniteTree: false, - setIsInfiniteTree: () => {}, - sortState: undefined, - setSortState: () => {}, + treeType: DEFAULT_TREE_TYPE, + setTreeType: () => {}, + order: undefined, + setOrder: () => {}, + currentPage: 1, + setCurrentPage: () => {}, }; } diff --git a/src/helpers/treeHelper.tsx b/src/helpers/treeHelper.tsx index d48faad53..82dfd878a 100644 --- a/src/helpers/treeHelper.tsx +++ b/src/helpers/treeHelper.tsx @@ -31,6 +31,7 @@ const getTableColumns = ( tree: TreeOoui, components: any, context: any, + treeType: "infinite" | "paginated" | "legacy", ): Column[] => { const tableColumns = tree.columns.map((column) => { const type = column.type; @@ -59,6 +60,15 @@ const getTableColumns = ( }; } + let isSortable = true; + + if (treeType === "legacy") { + isSortable = type !== "one2many"; + } else { + isSortable = + type !== "one2many" && type !== "many2one" && !column.isFunction; + } + return { key, dataIndex: key, @@ -77,8 +87,7 @@ const getTableColumns = ( if (aItem > bItem) return 1; return 0; }, - isSortable: - type !== "one2many" && type !== "many2one" && !column.isFunction, + isSortable, }; }); return tableColumns; diff --git a/src/hooks/useSearchTreeState.ts b/src/hooks/useSearchTreeState.ts index f02e4243a..a3594cbea 100644 --- a/src/hooks/useSearchTreeState.ts +++ b/src/hooks/useSearchTreeState.ts @@ -5,6 +5,12 @@ import { useIsUnderActionViewContext, } from "@/context/ActionViewContext"; import { ColumnState } from "@gisce/react-formiga-table"; +import { DEFAULT_PAGE_SIZE } from "@/widgets/views/Tree/Paginated/hooks/usePaginatedSearch"; +import { DEFAULT_SEARCH_LIMIT } from "@/models/constants"; +import { + DEFAULT_TREE_TYPE, + TreeType, +} from "@/views/actionViews/TreeActionView"; export type SearchTreeState = { treeIsLoading: boolean; @@ -12,9 +18,11 @@ export type SearchTreeState = { searchVisible: boolean; setSearchVisible: (value: boolean) => void; selectedRowItems: any[]; - setSelectedRowItems: (value: any[]) => void; + setSelectedRowItems: (value: any[] | ((prevValue: any[]) => any[])) => void; treeFirstVisibleRow: number; setTreeFirstVisibleRow: (value: number) => void; + treeFirstVisibleColumn: string | undefined; + setTreeFirstVisibleColumn: (value: string | undefined) => void; searchParams: any[]; setSearchParams: (value: any[]) => void; searchValues: any; @@ -28,8 +36,14 @@ export type SearchTreeState = { totalItems: number; setTotalItems: (value: number) => void; isActive?: boolean; - sortState?: ColumnState[]; - setSortState: (value: ColumnState[] | undefined) => void; + order?: ColumnState[]; + setOrder: (value: ColumnState[] | undefined) => void; + currentPage: number; + setCurrentPage: (value: number) => void; + treeType: TreeType; + setTreeType: (value: TreeType) => void; + limit: number; + setLimit: (value: number) => void; }; export function useSearchTreeState({ @@ -46,6 +60,8 @@ export function useSearchTreeState({ const [localSearchVisible, setLocalSearchVisible] = useState(false); const [localSelectedRowItems, setLocalSelectedRowItems] = useState([]); const [localTreeFirstVisibleRow, setLocalTreeFirstVisibleRow] = useState(0); + const [localTreeFirstVisibleColumn, setLocalTreeFirstVisibleColumn] = + useState(undefined); const [localSearchParams, setLocalSearchParams] = useState([]); const [localSearchValues, setLocalSearchValues] = useState({}); const [localSearchTreeNameSearch, setLocalSearchTreeNameSearch] = @@ -53,9 +69,11 @@ export function useSearchTreeState({ const [localResults, setLocalResults] = useState([]); const [localSearchQuery, setLocalSearchQuery] = useState(); const [localTotalItems, setLocalTotalItems] = useState(0); - const [localSortState, setLocalSortState] = useState< - ColumnState[] | undefined - >(); + const [localOrder, setLocalOrder] = useState(); + const [localCurrentPage, setLocalCurrentPage] = useState(1); + const [localTreeType, setLocalTreeType] = + useState(DEFAULT_TREE_TYPE); + const [localLimit, setLocalLimit] = useState(DEFAULT_SEARCH_LIMIT); // Return either context values or local state values based on isUnderActionViewContext return isUnderActionViewContext @@ -70,6 +88,9 @@ export function useSearchTreeState({ treeFirstVisibleRow: actionViewContext.treeFirstVisibleRow ?? 0, setTreeFirstVisibleRow: actionViewContext.setTreeFirstVisibleRow ?? (() => {}), + treeFirstVisibleColumn: actionViewContext.treeFirstVisibleColumn, + setTreeFirstVisibleColumn: + actionViewContext.setTreeFirstVisibleColumn ?? (() => {}), searchParams: actionViewContext.searchParams || [], setSearchParams: actionViewContext.setSearchParams ?? (() => {}), searchValues: actionViewContext.searchValues || {}, @@ -84,8 +105,14 @@ export function useSearchTreeState({ totalItems: actionViewContext.totalItems ?? 0, setTotalItems: actionViewContext.setTotalItems ?? (() => {}), isActive: actionViewContext.isActive, - sortState: actionViewContext.sortState, - setSortState: actionViewContext.setSortState ?? (() => {}), + order: actionViewContext.order, + setOrder: actionViewContext.setOrder ?? (() => {}), + currentPage: actionViewContext.currentPage ?? 1, + setCurrentPage: actionViewContext.setCurrentPage ?? (() => {}), + treeType: actionViewContext.treeType ?? DEFAULT_TREE_TYPE, + setTreeType: actionViewContext.setTreeType ?? (() => {}), + limit: actionViewContext.limit ?? DEFAULT_SEARCH_LIMIT, + setLimit: actionViewContext.setLimit ?? (() => {}), } : { treeIsLoading: localTreeIsLoading, @@ -96,6 +123,8 @@ export function useSearchTreeState({ setSelectedRowItems: setLocalSelectedRowItems, treeFirstVisibleRow: localTreeFirstVisibleRow, setTreeFirstVisibleRow: setLocalTreeFirstVisibleRow, + treeFirstVisibleColumn: localTreeFirstVisibleColumn, + setTreeFirstVisibleColumn: setLocalTreeFirstVisibleColumn, searchParams: localSearchParams, setSearchParams: setLocalSearchParams, searchValues: localSearchValues, @@ -109,7 +138,13 @@ export function useSearchTreeState({ totalItems: localTotalItems, setTotalItems: setLocalTotalItems, isActive: undefined, - sortState: localSortState, - setSortState: setLocalSortState, + order: localOrder, + setOrder: setLocalOrder, + currentPage: localCurrentPage, + setCurrentPage: setLocalCurrentPage, + treeType: localTreeType, + setTreeType: setLocalTreeType, + limit: localLimit, + setLimit: setLocalLimit, }; } diff --git a/src/hooks/useTableConfiguration.ts b/src/hooks/useTableConfiguration.ts new file mode 100644 index 000000000..6a65b4ff7 --- /dev/null +++ b/src/hooks/useTableConfiguration.ts @@ -0,0 +1,35 @@ +import { useDeepCompareMemo } from "use-deep-compare"; +import { Tree as TreeOoui } from "@gisce/ooui"; +import { getTableColumns } from "@/helpers/treeHelper"; +import { COLUMN_COMPONENTS } from "../widgets/views/Tree/treeComponents"; +import { useMemo } from "react"; +import { useLocale } from "@gisce/react-formiga-components"; + +export const useTableConfiguration = ( + treeOoui: TreeOoui | undefined, + parentContext: Record, +) => { + const { t } = useLocale(); + + const columns = useDeepCompareMemo(() => { + if (!treeOoui) return undefined; + return getTableColumns( + treeOoui, + { ...COLUMN_COMPONENTS }, + parentContext, + "paginated", + ); + }, [treeOoui, parentContext]); + + const strings = useMemo( + () => ({ + resetTableViewLabel: t("resetTableView"), + }), + [t], + ); + + return { + columns, + strings, + }; +}; diff --git a/src/hooks/useUrlFromCurrentTab.ts b/src/hooks/useUrlFromCurrentTab.ts index 099e0d8ca..ef135f164 100644 --- a/src/hooks/useUrlFromCurrentTab.ts +++ b/src/hooks/useUrlFromCurrentTab.ts @@ -13,7 +13,8 @@ export function useUrlFromCurrentTab({ }: { currentTab?: Tab; }): UseUrlFromCurrentTabResult { - const { currentView, searchParams, currentId } = useActionViewContext(); + const { currentView, searchParams, currentId, limit, currentPage, order } = + useActionViewContext(); const { currentTab: currentTabContext } = useTabs(); const currentTab = currentTabProps || currentTabContext; @@ -33,6 +34,9 @@ export function useUrlFromCurrentTab({ ...(initialView && { initialView }), ...(searchParams && { searchParams }), ...(currentId && { res_id: currentId }), + ...(limit && { limit }), + ...(currentPage && currentPage > 1 && { currentPage }), + ...(order && { order }), }; const shareUrl = createShareOpenUrl(finalActionData); diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 86572832a..7f4f1d41f 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -91,9 +91,9 @@ export default { errorWhileSavingForm: "Error while saving form", author: "Author", recordsSelected: - "Hi ha {numberOfSelectedRows} registres seleccionats en aquesta pĂ gina.", - selectAllRecords: "Seleccionar tots els {totalRecords} registres.", - allRecordsSelected: "Hi ha {totalRecords} registres seleccionats.", + "There are {numberOfSelectedRows} records selected on this page.", + selectAllRecords: "Select all {totalRecords} records.", + allRecordsSelected: "There are {totalRecords} records selected.", openInSameWindow: "Open in the current tab", openInNewTab: "Open in a new tab", confirmDuplicate: "Are you sure you want to duplicate the selected item/s?", diff --git a/src/types/index.ts b/src/types/index.ts index 324d888b4..707844a0d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -413,6 +413,8 @@ type ActionInfo = { limit?: number; actionRawData?: ActionRawData; searchParams?: any[]; + currentPage?: number; + order?: any[]; }; type Tab = { diff --git a/src/ui/TitleHeader.tsx b/src/ui/TitleHeader.tsx index f64133c5e..f2984b920 100644 --- a/src/ui/TitleHeader.tsx +++ b/src/ui/TitleHeader.tsx @@ -29,7 +29,7 @@ const TitleHeader: React.FC = ({ results, totalItems, selectedRowItems, - isInfiniteTree, + treeType, } = useContext(ActionViewContext) as ActionViewContextType; const { t } = useLocale(); const { token } = useToken(); @@ -53,11 +53,12 @@ const TitleHeader: React.FC = ({ } const currentItemNumber = (currentItemIndex ?? 0) + 1; - const itemCount = isInfiniteTree ? totalItems : results?.length; + const itemCount = treeType === "infinite" ? totalItems : results?.length; return ( <> - {t("register")} {currentItemNumber} {isInfiniteTree ? t("of") : "/"}{" "} - {itemCount} {!isInfiniteTree && `${t("of")} ${totalItems}`} -{" "} + {t("register")} {currentItemNumber}{" "} + {treeType === "infinite" ? t("of") : "/"} {itemCount}{" "} + {treeType !== "infinite" && `${t("of")} ${totalItems}`} -{" "} {t("editingDocument")} (id: {currentId}) ); @@ -92,7 +93,7 @@ const TitleHeader: React.FC = ({ selectedRowItems, totalItems, currentItemIndex, - isInfiniteTree, + treeType, results?.length, t, ]); diff --git a/src/views/ActionView.tsx b/src/views/ActionView.tsx index bdccc7bf3..5d9a8fa6c 100644 --- a/src/views/ActionView.tsx +++ b/src/views/ActionView.tsx @@ -55,6 +55,8 @@ type Props = { treeExpandable?: boolean; limit?: number; initialSearchParams?: any[]; + currentPage?: number; + order?: any[]; }; function ActionView(props: Props, ref: any) { @@ -75,6 +77,8 @@ function ActionView(props: Props, ref: any) { treeExpandable = false, limit, initialSearchParams = [], + currentPage, + order, } = props; const [currentView, setCurrentViewInternal] = useState(); @@ -467,6 +471,8 @@ function ActionView(props: Props, ref: any) { limit={limit} isActive={tabKey === activeKey} initialSearchParams={initialSearchParams} + initialCurrentPage={currentPage} + initialOrder={order} > ("welcome"); @@ -77,7 +79,7 @@ function RootView(props: RootViewProps, ref: any) { } async function handleOpenActionUrl(action: ActionInfo) { - const { actionRawData, res_id, initialView } = action; + const { actionRawData, res_id, limit } = action; const fields = await ConnectionProvider.getHandler().getFields({ model: action.model, @@ -148,6 +150,7 @@ function RootView(props: RootViewProps, ref: any) { openAction({ ...action, + limit: limit && limit > MAX_SEARCH_LIMIT ? MAX_SEARCH_LIMIT : limit, context: { ...rootContext, ...parsedContext }, domain: parsedDomain, actionRawData: { @@ -562,6 +565,8 @@ function RootView(props: RootViewProps, ref: any) { treeExpandable = false, limit, searchParams, + currentPage, + order, } = parms; const key = nanoid(); @@ -612,6 +617,8 @@ function RootView(props: RootViewProps, ref: any) { treeExpandable={treeExpandable} limit={limit} initialSearchParams={searchParams} + currentPage={currentPage} + order={order} /> ), key, diff --git a/src/views/actionViews/TreeActionView.tsx b/src/views/actionViews/TreeActionView.tsx index a9bde73ee..d63756f4f 100644 --- a/src/views/actionViews/TreeActionView.tsx +++ b/src/views/actionViews/TreeActionView.tsx @@ -16,6 +16,7 @@ import { import { SearchTreeInfinite } from "@/widgets/views/SearchTreeInfinite"; import SearchTree from "@/widgets/views/SearchTree"; import { extractTreeXmlAttribute } from "@/helpers/treeHelper"; +import { SearchTreePaginated } from "@/widgets/views/Tree/Paginated/SearchTreePaginated"; export type TreeActionViewProps = { formView: FormView; @@ -34,6 +35,9 @@ export type TreeActionViewProps = { limit?: number; }; +export type TreeType = "infinite" | "paginated" | "legacy"; +export const DEFAULT_TREE_TYPE: TreeType = "legacy"; + export const TreeActionView = (props: TreeActionViewProps) => { const { visible, @@ -52,25 +56,24 @@ export const TreeActionView = (props: TreeActionViewProps) => { } = props; const previousVisibleRef = useRef(visible); - const isInfiniteTree = useMemo(() => { + const treeType: TreeType = useMemo(() => { if (!treeView?.arch || treeView.isExpandable) { - return false; + return "legacy"; } const tagValue = extractTreeXmlAttribute(treeView.arch, "infinite"); - return tagValue === "1"; + if (!tagValue) { + return "legacy"; + } + return tagValue === "1" ? "infinite" : "paginated"; }, [treeView]); + const { currentView, setPreviousView, setTreeType, setSelectedRowItems } = + useContext(ActionViewContext) as ActionViewContextType; + useEffect(() => { - setIsInfiniteTree?.(isInfiniteTree); + setTreeType?.(treeType); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInfiniteTree]); - - const { - currentView, - setPreviousView, - setIsInfiniteTree, - setSelectedRowItems, - } = useContext(ActionViewContext) as ActionViewContextType; + }, [treeType]); const onRowClicked = useCallback( (event: any) => { @@ -98,12 +101,12 @@ export const TreeActionView = (props: TreeActionViewProps) => { ); useEffect(() => { - if (previousVisibleRef.current && !visible && isInfiniteTree) { + if (previousVisibleRef.current && !visible && treeType === "infinite") { setSelectedRowItems?.([]); } previousVisibleRef.current = visible; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible, isInfiniteTree]); + }, [visible, treeType]); if (!visible) { return null; @@ -111,14 +114,14 @@ export const TreeActionView = (props: TreeActionViewProps) => { return ( - + - {isInfiniteTree && ( + {treeType === "infinite" && ( { onRowClicked={onRowClicked} /> )} - {!isInfiniteTree && ( + {treeType === "paginated" && ( + + )} + {treeType === "legacy" && ( { })}%`; return ( -
+ -
{textValue}
-
+ {textValue} + ); }; +const StyledProgressContainer = styled.div` + display: flex; + align-items: center; + width: 100%; + min-width: 0; +`; + const StyledProgress = styled(Progress)` + flex: 1; + min-width: 0; .ant-progress-outer { margin-right: 0px; padding-right: 0px; @@ -36,3 +45,8 @@ const StyledProgress = styled(Progress)` display: none; } `; + +const StyledText = styled.div` + padding-left: 10px; + white-space: nowrap; +`; diff --git a/src/widgets/base/one2many/One2manyTree.tsx b/src/widgets/base/one2many/One2manyTree.tsx index 6eda2a9f8..fa1436ed1 100644 --- a/src/widgets/base/one2many/One2manyTree.tsx +++ b/src/widgets/base/one2many/One2manyTree.tsx @@ -110,6 +110,7 @@ export const One2manyTree = ({ ...COLUMN_COMPONENTS, }, context, + "infinite", ); }, [context, ooui]); @@ -153,12 +154,12 @@ export const One2manyTree = ({ }, []); const { loading, getColumnState, updateColumnState } = - useTreeColumnStorageFetch( - getKey({ + useTreeColumnStorageFetch({ + key: getKey({ ...dataForHash, model: relation, }), - ); + }); if (loading) { return ; diff --git a/src/widgets/base/one2many/useTreeColumnStorageFetch.ts b/src/widgets/base/one2many/useTreeColumnStorageFetch.ts index 6bbd173ef..f0435091e 100644 --- a/src/widgets/base/one2many/useTreeColumnStorageFetch.ts +++ b/src/widgets/base/one2many/useTreeColumnStorageFetch.ts @@ -2,42 +2,68 @@ import { ColumnState } from "@gisce/react-formiga-table"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTreeColumnStorage } from "./useTreeColumnStorage"; -export const useTreeColumnStorageFetch = (key?: string) => { +type TreeColumnStorageFetchProps = { + key?: string; + treeViewFetching?: boolean; +}; + +export const useTreeColumnStorageFetch = ({ + key, + treeViewFetching = false, +}: TreeColumnStorageFetchProps) => { const [loading, setLoading] = useState(true); const columnState = useRef(undefined); const fetchInProgress = useRef(false); - const { getColumnState: getColumnStateInternal, updateColumnState } = - useTreeColumnStorage(key); + const { + getColumnState: getColumnStateInternal, + updateColumnState: updateColumnStateInternal, + } = useTreeColumnStorage(key); + + const fetchColumnState = useCallback(async () => { + if (fetchInProgress.current || treeViewFetching) { + return; + } + + fetchInProgress.current = true; + setLoading(true); + try { + columnState.current = await getColumnStateInternal(); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + fetchInProgress.current = false; + } + return columnState.current; + }, [getColumnStateInternal, treeViewFetching]); useEffect(() => { if (!key) { setLoading(false); return; } - const fetchColumnState = async () => { - if (fetchInProgress.current) { - return; - } - - fetchInProgress.current = true; - setLoading(true); - try { - columnState.current = await getColumnStateInternal(); - } catch (err) { - console.error(err); - } finally { - setLoading(false); - fetchInProgress.current = false; - } - }; fetchColumnState(); - }, [getColumnStateInternal, key]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, treeViewFetching]); const getColumnState = useCallback(() => { return columnState.current; }, []); - return { getColumnState, loading, updateColumnState }; + const updateColumnState = useCallback( + (state: ColumnState[]) => { + const columnStatesWithoutSort = state.map((columnState) => { + const { sort, ...columnStateWithoutSort } = columnState; + return columnStateWithoutSort; + }); + columnState.current = columnStatesWithoutSort; + + updateColumnStateInternal(state); + }, + [updateColumnStateInternal], + ); + + return { getColumnState, loading, updateColumnState, fetchColumnState }; }; diff --git a/src/widgets/views/SearchTreeInfinite.tsx b/src/widgets/views/SearchTreeInfinite.tsx index e91ed2d69..05ec0720b 100644 --- a/src/widgets/views/SearchTreeInfinite.tsx +++ b/src/widgets/views/SearchTreeInfinite.tsx @@ -136,8 +136,8 @@ function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { setSearchQuery, setTotalItems: setTotalItemsActionView, isActive, - sortState: actionViewSortState, - setSortState: setActionViewSortState, + order: actionViewSortState, + setOrder: setActionViewSortState, } = useSearchTreeState({ useLocalState: !rootTree }); const nameSearch = nameSearchProps || searchTreeNameSearch; @@ -192,6 +192,7 @@ function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { ...COLUMN_COMPONENTS, }, parentContext, + "infinite", ); }, [treeOoui, parentContext]); @@ -209,7 +210,7 @@ function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { loading: getColumnStateInProgress, getColumnState, updateColumnState, - } = useTreeColumnStorageFetch(columnStateKey); + } = useTreeColumnStorageFetch({ key: columnStateKey }); const mergedParams = useMemo( () => mergeParams(searchParams || [], domain), diff --git a/src/widgets/views/Tree/Paginated/SearchTreePaginated.tsx b/src/widgets/views/Tree/Paginated/SearchTreePaginated.tsx new file mode 100644 index 000000000..d721017f1 --- /dev/null +++ b/src/widgets/views/Tree/Paginated/SearchTreePaginated.tsx @@ -0,0 +1,250 @@ +import { + Fragment, + RefObject, + forwardRef, + useCallback, + useImperativeHandle, + useMemo, + useRef, + useEffect, +} from "react"; + +import { Tree as TreeOoui } from "@gisce/ooui"; +import { PaginatedTableRef } from "@gisce/react-formiga-table"; + +import { Badge, Spin } from "antd"; +import { PaginationHeader } from "@gisce/react-formiga-components"; +import { AggregatesFooter } from "../../../base/one2many/AggregatesFooter"; + +import { useFetchTreeViews } from "@/hooks/useFetchTreeViews"; +import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useTreeAggregates } from "../../../base/one2many/useTreeAggregates"; +import { useAutorefreshableTreeFields } from "@/hooks/useAutorefreshableTreeFields"; +import { + DEFAULT_PAGE_SIZE, + usePaginatedSearch, +} from "@/widgets/views/Tree/Paginated/hooks/usePaginatedSearch"; + +import { getTree } from "@/helpers/treeHelper"; +import { + SearchTreePaginatedProps, + OnRowClickedData, +} from "./SearchTreePaginated.types"; +import { useTableConfiguration } from "../../../../hooks/useTableConfiguration"; +import { PaginatedSearchControls } from "./components/PaginatedSearchControls"; +import { PaginatedTableComponent } from "./components/PaginatedTableComponent"; + +export const HEIGHT_OFFSET = 10; + +function SearchTreePaginatedComp(props: SearchTreePaginatedProps, ref: any) { + const { + model, + formView: formViewProps, + treeView: treeViewProps, + onRowClicked, + domain = [], + visible = true, + rootTree = false, + parentContext = {}, + nameSearch: nameSearchProps, + filterType = "side", + } = props; + + // Refs + const tableRef: RefObject = useRef(null); + const containerRef = useRef(null); + const onRowClickedRef = useRef(onRowClicked); + + // Update ref when onRowClicked changes + useEffect(() => { + onRowClickedRef.current = onRowClicked; + }, [onRowClicked]); + + // Callback that uses the ref + const handleRowDoubleClick = useCallback((data: OnRowClickedData) => { + onRowClickedRef.current?.(data); + }, []); + + const availableHeight = useAvailableHeight({ + elementRef: containerRef, + offset: HEIGHT_OFFSET, + }); + + // Views data fetching + const { treeView, formView, loading } = useFetchTreeViews({ + model, + formViewProps, + treeViewProps, + context: parentContext, + }); + + const treeOoui: TreeOoui | undefined = useMemo(() => { + if (!treeView) return; + return getTree(treeView); + }, [treeView]); + + const { columns, strings } = useTableConfiguration(treeOoui, parentContext); + + // Ensure columns is never undefined + const safeColumns = useMemo(() => columns || [], [columns]); + + // Pagination and search state + const { + isActive, + searchVisible, + searchValues, + selectedRowKeys, + refresh, + onRowStatus, + onGetFirstVisibleRowIndex, + setTreeFirstVisibleRow, + onRowHasBeenSelected, + onSearchFilterClear, + onSearchFilterSubmit, + onSideSearchFilterClose, + onSideSearchFilterSubmit, + totalRowsLoading, + totalRows, + onRowStyle, + results, + onRequestPageChange, + treeIsLoading, + selectAllRecords, + onHeaderCheckboxClick, + headerCheckboxState, + getColumnStateInProgress, + getColumnState, + updateColumnState, + currentPage, + limit, + order: actionViewSortState, + setOrder: setActionViewSortState, + setTreeFirstVisibleColumn, + onGetFirstVisibleColumn, + } = usePaginatedSearch({ + treeViewFetching: loading, + treeOoui, + treeView, + model, + rootTree, + nameSearchProps, + tableRef, + domain, + filterType, + context: parentContext, + }); + + // Aggregates handling + 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, + }); + + // Auto-refresh setup + useAutorefreshableTreeFields({ + model, + tableRef, + autorefreshableFields: treeOoui?.autorefreshableFields, + fieldDefs: treeView?.field_parent + ? { ...treeView?.fields, [treeView?.field_parent]: {} } + : treeView?.fields, + context: parentContext, + isActive, + }); + + // External control + useImperativeHandle(ref, () => ({ + refreshResults: refresh, + getFields: () => treeView?.fields, + getDomain: () => domain, + })); + + // UI Components and Styles + const footerComp = useMemo(() => { + if (!hasAggregates) return null; + return ( + + ); + }, [aggregates, loadingAggregates, hasAggregates]); + + const statusComp = useCallback( + (status: any) => , + [], + ); + + const containerStyle = useMemo( + () => ({ + overflow: "hidden", + height: `${availableHeight}px`, + ...(visible ? {} : { display: "none" }), + }), + [availableHeight, visible], + ); + + // Render + return ( + + + +
+ {loading ? ( + + ) : ( + + )} +
+
+ ); +} + +export const SearchTreePaginated = forwardRef(SearchTreePaginatedComp); diff --git a/src/widgets/views/Tree/Paginated/SearchTreePaginated.types.ts b/src/widgets/views/Tree/Paginated/SearchTreePaginated.types.ts new file mode 100644 index 000000000..a9760bdbd --- /dev/null +++ b/src/widgets/views/Tree/Paginated/SearchTreePaginated.types.ts @@ -0,0 +1,66 @@ +import { FormView, TreeView } from "@/types/index"; +import { Tree as TreeOoui } from "@gisce/ooui"; +import { PaginatedTableRef } from "@gisce/react-formiga-table"; +import { CheckboxState } from "@gisce/react-formiga-table/dist/components/PaginatedTable/PaginatedHeaderCheckbox"; +import { RefObject } from "react"; + +export type OnRowClickedData = { + id: number; + model: string; + formView: FormView; + treeView: TreeView; +}; + +export type SearchTreePaginatedProps = { + model: string; + formView: FormView; + treeView: TreeView; + onRowClicked: (data: OnRowClickedData) => void; + nameSearch?: string; + domain?: any[]; + visible?: boolean; + rootTree?: boolean; + parentContext?: Record; + filterType?: "side" | "top"; +}; + +export type PaginatedSearchControlsProps = { + filterType: "side" | "top"; + formView?: FormView; + treeView?: TreeView; + searchVisible: boolean; + searchValues: any; + onSearchFilterClear: () => void; + onSearchFilterSubmit: (values: any) => void; + onSideSearchFilterClose: () => void; + onSideSearchFilterSubmit: (values: any) => void; +}; + +export type PaginatedTableContentProps = { + columns: any[]; + treeOoui: TreeOoui; + strings: Record; + isLoading: boolean; + availableHeight: number; + results: any[]; + handleRowDoubleClick: (data: OnRowClickedData) => void; + onRowHasBeenSelected: + | ((changedRow: { id: number; selected: boolean }) => void) + | undefined; + updateColumnState: (state: any) => void; + getColumnState: () => any; + setTreeFirstVisibleRow: (index: number) => void; + onGetFirstVisibleRowIndex: () => number; + onGetFirstVisibleColumn?: (() => string | undefined) | undefined; + setTreeFirstVisibleColumn: ((columnId: string) => void) | undefined; + footerComp: React.ReactNode; + statusComp: (status: any) => React.ReactNode; + onRowStatus: (record: any) => any; + onRowStyle: (record: any) => any; + headerCheckboxState: CheckboxState; + onHeaderCheckboxClick: () => void; + refresh: () => void; + actionViewSortState: any; + setActionViewSortState: (state: any) => void; + tableRef: RefObject; +}; diff --git a/src/widgets/views/Tree/Paginated/components/PaginatedSearchControls.tsx b/src/widgets/views/Tree/Paginated/components/PaginatedSearchControls.tsx new file mode 100644 index 000000000..ce9dcec53 --- /dev/null +++ b/src/widgets/views/Tree/Paginated/components/PaginatedSearchControls.tsx @@ -0,0 +1,78 @@ +import { FC, useMemo } from "react"; +import SearchFilter from "../../../searchFilter/SearchFilter"; +import { SideSearchFilter } from "../../../searchFilter/SideSearchFilter"; +import { mergeSearchFields } from "@/helpers/formHelper"; +import { PaginatedSearchControlsProps } from "../SearchTreePaginated.types"; + +export const PaginatedSearchControls: FC = ({ + filterType, + formView, + treeView, + searchVisible, + searchValues, + onSearchFilterClear, + onSearchFilterSubmit, + onSideSearchFilterClose, + onSideSearchFilterSubmit, +}) => { + const searchFilterProps = useMemo( + () => ({ + fields: { ...formView?.fields, ...treeView?.fields }, + searchFields: mergeSearchFields([ + formView?.search_fields, + treeView?.search_fields, + ]), + showLimitOptions: false, + limit: 0, + offset: 0, + isSearching: false, + searchValues, + searchVisible: true, + }), + [ + formView?.fields, + formView?.search_fields, + treeView?.fields, + treeView?.search_fields, + searchValues, + ], + ); + + const sideSearchFilterProps = useMemo( + () => ({ + isOpen: searchVisible, + fields: { ...formView?.fields, ...treeView?.fields }, + searchFields: mergeSearchFields([ + formView?.search_fields, + treeView?.search_fields, + ]), + searchValues, + }), + [ + formView?.fields, + formView?.search_fields, + treeView?.fields, + treeView?.search_fields, + searchValues, + searchVisible, + ], + ); + + if (filterType === "top") { + return ( + + ); + } + + return ( + + ); +}; diff --git a/src/widgets/views/Tree/Paginated/components/PaginatedTableComponent.tsx b/src/widgets/views/Tree/Paginated/components/PaginatedTableComponent.tsx new file mode 100644 index 000000000..9e1d11fad --- /dev/null +++ b/src/widgets/views/Tree/Paginated/components/PaginatedTableComponent.tsx @@ -0,0 +1,65 @@ +import { memo } from "react"; +import { PaginatedTable } from "@gisce/react-formiga-table"; +import { PaginatedTableContentProps } from "../SearchTreePaginated.types"; + +export const PaginatedTableComponent = memo( + ({ + columns, + treeOoui, + strings, + isLoading, + availableHeight, + results, + handleRowDoubleClick, + onRowHasBeenSelected, + updateColumnState, + getColumnState, + setTreeFirstVisibleRow, + onGetFirstVisibleRowIndex, + onGetFirstVisibleColumn, + setTreeFirstVisibleColumn, + footerComp, + statusComp, + onRowStatus, + onRowStyle, + headerCheckboxState, + onHeaderCheckboxClick, + refresh, + actionViewSortState, + setActionViewSortState, + tableRef, + }: PaginatedTableContentProps) => { + if (!columns || !treeOoui) return null; + + return ( + + ); + }, +); + +PaginatedTableComponent.displayName = "PaginatedTableComponent"; diff --git a/src/widgets/views/Tree/Paginated/hooks/usePaginatedSearch.ts b/src/widgets/views/Tree/Paginated/hooks/usePaginatedSearch.ts new file mode 100644 index 000000000..1e9c4aff5 --- /dev/null +++ b/src/widgets/views/Tree/Paginated/hooks/usePaginatedSearch.ts @@ -0,0 +1,536 @@ +import { mergeParams } from "@/helpers/searchHelper"; +import { useSearchTreeState } from "@/hooks/useSearchTreeState"; +import { PaginatedTableRef, CheckboxState } from "@gisce/react-formiga-table"; +import { + CSSProperties, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useNetworkRequest } from "../../../../../hooks/useNetworkRequest"; +import { ConnectionProvider, TreeView } from "../../../../.."; +import { useShowErrorDialog } from "@/ui/GenericErrorDialog"; +import { useDeepCompareEffect } from "use-deep-compare"; +import deepEqual from "deep-equal"; +import { + getColorMap, + getStatusMap, + getTableItems, + getSortedFieldsFromState, + getOrderFromSortFields, +} from "@/helpers/treeHelper"; +import { Tree as TreeOoui } from "@gisce/ooui"; +import { getKey } from "@/helpers/tree-columnStorageHelper"; +import { useTreeColumnStorageFetch } from "@/widgets/base/one2many/useTreeColumnStorageFetch"; + +export const DEFAULT_PAGE_SIZE = 80; + +export type PaginatedSearchProps = { + treeViewFetching: boolean; + treeOoui?: TreeOoui; + treeView?: TreeView; + model: string; + rootTree?: boolean; + nameSearchProps?: string; + tableRef: React.RefObject; + domain?: any; + context?: any; + filterType?: "side" | "top"; +}; + +export const usePaginatedSearch = (props: PaginatedSearchProps) => { + const { + treeViewFetching, + treeOoui, + treeView, + model, + rootTree = false, + nameSearchProps, + tableRef, + domain = [], + context, + filterType = "side", + } = props; + + // State from useSearchTreeState + const { + treeIsLoading, + setTreeIsLoading, + searchVisible, + setSearchVisible, + setSelectedRowItems, + setTreeFirstVisibleRow, + treeFirstVisibleRow, + treeFirstVisibleColumn, + setTreeFirstVisibleColumn, + selectedRowItems, + setSearchParams, + searchValues, + searchParams, + setSearchValues, + searchTreeNameSearch, + setSearchTreeNameSearch, + setResults: setActionViewResults, + setSearchQuery, + setTotalItems: setTotalItemsActionView, + isActive, + currentPage, + setCurrentPage, + order: actionViewOrder, + setOrder: setActionViewOrder, + limit, + setLimit, + } = useSearchTreeState({ useLocalState: !rootTree }); + + // Local state + const [totalRowsLoading, setTotalRowsLoading] = useState(true); + const [totalRows, setTotalRows] = useState(); + const [results, setResults] = useState([]); + + // Refs + const nameSearch = nameSearchProps || searchTreeNameSearch; + const prevNameSearch = useRef(nameSearch); + const prevSearchParamsRef = useRef(searchParams); + const prevSearchVisibleRef = useRef(searchVisible); + const currentSearchParamsString = useRef(); + const colorsForResults = useRef<{ [key: number]: string }>({}); + const statusForResults = useRef<{ [key: number]: string }>(); + const lastAssignedResults = useRef([]); + + const columnStateKey = useMemo(() => { + return getKey({ treeViewId: treeView?.view_id, model }); + }, [treeView?.view_id, model]); + + const { + fetchColumnState, + loading: getColumnStateInProgress, + getColumnState, + updateColumnState, + } = useTreeColumnStorageFetch({ + key: columnStateKey, + treeViewFetching, + }); + + // Hooks + const showErrorDialog = useShowErrorDialog(); + const [fetchTotalRows, cancelFetchTotalRows] = useNetworkRequest( + ConnectionProvider.getHandler().searchCount, + ); + const [searchForTree, cancelSearchForTree] = useNetworkRequest( + ConnectionProvider.getHandler().searchForTree, + ); + + const [fetchAllIds, cancelFetchAllIds] = useNetworkRequest( + ConnectionProvider.getHandler().searchAllIds, + ); + + // Memoized values + const mergedParams = useMemo( + () => mergeParams(searchParams || [], domain), + [domain, searchParams], + ); + + const selectedRowKeys = useMemo(() => { + return selectedRowItems?.map((item) => item.id) || []; + }, [selectedRowItems]); + + // Helper functions + const mustUpdateTotal = useCallback(() => { + const params = nameSearch ? domain : mergedParams; + const paramsString = `${JSON.stringify(params)}-${nameSearch}`; + + if (paramsString !== currentSearchParamsString.current) { + currentSearchParamsString.current = paramsString; + return true; + } + return false; + }, [domain, mergedParams, nameSearch]); + + // Core functionality + const updateTotalRows = useCallback(async () => { + setTotalRows(undefined); + setTotalItemsActionView(0); + setTotalRowsLoading(true); + try { + const totalItems = await fetchTotalRows({ + params: nameSearch ? domain : mergedParams, + model, + context, + name_search: nameSearch, + }); + setTotalRows(totalItems); + setTotalItemsActionView(totalItems); + } catch (err) { + showErrorDialog(err); + } finally { + setTotalRowsLoading(false); + } + }, [ + setTotalItemsActionView, + fetchTotalRows, + nameSearch, + domain, + mergedParams, + model, + context, + showErrorDialog, + ]); + + // Event handlers + const onGetFirstVisibleRowIndex = useCallback(() => { + return treeFirstVisibleRow; + }, [treeFirstVisibleRow]); + + const onGetFirstVisibleColumn = useCallback(() => { + return treeFirstVisibleColumn; + }, [treeFirstVisibleColumn]); + + const onRowStyle = useCallback((item: Record): CSSProperties => { + if (colorsForResults.current[item.node?.data?.id]) { + return { color: colorsForResults.current[item.node?.data?.id] }; + } + return {}; + }, []); + + const onRowStatus = useCallback( + (record: any) => statusForResults.current?.[record.id], + [], + ); + + // Search filter handlers + const onSearchFilterClear = useCallback(() => { + setSelectedRowItems([]); + tableRef.current?.unselectAll(); + setSearchTreeNameSearch?.(undefined); + setSearchParams?.([]); + setSearchValues?.(undefined); + }, [ + setSelectedRowItems, + tableRef, + setSearchTreeNameSearch, + setSearchParams, + setSearchValues, + ]); + + const onSearchFilterSubmit = useCallback( + ({ params, searchValues }: any) => { + setSelectedRowItems([]); + tableRef.current?.unselectAll(); + setSearchTreeNameSearch?.(undefined); + setSearchParams?.(params); + setSearchValues?.(searchValues); + }, + [ + setSelectedRowItems, + tableRef, + setSearchTreeNameSearch, + setSearchParams, + setSearchValues, + ], + ); + + const onSideSearchFilterClose = useCallback( + () => setSearchVisible?.(false), + [setSearchVisible], + ); + + const onSideSearchFilterSubmit = useCallback( + ({ params, values }: any) => { + setSelectedRowItems([]); + tableRef.current?.unselectAll(); + setSearchTreeNameSearch?.(undefined); + setSearchParams?.(params); + setSearchValues?.(values); + setSearchVisible?.(false); + }, + [ + setSelectedRowItems, + tableRef, + setSearchTreeNameSearch, + setSearchParams, + setSearchValues, + setSearchVisible, + ], + ); + + // Effects + useEffect(() => { + if (treeViewFetching) { + return; + } + return () => { + cancelFetchTotalRows(); + cancelSearchForTree(); + cancelFetchAllIds(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [treeViewFetching]); + + useDeepCompareEffect(() => { + if (!treeOoui || !treeView || treeViewFetching) { + return; + } + fetchResults(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + treeView, + treeOoui, + limit, + currentPage, + mergedParams, + nameSearch, + domain, + actionViewOrder, + ]); + + useEffect(() => { + if ( + (nameSearch !== undefined && prevNameSearch.current === undefined) || + (typeof nameSearch === "string" && + typeof prevNameSearch.current === "string" && + nameSearch !== prevNameSearch.current) + ) { + setSearchParams?.([]); + setSearchValues?.({}); + tableRef.current?.unselectAll(); + refresh(); + } + prevNameSearch.current = nameSearch; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nameSearch]); + + useDeepCompareEffect(() => { + const searchParamsChanged = !deepEqual( + searchParams, + prevSearchParamsRef.current, + ); + const searchVisibleChangedToFalse = + prevSearchVisibleRef.current && !searchVisible; + + if ( + searchParamsChanged && + (searchVisibleChangedToFalse || filterType === "top") + ) { + refresh(); + } + + prevSearchParamsRef.current = searchParams; + prevSearchVisibleRef.current = searchVisible; + }, [searchParams, searchVisible]); + + const fetchResults = useCallback(async () => { + if (!treeOoui || treeViewFetching) { + return []; + } + setTreeIsLoading(true); + + const attrs: any = {}; + if (treeOoui.colors) { + attrs.colors = treeOoui.colors; + } + if (treeOoui.status) { + attrs.status = treeOoui.status; + } + + let order; + if (actionViewOrder?.length) { + const sortFields = getSortedFieldsFromState({ + state: actionViewOrder, + }); + order = getOrderFromSortFields(sortFields); + } + + const params = nameSearch ? domain : mergedParams; + + const { results, attrsEvaluated } = await searchForTree({ + params, + limit, + offset: ((currentPage || 1) - 1) * limit, + model, + fields: treeView!.field_parent + ? { ...treeView!.fields, [treeView!.field_parent]: {} } + : treeView!.fields, + context, + attrs, + order, + name_search: nameSearch, + }); + + const newResults = results.map((item: any) => ({ id: item.id })); + + setSearchQuery?.({ + model, + params, + name_search: nameSearch, + context, + }); + + setActionViewResults?.(newResults); + + if (mustUpdateTotal()) { + updateTotalRows(); + } + + if (results.length === 0) { + lastAssignedResults.current = []; + setTotalRows(0); + setTotalItemsActionView(0); + setResults([]); + setTreeIsLoading(false); + return; + } + + 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, + }; + } + + setTreeIsLoading(false); + lastAssignedResults.current = [...preparedResults]; + setResults([...preparedResults]); + }, [ + treeOoui, + treeViewFetching, + setTreeIsLoading, + actionViewOrder, + nameSearch, + domain, + mergedParams, + searchForTree, + limit, + currentPage, + model, + treeView, + context, + setSearchQuery, + setActionViewResults, + mustUpdateTotal, + updateTotalRows, + setTotalItemsActionView, + ]); + + const refresh = useCallback(async () => { + setTreeFirstVisibleRow(0); + fetchColumnState(); + setSelectedRowItems([]); + currentSearchParamsString.current = undefined; + fetchResults(); + }, [ + fetchColumnState, + fetchResults, + setSelectedRowItems, + setTreeFirstVisibleRow, + ]); + + const onRequestPageChange = useCallback( + (page: number, pageSize?: number) => { + setTreeFirstVisibleRow(0); + setSelectedRowItems([]); + setCurrentPage(page); + pageSize && setLimit(pageSize); + }, + [setCurrentPage, setLimit, setSelectedRowItems, setTreeFirstVisibleRow], + ); + + const getAllIds = useCallback(async () => { + return await fetchAllIds({ + params: mergeParams(searchParams, domain), + model, + context, + totalItems: totalRows, + }); + }, [fetchAllIds, searchParams, domain, model, context, totalRows]); + + const selectAllRecords = useCallback(async () => { + const allIds = await getAllIds(); + setSelectedRowItems?.(allIds.map((id: number) => ({ id }))); + // onChangeSelectedRowKeys?.(allIds); + }, [getAllIds, setSelectedRowItems]); + + const headerCheckboxState: CheckboxState = useMemo(() => { + if (selectedRowKeys.length === 0) return "unchecked"; + if (selectedRowKeys.length === limit && limit > 0) return "checked"; + if (selectedRowKeys.length === totalRows) return "checked"; + return "indeterminate"; + }, [selectedRowKeys, limit, totalRows]); + + const onHeaderCheckboxClick = useCallback(() => { + if (headerCheckboxState === "unchecked") { + // Moving to checked state + tableRef.current?.selectAll(); + setSelectedRowItems(results.map((item) => ({ id: item.id }))); + } else { + // Moving to unchecked state + setSelectedRowItems([]); + tableRef.current?.unselectAll(); + } + }, [tableRef, setSelectedRowItems, results, headerCheckboxState]); + + const onRowHasBeenSelected = useCallback( + ({ id, selected }: { id: number; selected: boolean }) => { + setSelectedRowItems((prevItems) => { + if (selected) { + const item = results.find((result) => result.id === id); + if (item && !prevItems.some((existing) => existing.id === id)) { + return [...prevItems, item]; + } + return prevItems; + } + return prevItems.filter((existing) => existing.id !== id); + }); + }, + [results, setSelectedRowItems], + ); + + return { + isActive, + searchVisible, + searchValues, + selectedRowKeys, + refresh, + onRowStatus, + onGetFirstVisibleRowIndex, + setTreeFirstVisibleRow, + onRowHasBeenSelected, + onSearchFilterClear, + onSearchFilterSubmit, + onSideSearchFilterClose, + onSideSearchFilterSubmit, + totalRowsLoading, + totalRows, + onRowStyle, + results, + onRequestPageChange, + treeIsLoading, + selectAllRecords, + onHeaderCheckboxClick, + headerCheckboxState, + getColumnStateInProgress, + getColumnState, + updateColumnState, + currentPage, + limit, + order: actionViewOrder, + setOrder: setActionViewOrder, + setTreeFirstVisibleColumn, + onGetFirstVisibleColumn, + }; +}; diff --git a/src/widgets/views/Tree/Tree.tsx b/src/widgets/views/Tree/Tree.tsx index 51492051e..67fa64e26 100644 --- a/src/widgets/views/Tree/Tree.tsx +++ b/src/widgets/views/Tree/Tree.tsx @@ -113,6 +113,7 @@ export const UnmemoizedTree = forwardRef( ...COLUMN_COMPONENTS, }, context, + "legacy", ); }, [context, treeOoui]);