From c9cb7ad845ee723c898afecfe1b40a41e16e8a8a Mon Sep 17 00:00:00 2001 From: atrincas Date: Wed, 18 Dec 2024 17:00:34 +0100 Subject: [PATCH 1/5] feat: enhance UI components and navigation --- client/src/components/ui/search.tsx | 4 +- client/src/components/ui/sidebar.tsx | 8 ++- client/src/containers/my-projects/columns.tsx | 53 ++++++++++++------- client/src/containers/my-projects/index.tsx | 15 +++--- client/src/containers/nav/index.tsx | 24 +++++---- .../overview/header/parameters/index.tsx | 8 ++- client/src/containers/overview/map/index.tsx | 29 +++++++++- .../overview/table/view/overview/index.tsx | 11 +++- client/src/hooks/use-feature-flags.ts | 14 +++++ 9 files changed, 121 insertions(+), 45 deletions(-) diff --git a/client/src/components/ui/search.tsx b/client/src/components/ui/search.tsx index 2f5b1dcf..63a2c9e4 100644 --- a/client/src/components/ui/search.tsx +++ b/client/src/components/ui/search.tsx @@ -35,12 +35,12 @@ export default function Search({ return (
-
+ { setValue(e.target.value); diff --git a/client/src/components/ui/sidebar.tsx b/client/src/components/ui/sidebar.tsx index b7e11960..993f6aa3 100644 --- a/client/src/components/ui/sidebar.tsx +++ b/client/src/components/ui/sidebar.tsx @@ -37,6 +37,7 @@ type SidebarContext = { setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; + sidebarRef: React.RefObject; }; const SidebarContext = React.createContext(null); @@ -120,6 +121,9 @@ const SidebarProvider = React.forwardRef< // This makes it easier to style the sidebar with Tailwind classes. const state = open ? "expanded" : "collapsed"; + const internalRef = React.useRef(null); + const sidebarRef = (ref as React.RefObject) || internalRef; + const contextValue = React.useMemo( () => ({ state, @@ -129,6 +133,7 @@ const SidebarProvider = React.forwardRef< openMobile, setOpenMobile, toggleSidebar, + sidebarRef, }), [ state, @@ -138,6 +143,7 @@ const SidebarProvider = React.forwardRef< openMobile, setOpenMobile, toggleSidebar, + sidebarRef, ], ); @@ -145,6 +151,7 @@ const SidebarProvider = React.forwardRef<
{children} diff --git a/client/src/containers/my-projects/columns.tsx b/client/src/containers/my-projects/columns.tsx index 502541d5..cb4e01d9 100644 --- a/client/src/containers/my-projects/columns.tsx +++ b/client/src/containers/my-projects/columns.tsx @@ -1,6 +1,10 @@ import Link from "next/link"; -import { DotsHorizontalIcon, TrashIcon } from "@radix-ui/react-icons"; +import { + DotsHorizontalIcon, + ExclamationTriangleIcon, + TrashIcon, +} from "@radix-ui/react-icons"; import { ACTIVITY } from "@shared/entities/activity.enum"; import { CustomProject as CustomProjectEntity } from "@shared/entities/custom-project.entity"; import { Table as TableInstance, Row, ColumnDef } from "@tanstack/react-table"; @@ -8,6 +12,8 @@ import { Table as TableInstance, Row, ColumnDef } from "@tanstack/react-table"; import { formatCurrency } from "@/lib/format"; import { cn } from "@/lib/utils"; +import { useFeatureFlags } from "@/hooks/use-feature-flags"; + import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -24,25 +30,32 @@ type CustomColumn = ColumnDef & { className?: string; }; -const ActionsDropdown = () => ( - - - - - - {/* - - Update selection - */} - - - Delete selection - - - -); +const ActionsDropdown = () => { + const { "update-selection": updateSelection } = useFeatureFlags(); + return ( +
+ + + + + + {updateSelection && ( + + + Update selection + + )} + + + Delete selection + + + +
+ ); +}; export const columns: CustomColumn[] = [ { diff --git a/client/src/containers/my-projects/index.tsx b/client/src/containers/my-projects/index.tsx index 2a7c6477..2fe2a490 100644 --- a/client/src/containers/my-projects/index.tsx +++ b/client/src/containers/my-projects/index.tsx @@ -126,6 +126,7 @@ export default function MyProjectsView() { { select: (d) => d.body, queryKey: AllProjectsQueryKey, + placeholderData: keepPreviousData, }, ); const activities = useMemo( @@ -219,14 +220,12 @@ export default function MyProjectsView() { : undefined } > -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} {header.column.getCanSort() && ( -
+ <> {{ asc: , desc: ( @@ -235,7 +234,7 @@ export default function MyProjectsView() { }[header.column.getIsSorted() as string] ?? ( )} -
+ )}
)} diff --git a/client/src/containers/nav/index.tsx b/client/src/containers/nav/index.tsx index 3ee59a15..f4c08d78 100644 --- a/client/src/containers/nav/index.tsx +++ b/client/src/containers/nav/index.tsx @@ -9,6 +9,7 @@ import { ROLES } from "@shared/entities/users/roles.enum"; import { ClipboardEditIcon, ClipboardListIcon, + FileQuestionIcon, LayoutDashboardIcon, ServerCogIcon, UserIcon, @@ -17,6 +18,8 @@ import { useSession } from "next-auth/react"; import { cn } from "@/lib/utils"; +import { useFeatureFlags } from "@/hooks/use-feature-flags"; + import { Sidebar, SidebarContent, @@ -60,12 +63,12 @@ const navItems = { }, ], footer: [ - // { - // title: "Methodology", - // url: "/methodology", - // icon: FileQuestionIcon, - // match: (pathname: string) => pathname === "/methodology", - // }, + { + title: "Methodology", + url: "/methodology", + icon: FileQuestionIcon, + match: (pathname: string) => pathname === "/methodology", + }, ], }; @@ -74,7 +77,7 @@ export default function MainNav() { const { status, data } = useSession(); const pathname = usePathname(); const isAdmin = data?.user.role === ROLES.ADMIN; - + const { "methodology-page": methodologyPage } = useFeatureFlags(); const mainNavItems = useMemo( () => navItems.main.filter((item) => { @@ -88,6 +91,9 @@ export default function MainNav() { }), [isAdmin, status], ); + const footerNavItems = useMemo(() => { + return navItems.footer.filter(() => methodologyPage); + }, [methodologyPage]); return ( @@ -127,7 +133,7 @@ export default function MainNav() { - {/* {navItems.footer.map((item) => ( + {footerNavItems.map((item) => ( - ))} */} + ))} , "keyword">, @@ -96,7 +98,9 @@ export default function ParametersProjects() { }; return ( -
+
{PROJECT_PARAMETERS.map((parameter) => (
diff --git a/client/src/containers/overview/map/index.tsx b/client/src/containers/overview/map/index.tsx index edd0cfe9..43f91477 100644 --- a/client/src/containers/overview/map/index.tsx +++ b/client/src/containers/overview/map/index.tsx @@ -1,4 +1,6 @@ -import { ComponentProps, useState } from "react"; +import { ComponentProps, useEffect, useState } from "react"; + +import { useMap } from "react-map-gl"; import { useSetAtom } from "jotai"; import { LayersIcon } from "lucide-react"; @@ -21,6 +23,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { useSidebar } from "@/components/ui/sidebar"; export default function ProjectsMap() { const [isLegendOpen, setIsLegendOpen] = useState(false); @@ -31,9 +34,31 @@ export default function ProjectsMap() { })); const setPopup = useSetAtom(popupAtom); + const { state, sidebarRef } = useSidebar(); + const { default: map } = useMap(); + + useEffect(() => { + if (state === "collapsed") { + const handleTransitionEnd = (e: TransitionEvent) => { + if (e.propertyName === "width") { + map?.resize(); + } + }; + + const sidebarElement = sidebarRef.current; + sidebarElement?.addEventListener("transitionend", handleTransitionEnd); + + return () => { + sidebarElement?.removeEventListener( + "transitionend", + handleTransitionEnd, + ); + }; + } + }, [state, map, sidebarRef]); return ( -
+
( { @@ -211,7 +211,16 @@ export function OverviewTable() { key={cell.id} className={cn({ "p-0": cell.column.id === "scoreCardRating", + "group-hover:underline": cell.column.id === "projectName", + "min-w-[200px] max-w-[200px] truncate": + cell.column.id === "projectName", + "px-4 py-2": cell.column.id !== "scoreCardRating", })} + title={ + typeof cell.getValue() === "string" + ? (cell.getValue() as string) + : undefined + } > {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/client/src/hooks/use-feature-flags.ts b/client/src/hooks/use-feature-flags.ts index 0709f50e..fd8e5638 100644 --- a/client/src/hooks/use-feature-flags.ts +++ b/client/src/hooks/use-feature-flags.ts @@ -1,7 +1,21 @@ const FEATURE_FLAGS = { + /** Controls whether users can edit project details and settings in: + * - /projects/custom-project/details + * - /projects/custom-project/summary + */ "edit-project": false, + + /** Controls the visibility and sharing functionality in: + * - /profile + */ "share-information": false, "project-comparison": false, + /** Controls the actions dropdown functionality in: + * - /my-projects table + */ + "update-selection": false, + /** Controls the visibility of the methodology page */ + "methodology-page": false, } as const; type FeatureFlags = typeof FEATURE_FLAGS; From 619bc3728fd9daae97835badea35b7a10c20bd55 Mon Sep 17 00:00:00 2001 From: atrincas Date: Wed, 18 Dec 2024 17:06:32 +0100 Subject: [PATCH 2/5] Refactor project comparison feature flag --- client/src/containers/overview/project-details/index.tsx | 6 +++--- client/src/hooks/use-feature-flags.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/containers/overview/project-details/index.tsx b/client/src/containers/overview/project-details/index.tsx index 2292613e..b3d01232 100644 --- a/client/src/containers/overview/project-details/index.tsx +++ b/client/src/containers/overview/project-details/index.tsx @@ -143,7 +143,7 @@ const projectData = { export default function ProjectDetails() { const [projectDetails, setProjectDetails] = useAtom(projectDetailsAtom); - const featureFlags = useFeatureFlags(); + const { "project-comparison": projectComparison } = useFeatureFlags(); const handleOpenDetails = (open: boolean) => setProjectDetails({ ...projectDetails, isOpen: open }); @@ -366,7 +366,7 @@ export default function ProjectDetails() {

Scorecard ratings

- {featureFlags["project-comparison"] && ( + {projectComparison && (
- {featureFlags["project-comparison"] && ( + {projectComparison && (