From cc8959c835ab71df0bdddbe3a9dcb33d9a101b9d Mon Sep 17 00:00:00 2001 From: Platon Yasev Date: Sun, 5 Mar 2023 14:17:56 +0300 Subject: [PATCH] Refactor table data merging --- src/Map/MapComponent.jsx | 4 +- .../Table/InitialTable/InitialTable.jsx | 57 ++++ .../Table/InitialTable/useInitialTableData.js | 56 ++++ src/modules/Table/Table.jsx | 293 +++++------------- src/modules/Table/TableWrapper.jsx | 13 + src/modules/Table/constants.js | 1 + src/modules/Table/useGetClickedPoint.js | 28 ++ src/modules/Table/useMergeTableData.js | 59 ++++ 8 files changed, 293 insertions(+), 218 deletions(-) create mode 100644 src/modules/Table/InitialTable/InitialTable.jsx create mode 100644 src/modules/Table/InitialTable/useInitialTableData.js create mode 100644 src/modules/Table/TableWrapper.jsx create mode 100644 src/modules/Table/constants.js create mode 100644 src/modules/Table/useGetClickedPoint.js create mode 100644 src/modules/Table/useMergeTableData.js diff --git a/src/Map/MapComponent.jsx b/src/Map/MapComponent.jsx index 3e4bf08..c1da0d4 100644 --- a/src/Map/MapComponent.jsx +++ b/src/Map/MapComponent.jsx @@ -7,11 +7,11 @@ import { MapPopup } from "./Popup"; import { Basemap } from "./Basemap"; import { SignOut } from "../SignOut"; import debounce from "lodash.debounce"; -import { Table } from "../modules/Table/Table"; import { usePopup } from "../stores/usePopup"; import { useClickedPointConfig } from "../stores/useClickedPointConfig"; import { Legend } from "../modules/Sidebar/Legend"; import { AddressSearch } from "../modules/Sidebar/AddressSearch"; +import { TableWrapper } from "../modules/Table/TableWrapper"; export const MapComponent = () => { const mapRef = useRef(null); @@ -117,7 +117,7 @@ export const MapComponent = () => { - + ); }; diff --git a/src/modules/Table/InitialTable/InitialTable.jsx b/src/modules/Table/InitialTable/InitialTable.jsx new file mode 100644 index 0000000..5063aec --- /dev/null +++ b/src/modules/Table/InitialTable/InitialTable.jsx @@ -0,0 +1,57 @@ +import { useCallback, useState } from "react"; +import { Table } from "../Table"; +import { usePointSelection } from "../../../stores/usePointSelection"; +import { useClickedPointConfig } from "../../../stores/useClickedPointConfig"; +import { useInitialTableData } from "./useInitialTableData"; + +export const InitialTable = () => { + const { selection, include, exclude } = usePointSelection(); + const { clickedPointConfig } = useClickedPointConfig(); + const [page, setPage] = useState(1); + + const { data, pageSize, isClickedPointLoading } = useInitialTableData(page); + + const getSelectedRowKeys = useCallback(() => { + const ids = data?.results.map((item) => item.location_id) ?? []; + const clickedPoint = data?.results.find( + (item) => item.location_id === clickedPointConfig?.id + ); + + const inExcludedList = (id) => selection.excluded.has(id); + const shouldNotSelect = (id) => + id === clickedPoint?.id && clickedPointConfig?.shouldSelect === false; + + return [ + ...ids.filter((id) => { + return !inExcludedList(id) && !shouldNotSelect(id); + }), + ...selection.included, + ]; + }, [data, clickedPointConfig, selection]); + + const rowSelection = { + selectedRowKeys: getSelectedRowKeys(), + onSelect: (record, selected) => { + const { location_id: id } = record; + if (selected) { + include(id); + } else { + exclude(id); + } + }, + hideSelectAll: true, + }; + + const handlePageChange = useCallback((page) => setPage(page), []); + + return ( +
+ ); +}; diff --git a/src/modules/Table/InitialTable/useInitialTableData.js b/src/modules/Table/InitialTable/useInitialTableData.js new file mode 100644 index 0000000..c4ae7ae --- /dev/null +++ b/src/modules/Table/InitialTable/useInitialTableData.js @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { PAGE_SIZE } from "../constants"; +import { useFilters } from "../../../stores/useFilters"; +import { usePointSelection } from "../../../stores/usePointSelection"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "../../../api"; +import { useMergeTableData } from "../useMergeTableData"; + +export const useInitialTableData = (page) => { + const [pageSize, setPageSize] = useState(PAGE_SIZE); + const { filters } = useFilters(); + const { prediction, status, categories, region } = filters; + const { selection } = usePointSelection(); + + const { data } = useQuery( + ["table", page, filters, selection], + async () => { + const params = new URLSearchParams({ + page, + page_size: pageSize, + "prediction_current[]": prediction, + "status[]": status, + "categories[]": categories, + "included[]": [...selection.included], + }); + + if (region) { + if (region.type === "ao") { + params.append("ao[]", region.id); + } + + if (region.type === "rayon") { + params.append("rayon[]", region.id); + } + } + + const { data } = await api.get( + `/api/placement_points?${params.toString()}` + ); + + return data; + }, + { keepPreviousData: true } + ); + + const { data: mergedData, isClickedPointLoading } = useMergeTableData( + data, + setPageSize + ); + + return { + data: mergedData, + pageSize, + isClickedPointLoading, + }; +}; diff --git a/src/modules/Table/Table.jsx b/src/modules/Table/Table.jsx index 48d76eb..f63815e 100644 --- a/src/modules/Table/Table.jsx +++ b/src/modules/Table/Table.jsx @@ -1,12 +1,8 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef } from "react"; import { Collapse, Empty, Table as AntdTable } from "antd"; import "./Table.css"; -import { useQuery } from "@tanstack/react-query"; -import { api } from "../../api"; import parse from "wellknown"; import { useMap } from "react-map-gl"; -import { usePointSelection } from "../../stores/usePointSelection"; -import { useFilters } from "../../stores/useFilters"; import { useClickedPointConfig } from "../../stores/useClickedPointConfig"; import scrollIntoView from "scroll-into-view-if-needed"; @@ -68,216 +64,81 @@ const columns = [ }, ]; -const PAGE_SIZE = 30; - -const useTableData = (page) => { - const [pageSize, setPageSize] = useState(PAGE_SIZE); - const { filters } = useFilters(); - const { prediction, status, categories, region } = filters; - const { selection } = usePointSelection(); - const { clickedPointConfig } = useClickedPointConfig(); - const [finalData, setFinalData] = useState(); - - const { data } = useQuery( - ["table", page, filters, selection], - async () => { - const params = new URLSearchParams({ - page, - page_size: pageSize, - "prediction_current[]": prediction, - "status[]": status, - "categories[]": categories, - "included[]": [...selection.included], - }); - - if (region) { - if (region.type === "ao") { - params.append("ao[]", region.id); - } - - if (region.type === "rayon") { - params.append("rayon[]", region.id); - } - } - - const { data } = await api.get( - `/api/placement_points?${params.toString()}` - ); - - return data; - }, - { keepPreviousData: true } - ); - - useEffect(() => { - if (!data) return; - - setFinalData(data); - }, [data]); - - const [shouldLoadClickedPoint, setShouldLoadClickedPoint] = useState(false); - - useEffect(() => { - if (!data || clickedPointConfig === null) return; - - const clickedPoint = data.results.find( - (item) => item.location_id === clickedPointConfig.id - ); - - if (clickedPoint) { - return; - } - - setShouldLoadClickedPoint(true); - }, [data, clickedPointConfig]); - - const { - data: remoteClickedPoint, - isInitialLoading, - isFetching, - } = useQuery( - ["clicked-point", clickedPointConfig], - async () => { - const params = new URLSearchParams({ - "location_ids[]": [clickedPointConfig.id], - }); - - const { data } = await api.get( - `/api/placement_points?${params.toString()}` - ); - - return data; - }, - { - enabled: shouldLoadClickedPoint, - onSuccess: () => setShouldLoadClickedPoint(false), - } - ); - - useEffect(() => { - if (!remoteClickedPoint) return; - - setPageSize((prevState) => prevState + 1); - setFinalData((prevState) => ({ - ...prevState, - count: prevState.count + 1, - results: [remoteClickedPoint.results[0], ...prevState.results], - })); - }, [remoteClickedPoint]); - - useEffect(() => { - if (clickedPointConfig === null) { - setPageSize(PAGE_SIZE); - setFinalData(data); - } - }, [clickedPointConfig]); - - return { - data: finalData, +export const Table = React.memo( + ({ + height = 200, + rowSelection, + data, pageSize, - isClickedPointLoading: isInitialLoading || isFetching, - }; -}; - -export const Table = React.memo(({ height = 200 }) => { - const { map } = useMap(); - const tableRef = useRef(null); - const [page, setPage] = useState(1); - const { selection, include, exclude } = usePointSelection(); - const { clickedPointConfig } = useClickedPointConfig(); - - const { data, pageSize, isClickedPointLoading } = useTableData(page); - - const SCROLL = { - y: `${height}px`, - x: "max-content", - }; - - const handlePageChange = useCallback((page) => setPage(page), []); - - const getSelectedRowKeys = useCallback(() => { - const ids = data?.results.map((item) => item.location_id) ?? []; - const clickedPoint = data?.results.find( - (item) => item.location_id === clickedPointConfig?.id - ); - - const inExcludedList = (id) => selection.excluded.has(id); - const shouldNotSelect = (id) => - id === clickedPoint?.id && clickedPointConfig?.shouldSelect === false; - - return [ - ...ids.filter((id) => { - return !inExcludedList(id) && !shouldNotSelect(id); - }), - ...selection.included, - ]; - }, [data, clickedPointConfig, selection]); - - const rowSelection = { - selectedRowKeys: getSelectedRowKeys(), - onSelect: (record, selected) => { - const { location_id: id } = record; - if (selected) { - include(id); - } else { - exclude(id); + isClickedPointLoading, + page, + onPageChange, + }) => { + const { clickedPointConfig } = useClickedPointConfig(); + const { map } = useMap(); + const tableRef = useRef(null); + + const SCROLL = { + y: `${height}px`, + x: "max-content", + }; + + useEffect(() => { + if (clickedPointConfig === null || isClickedPointLoading) return; + + const row = document.querySelector(".scroll-row"); + if (row) { + scrollIntoView(row, { behavior: "smooth" }); } - }, - hideSelectAll: true, - }; - - useEffect(() => { - if (clickedPointConfig === null || isClickedPointLoading) return; - - const row = document.querySelector(".scroll-row"); - if (row) { - scrollIntoView(row, { behavior: "smooth" }); - } - }, [clickedPointConfig, data]); - - return ( -
- - - }} - // loading={isLoading} - pagination={{ - pageSize, - current: page, - onChange: handlePageChange, - total: data?.count, - showSizeChanger: false, - position: "bottomCenter", - }} - dataSource={data?.results} - columns={columns} - rowKey="location_id" - scroll={SCROLL} - sticky={true} - onRow={(record) => { - return { - onClick: () => { - const geometry = parse(record.geometry); - map.flyTo({ - center: [geometry.coordinates[0], geometry.coordinates[1]], - zoom: 15, - speed: 5, - }); - }, - }; - }} - rowSelection={rowSelection} - rowClassName={(record) => - record.location_id === clickedPointConfig?.id ? "scroll-row" : "" - } - /> - - -
- ); -}); + }, [clickedPointConfig, data]); + + return ( +
+ + + }} + pagination={{ + pageSize, + current: page, + onChange: onPageChange, + total: data?.count, + showSizeChanger: false, + position: "bottomCenter", + }} + dataSource={data?.results} + columns={columns} + rowKey="location_id" + scroll={SCROLL} + sticky={true} + onRow={(record) => { + return { + onClick: () => { + const geometry = parse(record.geometry); + map.flyTo({ + center: [ + geometry.coordinates[0], + geometry.coordinates[1], + ], + zoom: 15, + speed: 5, + }); + }, + }; + }} + rowSelection={rowSelection} + rowClassName={(record) => + record.location_id === clickedPointConfig?.id + ? "scroll-row" + : "" + } + /> + + +
+ ); + } +); diff --git a/src/modules/Table/TableWrapper.jsx b/src/modules/Table/TableWrapper.jsx new file mode 100644 index 0000000..3870831 --- /dev/null +++ b/src/modules/Table/TableWrapper.jsx @@ -0,0 +1,13 @@ +import { useMode } from "../../stores/useMode"; +import { MODES } from "../../config"; +import { InitialTable } from "./InitialTable/InitialTable"; + +export const TableWrapper = () => { + const { mode } = useMode(); + + if (mode === MODES.INITIAL) { + return ; + } + + return null; +}; diff --git a/src/modules/Table/constants.js b/src/modules/Table/constants.js new file mode 100644 index 0000000..5ade0a7 --- /dev/null +++ b/src/modules/Table/constants.js @@ -0,0 +1 @@ +export const PAGE_SIZE = 30; diff --git a/src/modules/Table/useGetClickedPoint.js b/src/modules/Table/useGetClickedPoint.js new file mode 100644 index 0000000..3277c71 --- /dev/null +++ b/src/modules/Table/useGetClickedPoint.js @@ -0,0 +1,28 @@ +import { useClickedPointConfig } from "../../stores/useClickedPointConfig"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "../../api"; + +export const useGetClickedPoint = (enabled, onSuccess) => { + const { clickedPointConfig } = useClickedPointConfig(); + + const { data, isInitialLoading, isFetching } = useQuery( + ["clicked-point", clickedPointConfig], + async () => { + const params = new URLSearchParams({ + "location_ids[]": [clickedPointConfig.id], + }); + + const { data } = await api.get( + `/api/placement_points?${params.toString()}` + ); + + return data; + }, + { + enabled, + onSuccess, + } + ); + + return { data, isLoading: isInitialLoading || isFetching }; +}; diff --git a/src/modules/Table/useMergeTableData.js b/src/modules/Table/useMergeTableData.js new file mode 100644 index 0000000..8ea30f8 --- /dev/null +++ b/src/modules/Table/useMergeTableData.js @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; +import { PAGE_SIZE } from "./constants"; +import { useClickedPointConfig } from "../../stores/useClickedPointConfig"; +import { useGetClickedPoint } from "./useGetClickedPoint"; + +export const useMergeTableData = (fullData, onPageSizeChange) => { + const [mergedData, setMergedData] = useState(); + const [shouldLoadClickedPoint, setShouldLoadClickedPoint] = useState(false); + + const { data: clickedPointData, isLoading: isClickedPointLoading } = + useGetClickedPoint(shouldLoadClickedPoint, () => + setShouldLoadClickedPoint(false) + ); + + const { clickedPointConfig } = useClickedPointConfig(); + + useEffect(() => { + if (!fullData) return; + + setMergedData(fullData); + }, [fullData]); + + // find clicked point among already loaded data - if no - fetch it + useEffect(() => { + if (!fullData || clickedPointConfig === null) return; + + const clickedPoint = fullData.results.find( + (item) => item.location_id === clickedPointConfig.id + ); + + if (clickedPoint) { + return; + } + + setShouldLoadClickedPoint(true); + }, [fullData, clickedPointConfig]); + + // merge data with clicked point + useEffect(() => { + if (!clickedPointData) return; + + onPageSizeChange((prevState) => prevState + 1); + setMergedData((prevState) => ({ + ...prevState, + count: prevState.count + 1, + results: [clickedPointData.results[0], ...prevState.results], + })); + }, [clickedPointData]); + + // reset data after popup disappeared + useEffect(() => { + if (clickedPointConfig === null) { + onPageSizeChange(PAGE_SIZE); + setMergedData(fullData); + } + }, [clickedPointConfig, fullData]); + + return { data: mergedData, isClickedPointLoading }; +};