From 83e91540854ea7279db2fd96b0d8714f5cea74e1 Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:10:40 +0530 Subject: [PATCH 1/8] Resolve infinite scroll issue in Resource page --- src/components/Kanban/Board.tsx | 89 +++++++++++++++++++-------------- tsconfig.json | 2 +- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx index 337f98f1caf..4dbaab78dc6 100644 --- a/src/components/Kanban/Board.tsx +++ b/src/components/Kanban/Board.tsx @@ -57,7 +57,7 @@ export default function KanbanBoard( -
+
{props.sections.map((section, i) => ( @@ -92,20 +92,25 @@ export function KanbanSection( const defaultLimit = 14; const { t } = useTranslation(); - // should be replaced with useInfiniteQuery when we move over to react query - const fetchNextPage = async (refresh: boolean = false) => { if (!refresh && (fetchingNextPage || !hasMore)) return; - if (refresh) setPages([]); + if (refresh) { + setPages([]); + setOffset(0); + } const offsetToUse = refresh ? 0 : offset; setFetchingNextPage(true); const res = await request(options.route, { ...options.options, - query: { ...options.options?.query, offsetToUse, limit: defaultLimit }, + query: { + ...options.options?.query, + offset: offsetToUse, + limit: defaultLimit, + }, }); + if (res.error) return; const newPages = refresh ? [] : [...pages]; const page = Math.floor(offsetToUse / defaultLimit); - if (res.error) return; newPages[page] = (res.data as any).results; setPages(newPages); setHasMore(!!(res.data as any)?.next); @@ -117,24 +122,30 @@ export function KanbanSection( const items = pages.flat(); useEffect(() => { - const onBoardReachEnd = async () => { - const sectionElementHeight = - sectionRef.current?.getBoundingClientRect().height; - const scrolled = props.boardRef.current?.scrollTop; - // if user has scrolled 3/4th of the current items + const onBoardReachEnd = () => { if ( - scrolled && - sectionElementHeight && - scrolled > sectionElementHeight * (3 / 4) - ) { + !sectionRef.current || + !props.boardRef.current || + fetchingNextPage || + !hasMore + ) + return; + + const scrollTop = props.boardRef.current.scrollTop; + const visibleHeight = props.boardRef.current.offsetHeight; + const sectionHeight = sectionRef.current.offsetHeight; + + if (scrollTop + visibleHeight >= sectionHeight - 100) { fetchNextPage(); } }; - props.boardRef.current?.addEventListener("scroll", onBoardReachEnd); + const debouncedScroll = debounce(onBoardReachEnd, 200); + + props.boardRef.current?.addEventListener("scroll", debouncedScroll); return () => - props.boardRef.current?.removeEventListener("scroll", onBoardReachEnd); - }, [props.boardRef, fetchingNextPage, hasMore]); + props.boardRef.current?.removeEventListener("scroll", debouncedScroll); + }, [fetchingNextPage, hasMore, props.boardRef]); useEffect(() => { fetchNextPage(true); @@ -145,9 +156,7 @@ export function KanbanSection( {(provided) => (
@@ -165,22 +174,20 @@ export function KanbanSection( {t("no_results_found")}
)} - {items - .filter((item) => item) - .map((item, i) => ( - - {(provided) => ( -
- {props.itemRender(item)} -
- )} -
- ))} + {items.map((item, i) => ( + + {(provided) => ( +
+ {props.itemRender(item)} +
+ )} +
+ ))} {fetchingNextPage && (
)} @@ -192,3 +199,11 @@ export function KanbanSection( } export type KanbanBoardType = typeof KanbanBoard; + +function debounce(fn: () => void, delay: number) { + let timeout: NodeJS.Timeout | null = null; + return () => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(fn, delay); + }; +} diff --git a/tsconfig.json b/tsconfig.json index 51ffeb4a0ae..54f36fde5d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["vite/client", "vite-plugin-pwa/client"], + "types": ["vite/client", "vite-plugin-pwa/client","node"], "baseUrl": ".", "paths": { "@/*": ["./src/*"], From 97119a00e47ecf2ab328cf0f5da666f88e6218e4 Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:02:45 +0530 Subject: [PATCH 2/8] useDebounce hook implemented --- package-lock.json | 112 +++++++++++++++++++++- package.json | 2 + src/components/Kanban/Board.tsx | 58 +++++------ src/components/Resource/ResourceBoard.tsx | 27 ++++-- src/components/Shifting/ShiftingBoard.tsx | 30 ++++-- src/hooks/useDebounce.ts | 29 ++++++ 6 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/package-lock.json b/package-lock.json index 97073c88ecb..1a5fef834d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "qrcode.react": "^4.1.0", "raviger": "^4.1.2", "react": "18.3.1", + "react-beautiful-dnd": "^13.1.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", @@ -72,6 +73,7 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", + "@types/react-beautiful-dnd": "^13.1.8", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.3.1", @@ -5509,6 +5511,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/is-empty": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/is-empty/-/is-empty-1.2.3.tgz", @@ -5591,20 +5603,28 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.8", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", + "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-copy-to-clipboard": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.7.tgz", @@ -5645,6 +5665,27 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -7681,7 +7722,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/cypress": { @@ -15623,6 +15663,72 @@ "react": ">=16.4.1" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-beautiful-dnd/node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/react-beautiful-dnd/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-beautiful-dnd/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-beautiful-dnd/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/react-copy-to-clipboard": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", diff --git a/package.json b/package.json index 798c910ea91..b57c347c224 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "qrcode.react": "^4.1.0", "raviger": "^4.1.2", "react": "18.3.1", + "react-beautiful-dnd": "^13.1.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", @@ -111,6 +112,7 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", + "@types/react-beautiful-dnd": "^13.1.8", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.3.1", diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx index 4dbaab78dc6..24c3263cd00 100644 --- a/src/components/Kanban/Board.tsx +++ b/src/components/Kanban/Board.tsx @@ -9,11 +9,13 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; +import useDebounce from "@/hooks/useDebounce"; + import request from "@/Utils/request/request"; import { QueryRoute } from "@/Utils/request/types"; import { QueryOptions } from "@/Utils/request/useQuery"; -interface KanbanBoardProps { +export interface KanbanBoardProps { title?: ReactNode; onDragEnd: OnDragEndResponder; sections: { @@ -30,6 +32,8 @@ interface KanbanBoardProps { itemRender: (item: T) => ReactNode; } +export type KanbanBoardType = KanbanBoardProps; + export default function KanbanBoard( props: KanbanBoardProps, ) { @@ -74,7 +78,7 @@ export default function KanbanBoard( ); } -export function KanbanSection( +function KanbanSection( props: Omit, "sections" | "onDragEnd"> & { section: KanbanBoardProps["sections"][number]; boardRef: RefObject; @@ -121,31 +125,29 @@ export function KanbanSection( const items = pages.flat(); - useEffect(() => { - const onBoardReachEnd = () => { - if ( - !sectionRef.current || - !props.boardRef.current || - fetchingNextPage || - !hasMore - ) - return; - - const scrollTop = props.boardRef.current.scrollTop; - const visibleHeight = props.boardRef.current.offsetHeight; - const sectionHeight = sectionRef.current.offsetHeight; - - if (scrollTop + visibleHeight >= sectionHeight - 100) { - fetchNextPage(); - } - }; - - const debouncedScroll = debounce(onBoardReachEnd, 200); + const debouncedScroll = useDebounce(() => { + if ( + !sectionRef.current || + !props.boardRef.current || + fetchingNextPage || + !hasMore + ) + return; + + const scrollTop = props.boardRef.current.scrollTop; + const visibleHeight = props.boardRef.current.offsetHeight; + const sectionHeight = sectionRef.current.offsetHeight; + + if (scrollTop + visibleHeight >= sectionHeight - 100) { + fetchNextPage(); + } + }, 200); + useEffect(() => { props.boardRef.current?.addEventListener("scroll", debouncedScroll); return () => props.boardRef.current?.removeEventListener("scroll", debouncedScroll); - }, [fetchingNextPage, hasMore, props.boardRef]); + }, [fetchingNextPage, hasMore, props.boardRef, debouncedScroll]); useEffect(() => { fetchNextPage(true); @@ -197,13 +199,3 @@ export function KanbanSection( ); } - -export type KanbanBoardType = typeof KanbanBoard; - -function debounce(fn: () => void, delay: number) { - let timeout: NodeJS.Timeout | null = null; - return () => { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(fn, delay); - }; -} diff --git a/src/components/Resource/ResourceBoard.tsx b/src/components/Resource/ResourceBoard.tsx index f76604ff9f6..27576630b6a 100644 --- a/src/components/Resource/ResourceBoard.tsx +++ b/src/components/Resource/ResourceBoard.tsx @@ -1,5 +1,5 @@ import { navigate } from "raviger"; -import { Suspense, lazy, useState } from "react"; +import React, { Suspense, lazy, useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -12,7 +12,7 @@ import PageTitle from "@/components/Common/PageTitle"; import Tabs from "@/components/Common/Tabs"; import { ResourceModel } from "@/components/Facility/models"; import SearchInput from "@/components/Form/SearchInput"; -import type { KanbanBoardType } from "@/components/Kanban/Board"; +import type { KanbanBoardProps } from "@/components/Kanban/Board"; import BadgesList from "@/components/Resource/ResourceBadges"; import ResourceBlock from "@/components/Resource/ResourceBlock"; import { formatFilter } from "@/components/Resource/ResourceCommons"; @@ -25,9 +25,17 @@ import { RESOURCE_CHOICES } from "@/common/constants"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; -const KanbanBoard = lazy( +// Helper function to type lazy-loaded components +function lazyWithProps( + factory: () => Promise<{ default: React.ComponentType }>, +) { + return lazy(factory) as React.LazyExoticComponent>; +} + +// Correctly lazy-load KanbanBoard with its props +const KanbanBoard = lazyWithProps>( () => import("@/components/Kanban/Board"), -) as KanbanBoardType; +); const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); @@ -39,8 +47,7 @@ export default function BoardView() { limit: -1, cacheBlacklist: ["title"], }); - const [boardFilter, setBoardFilter] = useState(ACTIVE); - // eslint-disable-next-line + const [boardFilter, setBoardFilter] = useState(resourceStatusOptions); const appliedFilters = formatFilter(qParams); const { t } = useTranslation(); @@ -103,7 +110,7 @@ export default function BoardView() {
}> - + } sections={boardFilter.map((board) => ({ id: board, @@ -127,7 +134,7 @@ export default function BoardView() { /> ), - fetchOptions: (id) => ({ + fetchOptions: (id: string) => ({ route: routes.listResourceRequests, options: { query: formatFilter({ @@ -143,7 +150,9 @@ export default function BoardView() { `/resource/${result.draggableId}/update?status=${result.destination?.droppableId}`, ); }} - itemRender={(resource) => } + itemRender={(resource: ResourceModel) => ( + + )} /> diff --git a/src/components/Shifting/ShiftingBoard.tsx b/src/components/Shifting/ShiftingBoard.tsx index 8486c8dd4d0..03dc5aeb948 100644 --- a/src/components/Shifting/ShiftingBoard.tsx +++ b/src/components/Shifting/ShiftingBoard.tsx @@ -1,6 +1,7 @@ import careConfig from "@careConfig"; import { navigate } from "raviger"; import { Suspense, lazy, useState } from "react"; +import { DropResult } from "react-beautiful-dnd"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -14,7 +15,7 @@ import PageTitle from "@/components/Common/PageTitle"; import Tabs from "@/components/Common/Tabs"; import { ShiftingModel } from "@/components/Facility/models"; import SearchInput from "@/components/Form/SearchInput"; -import type { KanbanBoardType } from "@/components/Kanban/Board"; +import type { KanbanBoardProps } from "@/components/Kanban/Board"; import BadgesList from "@/components/Shifting/ShiftingBadges"; import ShiftingBlock from "@/components/Shifting/ShiftingBlock"; import { formatFilter } from "@/components/Shifting/ShiftingCommons"; @@ -30,9 +31,15 @@ import { import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; -const KanbanBoard = lazy( +function lazyWithProps( + factory: () => Promise<{ default: React.ComponentType }>, +) { + return lazy(factory) as React.LazyExoticComponent>; +} + +const KanbanBoard = lazyWithProps>( () => import("@/components/Kanban/Board"), -) as KanbanBoardType; +); export default function BoardView() { const { qParams, updateQuery, FilterBadges, advancedFilter } = useFilters({ @@ -133,7 +140,7 @@ export default function BoardView() {
}> - + } sections={boardFilter.map((board) => ({ id: board.text, @@ -157,7 +164,7 @@ export default function BoardView() { /> ), - fetchOptions: (id) => ({ + fetchOptions: (id: string) => ({ route: routes.listShiftRequests, options: { query: formatFilter({ @@ -167,13 +174,18 @@ export default function BoardView() { }, }), }))} - onDragEnd={(result) => { - if (result.source.droppableId !== result.destination?.droppableId) + onDragEnd={(result: DropResult) => { + // Ensure destination is not null + if ( + result.destination && + result.source.droppableId !== result.destination.droppableId + ) { navigate( - `/shifting/${result.draggableId}/update?status=${result.destination?.droppableId}`, + `/shifting/${result.draggableId}/update?status=${result.destination.droppableId}`, ); + } }} - itemRender={(shift) => ( + itemRender={(shift: ShiftingModel) => ( setModalFor(shift)} shift={shift} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 00000000000..66ee398f962 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef } from "react"; + +export default function useDebounce( + callback: (...args: T) => void, + delay: number, +) { + const callbackRef = useRef(callback); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + const debouncedCallback = (...args: T) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + callbackRef.current(...args); + }, delay); + }; + return debouncedCallback; +} From 4b3a5573b8fe98f14bd92bd198923dbf54b02f33 Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:25:40 +0530 Subject: [PATCH 3/8] removed react-beautiful-dnd --- package-lock.json | 112 +--------------------- package.json | 2 - src/components/Shifting/ShiftingBoard.tsx | 2 +- 3 files changed, 4 insertions(+), 112 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a5fef834d8..97073c88ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,6 @@ "qrcode.react": "^4.1.0", "raviger": "^4.1.2", "react": "18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", @@ -73,7 +72,6 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", - "@types/react-beautiful-dnd": "^13.1.8", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.3.1", @@ -5511,16 +5509,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/is-empty": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/is-empty/-/is-empty-1.2.3.tgz", @@ -5603,28 +5591,20 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.8", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", - "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-copy-to-clipboard": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.7.tgz", @@ -5665,27 +5645,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "node_modules/@types/react-redux/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -7722,6 +7681,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, "license": "MIT" }, "node_modules/cypress": { @@ -15663,72 +15623,6 @@ "react": ">=16.4.1" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-beautiful-dnd/node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" - }, - "node_modules/react-beautiful-dnd/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, - "node_modules/react-beautiful-dnd/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-beautiful-dnd/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/react-copy-to-clipboard": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", diff --git a/package.json b/package.json index b57c347c224..798c910ea91 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "qrcode.react": "^4.1.0", "raviger": "^4.1.2", "react": "18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", @@ -112,7 +111,6 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", - "@types/react-beautiful-dnd": "^13.1.8", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-csv": "^1.1.10", "@types/react-dom": "^18.3.1", diff --git a/src/components/Shifting/ShiftingBoard.tsx b/src/components/Shifting/ShiftingBoard.tsx index 03dc5aeb948..915b7bd38ba 100644 --- a/src/components/Shifting/ShiftingBoard.tsx +++ b/src/components/Shifting/ShiftingBoard.tsx @@ -1,7 +1,7 @@ import careConfig from "@careConfig"; +import { DropResult } from "@hello-pangea/dnd"; import { navigate } from "raviger"; import { Suspense, lazy, useState } from "react"; -import { DropResult } from "react-beautiful-dnd"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; From c8452e5874e7329d661a05223fcd11ffca140e93 Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:57:12 +0530 Subject: [PATCH 4/8] removed node --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 54f36fde5d8..51ffeb4a0ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["vite/client", "vite-plugin-pwa/client","node"], + "types": ["vite/client", "vite-plugin-pwa/client"], "baseUrl": ".", "paths": { "@/*": ["./src/*"], From 669bdc901dd10cbb4786245f30004672b0b92d8d Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Wed, 11 Dec 2024 04:59:01 +0530 Subject: [PATCH 5/8] Removed KanbanBoardType --- src/components/Kanban/Board.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx index 24c3263cd00..cb92abbfcc4 100644 --- a/src/components/Kanban/Board.tsx +++ b/src/components/Kanban/Board.tsx @@ -32,8 +32,6 @@ export interface KanbanBoardProps { itemRender: (item: T) => ReactNode; } -export type KanbanBoardType = KanbanBoardProps; - export default function KanbanBoard( props: KanbanBoardProps, ) { From d3b81025c9f01259fab545e1e1903092ebff8f08 Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Wed, 11 Dec 2024 06:12:00 +0530 Subject: [PATCH 6/8] useInfiniteQuery hook implemented --- src/Utils/request/useInfiniteQuery.ts | 61 ++++++++++++++++++ src/components/Kanban/Board.tsx | 89 +++++---------------------- 2 files changed, 77 insertions(+), 73 deletions(-) create mode 100644 src/Utils/request/useInfiniteQuery.ts diff --git a/src/Utils/request/useInfiniteQuery.ts b/src/Utils/request/useInfiniteQuery.ts new file mode 100644 index 00000000000..33c2467d8a3 --- /dev/null +++ b/src/Utils/request/useInfiniteQuery.ts @@ -0,0 +1,61 @@ +import { useCallback, useState } from "react"; + +import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; + +import { PaginatedResponse, QueryRoute } from "@/Utils/request/types"; +import useQuery, { QueryOptions } from "@/Utils/request/useQuery"; + +export interface InfiniteQueryOptions + extends QueryOptions> { + deduplicateBy: (item: TItem) => string | number; +} + +export function useInfiniteQuery( + route: QueryRoute>, + options?: InfiniteQueryOptions, +) { + const [items, setItems] = useState([]); + const [totalCount, setTotalCount] = useState(); + const [offset, setOffset] = useState(0); + + const { refetch, loading, ...queryResponse } = useQuery(route, { + ...options, + query: { + ...(options?.query ?? {}), + offset, + }, + onResponse: ({ data }) => { + if (!data) return; + const allItems = items.concat(data.results); + + const deduplicatedItems = options?.deduplicateBy + ? Array.from( + allItems + .reduce((map, item) => { + const key = options.deduplicateBy(item); + return map.set(key, item); + }, new Map()) + .values(), + ) + : allItems; + + setItems(deduplicatedItems); + setTotalCount(data.count); + }, + }); + + const fetchNextPage = useCallback(() => { + if (loading) return; + setOffset((prevOffset) => prevOffset + RESULTS_PER_PAGE_LIMIT); + }, [loading]); + + return { + items, + loading, + fetchNextPage, + refetch, + totalCount, + hasMore: items.length < (totalCount ?? 0), + ...queryResponse, + }; +} diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx index cb92abbfcc4..8922b388b6c 100644 --- a/src/components/Kanban/Board.tsx +++ b/src/components/Kanban/Board.tsx @@ -4,15 +4,14 @@ import { Droppable, OnDragEndResponder, } from "@hello-pangea/dnd"; -import { ReactNode, RefObject, useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { ReactNode, RefObject, useEffect, useRef } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; import useDebounce from "@/hooks/useDebounce"; -import request from "@/Utils/request/request"; -import { QueryRoute } from "@/Utils/request/types"; +import { PaginatedResponse, QueryRoute } from "@/Utils/request/types"; +import { useInfiniteQuery } from "@/Utils/request/useInfiniteQuery"; import { QueryOptions } from "@/Utils/request/useQuery"; export interface KanbanBoardProps { @@ -25,7 +24,7 @@ export interface KanbanBoardProps { id: string, ...args: unknown[] ) => { - route: QueryRoute; + route: QueryRoute>; options?: QueryOptions; }; }[]; @@ -83,58 +82,21 @@ function KanbanSection( }, ) { const { section } = props; - const [offset, setOffset] = useState(0); - const [pages, setPages] = useState([]); - const [fetchingNextPage, setFetchingNextPage] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [totalCount, setTotalCount] = useState(); - const options = section.fetchOptions(section.id); - const sectionRef = useRef(null); - const defaultLimit = 14; - const { t } = useTranslation(); - - const fetchNextPage = async (refresh: boolean = false) => { - if (!refresh && (fetchingNextPage || !hasMore)) return; - if (refresh) { - setPages([]); - setOffset(0); - } - const offsetToUse = refresh ? 0 : offset; - setFetchingNextPage(true); - const res = await request(options.route, { - ...options.options, - query: { - ...options.options?.query, - offset: offsetToUse, - limit: defaultLimit, - }, - }); - if (res.error) return; - const newPages = refresh ? [] : [...pages]; - const page = Math.floor(offsetToUse / defaultLimit); - newPages[page] = (res.data as any).results; - setPages(newPages); - setHasMore(!!(res.data as any)?.next); - setTotalCount((res.data as any)?.count); - setOffset(offsetToUse + defaultLimit); - setFetchingNextPage(false); - }; - - const items = pages.flat(); + const { items, loading, fetchNextPage, hasMore } = useInfiniteQuery( + section.fetchOptions(section.id).route, + { + ...section.fetchOptions(section.id).options, + deduplicateBy: (item) => item.id, + }, + ); const debouncedScroll = useDebounce(() => { - if ( - !sectionRef.current || - !props.boardRef.current || - fetchingNextPage || - !hasMore - ) - return; + if (!props.boardRef.current || loading || !hasMore) return; const scrollTop = props.boardRef.current.scrollTop; const visibleHeight = props.boardRef.current.offsetHeight; - const sectionHeight = sectionRef.current.offsetHeight; + const sectionHeight = props.boardRef.current.scrollHeight; if (scrollTop + visibleHeight >= sectionHeight - 100) { fetchNextPage(); @@ -145,11 +107,7 @@ function KanbanSection( props.boardRef.current?.addEventListener("scroll", debouncedScroll); return () => props.boardRef.current?.removeEventListener("scroll", debouncedScroll); - }, [fetchingNextPage, hasMore, props.boardRef, debouncedScroll]); - - useEffect(() => { - fetchNextPage(true); - }, [props.section]); + }, [loading, hasMore, debouncedScroll, props.boardRef]); // Add props.boardRef here return ( @@ -158,22 +116,7 @@ function KanbanSection( ref={provided.innerRef} className="relative mr-2 w-[300px] shrink-0 rounded-xl bg-secondary-200" > -
-
-
{section.title}
-
- - {typeof totalCount === "undefined" ? "..." : totalCount} - -
-
-
-
- {!fetchingNextPage && totalCount === 0 && ( -
- {t("no_results_found")} -
- )} +
{items.map((item, i) => ( {(provided) => ( @@ -188,7 +131,7 @@ function KanbanSection( )} ))} - {fetchingNextPage && ( + {loading && (
)}
From f9249dc0d312ad425e7eef6f9a22fc0860adfebd Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:16:52 +0530 Subject: [PATCH 7/8] Initialize totalCount with Infinity --- src/Utils/request/useInfiniteQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Utils/request/useInfiniteQuery.ts b/src/Utils/request/useInfiniteQuery.ts index 33c2467d8a3..71746655eaf 100644 --- a/src/Utils/request/useInfiniteQuery.ts +++ b/src/Utils/request/useInfiniteQuery.ts @@ -15,7 +15,7 @@ export function useInfiniteQuery( options?: InfiniteQueryOptions, ) { const [items, setItems] = useState([]); - const [totalCount, setTotalCount] = useState(); + const [totalCount, setTotalCount] = useState(Infinity); const [offset, setOffset] = useState(0); const { refetch, loading, ...queryResponse } = useQuery(route, { @@ -55,7 +55,7 @@ export function useInfiniteQuery( fetchNextPage, refetch, totalCount, - hasMore: items.length < (totalCount ?? 0), + hasMore: totalCount ? items.length < totalCount : true, ...queryResponse, }; } From b9c0bc3f052c68c34e58d14b3ec31967399a599a Mon Sep 17 00:00:00 2001 From: Nikhila C <115739037+Nikhilaniki@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:38:35 +0530 Subject: [PATCH 8/8] useDebounce removed --- src/components/Kanban/Board.tsx | 109 ++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/src/components/Kanban/Board.tsx b/src/components/Kanban/Board.tsx index 8922b388b6c..1bed14bf269 100644 --- a/src/components/Kanban/Board.tsx +++ b/src/components/Kanban/Board.tsx @@ -4,15 +4,16 @@ import { Droppable, OnDragEndResponder, } from "@hello-pangea/dnd"; -import { ReactNode, RefObject, useEffect, useRef } from "react"; +import { ReactNode, RefObject, useEffect, useRef, useState } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import useDebounce from "@/hooks/useDebounce"; - -import { PaginatedResponse, QueryRoute } from "@/Utils/request/types"; -import { useInfiniteQuery } from "@/Utils/request/useInfiniteQuery"; -import { QueryOptions } from "@/Utils/request/useQuery"; +import { + PaginatedResponse, + QueryParams, + QueryRoute, +} from "@/Utils/request/types"; +import useTanStackQueryInstead from "@/Utils/request/useQuery"; export interface KanbanBoardProps { title?: ReactNode; @@ -25,7 +26,7 @@ export interface KanbanBoardProps { ...args: unknown[] ) => { route: QueryRoute>; - options?: QueryOptions; + options?: { query?: QueryParams }; }; }[]; itemRender: (item: T) => ReactNode; @@ -83,31 +84,61 @@ function KanbanSection( ) { const { section } = props; - const { items, loading, fetchNextPage, hasMore } = useInfiniteQuery( - section.fetchOptions(section.id).route, - { - ...section.fetchOptions(section.id).options, - deduplicateBy: (item) => item.id, - }, - ); - - const debouncedScroll = useDebounce(() => { - if (!props.boardRef.current || loading || !hasMore) return; + const [items, setItems] = useState([]); + const [isFetching, setIsFetching] = useState(false); + const [nextPage, setNextPage] = useState(null); + const [totalCount, setTotalCount] = useState(0); - const scrollTop = props.boardRef.current.scrollTop; - const visibleHeight = props.boardRef.current.offsetHeight; - const sectionHeight = props.boardRef.current.scrollHeight; + const { data, loading, refetch } = useTanStackQueryInstead< + PaginatedResponse + >(section.fetchOptions(section.id).route, { + ...section.fetchOptions(section.id).options, + query: section.fetchOptions(section.id).options?.query as QueryParams, + }); - if (scrollTop + visibleHeight >= sectionHeight - 100) { - fetchNextPage(); + useEffect(() => { + if (data?.results) { + setItems((prevItems) => { + const newItems = data.results.filter( + (newItem) => !prevItems.some((item) => item.id === newItem.id), + ); + return [...prevItems, ...newItems]; + }); + setNextPage(data.next || null); + setTotalCount(data.count || 0); } - }, 200); + }, [data]); + + const fetchNextPage = () => { + if (loading || isFetching || !nextPage) return; + setIsFetching(true); + refetch().finally(() => setIsFetching(false)); + }; useEffect(() => { - props.boardRef.current?.addEventListener("scroll", debouncedScroll); - return () => - props.boardRef.current?.removeEventListener("scroll", debouncedScroll); - }, [loading, hasMore, debouncedScroll, props.boardRef]); // Add props.boardRef here + const currentRef = props.boardRef.current; + + if (!currentRef) return; + + const observer = new IntersectionObserver( + (entries) => { + const lastEntry = entries[0]; + if (lastEntry.isIntersecting) { + fetchNextPage(); + } + }, + { root: currentRef, threshold: 0.5 }, + ); + + const lastItem = currentRef?.lastElementChild; + if (lastItem) observer.observe(lastItem); + + return () => { + if (lastItem) observer.unobserve(lastItem); + }; + }, [nextPage, loading, isFetching]); + + // console.log("Items:", items); return ( @@ -116,9 +147,20 @@ function KanbanSection( ref={provided.innerRef} className="relative mr-2 w-[300px] shrink-0 rounded-xl bg-secondary-200" > -
+
+
+
{section.title}
+
+ + {totalCount} + +
+
+
+ +
{items.map((item, i) => ( - + {(provided) => (
( )} ))} - {loading && ( -
- )} + + {loading || isFetching ? ( +
+ Loading... +
+ ) : null}
)}