From 8abcdc8799c596775c7f3524a5a0edea55ad8906 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Wed, 20 Nov 2024 18:29:07 +0100 Subject: [PATCH 01/55] refactor(sidebar): add very basic shadcn sidebar --- app/components/layout/sidebar/app-sidebar.tsx | 30 + app/components/layout/sidebar/sidebar-old.tsx | 99 +++ app/components/layout/sidebar/sidebar.tsx | 836 ++++++++++++++++-- app/components/marketing/logos.tsx | 9 +- app/components/shared/separator.tsx | 28 + app/components/shared/sheet.tsx | 137 +++ app/components/shared/skeleton.tsx | 15 + app/hooks/use-mobile.ts | 24 + app/routes/_layout+/_layout.tsx | 71 +- package-lock.json | 46 + package.json | 2 + tailwind.config.ts | 2 + 12 files changed, 1177 insertions(+), 122 deletions(-) create mode 100644 app/components/layout/sidebar/app-sidebar.tsx create mode 100644 app/components/layout/sidebar/sidebar-old.tsx create mode 100644 app/components/shared/separator.tsx create mode 100644 app/components/shared/sheet.tsx create mode 100644 app/components/shared/skeleton.tsx create mode 100644 app/hooks/use-mobile.ts diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx new file mode 100644 index 000000000..ea82de16e --- /dev/null +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -0,0 +1,30 @@ +import { ShelfSidebarLogo } from "~/components/marketing/logos"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, + useSidebar, +} from "./sidebar"; + +type AppSidebarProps = React.ComponentProps; + +export default function AppSidebar(props: AppSidebarProps) { + const { state } = useSidebar(); + + return ( + + +
+ +
+
+ + This is content of sidebar + + this is footer + +
+ ); +} diff --git a/app/components/layout/sidebar/sidebar-old.tsx b/app/components/layout/sidebar/sidebar-old.tsx new file mode 100644 index 000000000..b32d72635 --- /dev/null +++ b/app/components/layout/sidebar/sidebar-old.tsx @@ -0,0 +1,99 @@ +import { useRef } from "react"; +import { Link, NavLink, useFetcher, useLoaderData } from "@remix-run/react"; +import { useAtom } from "jotai"; +import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; +import { ScanQRIcon } from "~/components/icons/library"; +import type { loader } from "~/routes/_layout+/_layout"; +import { tw } from "~/utils/tw"; + +import { toggleMobileNavAtom } from "./atoms"; +import SidebarBottom from "./bottom"; +import MenuButton from "./menu-button"; +import MenuItems from "./menu-items"; +import { OrganizationSelectForm } from "./organization-select-form"; +import Overlay from "./overlay"; +import { ShelfMobileLogo, ShelfSidebarLogo } from "../../marketing/logos"; + +export default function Sidebar() { + const { user, minimizedSidebar, currentOrganizationId } = + useLoaderData(); + const [isMobileNavOpen, toggleMobileNav] = useAtom(toggleMobileNavAtom); + const mainNavigationRef = useRef(null); + + /** We use optimistic UI for folding of the scanner.tsx sidebar + * As we are making a request to the server to store the cookie, + * we need to use this approach, otherwise the sidebar will close/open + * only once the response is received from the server + */ + const sidebarFetcher = useFetcher(); + let optimisticMinimizedSidebar = minimizedSidebar; + if (sidebarFetcher.formData) { + optimisticMinimizedSidebar = + sidebarFetcher.formData.get("minimizeSidebar") === "open"; + } + const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); + return ( + <> + {/* this component is named sidebar as of now but also serves as a mobile navigation header in mobile device */} + + + + + + ); +} diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index b32d72635..11c6f2b06 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -1,99 +1,761 @@ -import { useRef } from "react"; -import { Link, NavLink, useFetcher, useLoaderData } from "@remix-run/react"; -import { useAtom } from "jotai"; -import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; -import { ScanQRIcon } from "~/components/icons/library"; -import type { loader } from "~/routes/_layout+/_layout"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; + +import { + createContext, + forwardRef, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useIsMobile } from "~/hooks/use-mobile"; +import { ArrowLeftIcon, SwitchIcon } from "~/components/icons/library"; +import { Button } from "~/components/shared/button"; import { tw } from "~/utils/tw"; +import Input from "~/components/forms/input"; +import { Separator } from "~/components/shared/separator"; +import { Sheet, SheetContent } from "~/components/shared/sheet"; +import { Skeleton } from "~/components/shared/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/shared/tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContext = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = createContext(null); -import { toggleMobileNavAtom } from "./atoms"; -import SidebarBottom from "./bottom"; -import MenuButton from "./menu-button"; -import MenuItems from "./menu-items"; -import { OrganizationSelectForm } from "./organization-select-form"; -import Overlay from "./overlay"; -import { ShelfMobileLogo, ShelfSidebarLogo } from "../../marketing/logos"; - -export default function Sidebar() { - const { user, minimizedSidebar, currentOrganizationId } = - useLoaderData(); - const [isMobileNavOpen, toggleMobileNav] = useAtom(toggleMobileNavAtom); - const mainNavigationRef = useRef(null); - - /** We use optimistic UI for folding of the scanner.tsx sidebar - * As we are making a request to the server to store the cookie, - * we need to use this approach, otherwise the sidebar will close/open - * only once the response is received from the server - */ - const sidebarFetcher = useFetcher(); - let optimisticMinimizedSidebar = minimizedSidebar; - if (sidebarFetcher.formData) { - optimisticMinimizedSidebar = - sidebarFetcher.formData.get("minimizeSidebar") === "open"; +function useSidebar() { + const context = useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); } - const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); - return ( - <> - {/* this component is named sidebar as of now but also serves as a mobile navigation header in mobile device */} - + ); + } - - - +const SidebarTrigger = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + ); -} +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + + + + + ); +} From c31510907a7ad094bcbba8d29fa28e702a8d004d Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 12:54:20 +0100 Subject: [PATCH 03/55] refactor(sidebar): fix organization-selector on mobile --- app/components/layout/sidebar/app-sidebar.tsx | 3 ++ .../layout/sidebar/organization-selector.tsx | 31 +++++++++++++------ app/components/layout/sidebar/sidebar.tsx | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index ea82de16e..07f7920f1 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -7,6 +7,7 @@ import { SidebarRail, useSidebar, } from "./sidebar"; +import OrganizationSelector from "./organization-selector"; type AppSidebarProps = React.ComponentProps; @@ -19,6 +20,8 @@ export default function AppSidebar(props: AppSidebarProps) {
+ + This is content of sidebar diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 75bfa2e7b..33c122343 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -6,7 +6,12 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from "~/components/shared/dropdown"; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "./sidebar"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./sidebar"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { useIsMobile } from "~/hooks/use-mobile"; import { useFetcher, useLoaderData } from "@remix-run/react"; @@ -16,9 +21,12 @@ import { Image } from "~/components/shared/image"; import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; +import When from "~/components/when/when"; export default function OrganizationSelector() { const isMobile = useIsMobile(); + const { open } = useSidebar(); + const { organizations, currentOrganizationId } = useLoaderData(); @@ -50,7 +58,7 @@ export default function OrganizationSelector() { {currentOrganization.type === "PERSONAL" ? ( @@ -58,16 +66,21 @@ export default function OrganizationSelector() { img )} -
- - {currentOrganization.name} - -
- + + + <> +
+ + {currentOrganization.name} + +
+ + +
diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index 11c6f2b06..be1e71048 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -511,7 +511,7 @@ const SidebarMenuItem = forwardRef>( SidebarMenuItem.displayName = "SidebarMenuItem"; const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: { From 47879a8d11e13d164afd0af5b3c6e0115cb1851c Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 13:24:53 +0100 Subject: [PATCH 04/55] refactor(sidebar): create user menu for sidebar --- app/components/layout/sidebar/app-sidebar.tsx | 5 +- .../layout/sidebar/organization-selector.tsx | 3 +- .../layout/sidebar/sidebar-user-menu.tsx | 125 ++++++++++++++++++ 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 app/components/layout/sidebar/sidebar-user-menu.tsx diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index 07f7920f1..73aed0613 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -8,6 +8,7 @@ import { useSidebar, } from "./sidebar"; import OrganizationSelector from "./organization-selector"; +import SidebarUserMenu from "./sidebar-user-menu"; type AppSidebarProps = React.ComponentProps; @@ -26,7 +27,9 @@ export default function AppSidebar(props: AppSidebarProps) { This is content of sidebar - this is footer + + + ); diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 33c122343..e300fd923 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -24,8 +24,7 @@ import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; export default function OrganizationSelector() { - const isMobile = useIsMobile(); - const { open } = useSidebar(); + const { open, isMobile } = useSidebar(); const { organizations, currentOrganizationId } = useLoaderData(); diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx new file mode 100644 index 000000000..9efb2ffa3 --- /dev/null +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -0,0 +1,125 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/shared/dropdown"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./sidebar"; +import { useLoaderData } from "@remix-run/react"; +import type { loader } from "~/routes/_layout+/_layout"; +import ProfilePicture from "~/components/user/profile-picture"; +import { ChevronRight, QuestionsIcon } from "~/components/icons/library"; +import { Button } from "~/components/shared/button"; +import { CrispButton } from "~/components/marketing/crisp"; +import { Form } from "~/components/custom-form"; + +export default function SidebarUserMenu() { + const { user } = useLoaderData(); + const { isMobile } = useSidebar(); + + return ( + + + + + + +
+ {user.username} + {user.email} +
+ +
+
+ + + +
+ +
+ + {user.username} + + {user.email} +
+
+
+ + + + + e.preventDefault()} + className="border-b border-gray-200 p-0" + > + + + + + + + Questions/Feedback + + + + + e.preventDefault()} + > +
+ +
+
+
+
+
+
+ ); +} From 5cff467277912a1ccc5b85c67f8418ccd3ae107d Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 13:51:28 +0100 Subject: [PATCH 05/55] refactor(sidebar): create basic sidebar nav menu --- app/components/layout/sidebar/app-sidebar.tsx | 5 +- app/components/layout/sidebar/menu-items.tsx | 21 +--- app/components/layout/sidebar/sidebar-nav.tsx | 95 +++++++++++++++++++ app/components/shared/collapsible.tsx | 9 ++ package-lock.json | 31 ++++++ package.json | 1 + 6 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 app/components/layout/sidebar/sidebar-nav.tsx create mode 100644 app/components/shared/collapsible.tsx diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index 73aed0613..89b99a3f3 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -9,6 +9,7 @@ import { } from "./sidebar"; import OrganizationSelector from "./organization-selector"; import SidebarUserMenu from "./sidebar-user-menu"; +import SidebarNav from "./sidebar-nav"; type AppSidebarProps = React.ComponentProps; @@ -25,7 +26,9 @@ export default function AppSidebar(props: AppSidebarProps) { - This is content of sidebar + + + diff --git a/app/components/layout/sidebar/menu-items.tsx b/app/components/layout/sidebar/menu-items.tsx index 22bea70aa..157941848 100644 --- a/app/components/layout/sidebar/menu-items.tsx +++ b/app/components/layout/sidebar/menu-items.tsx @@ -115,26 +115,7 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents }) => { ) : (
  • - - tw( - "my-1 flex items-center gap-3 rounded px-3 py-2.5 text-[16px] font-semibold text-gray-700 transition-all duration-75 hover:bg-primary-50 hover:text-primary-600", - isActive ? "active bg-primary-50 text-primary-600" : "", - workspaceSwitching ? "pointer-events-none" : "" - ) - } - to={item.to} - data-test-id={`${item.title.toLowerCase()}SidebarMenuItem`} - onClick={toggleMobileNav} - title={item.title} - > - - {item.icon} - - - {item.title} - - +
  • ) )} diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx new file mode 100644 index 000000000..3df05e32c --- /dev/null +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -0,0 +1,95 @@ +import { ChevronRight, HomeIcon } from "~/components/icons/library"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "./sidebar"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/shared/collapsible"; +import { cloneElement } from "react"; +import { NavLink } from "@remix-run/react"; +import { tw } from "~/utils/tw"; + +type NavItem = { + title: string; + url: string; + icon: React.ReactElement; + items: Array<{ title: string; url: string }>; +}; + +const NAV_ITEMS: NavItem[] = [ + { + title: "Nav items", + url: "/", + icon: , + items: [ + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + ], + }, +]; + +export default function SidebarNav() { + return ( + + Platform + + {NAV_ITEMS.map((item) => ( + + + + + {item.icon && cloneElement(item.icon)} + {item.title} + + + + + + {item.items.map((subItem) => ( + + + + + {subItem.title} + + + + + ))} + + + + + ))} + + + ); +} diff --git a/app/components/shared/collapsible.tsx b/app/components/shared/collapsible.tsx new file mode 100644 index 000000000..5c28cbcc3 --- /dev/null +++ b/app/components/shared/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/package-lock.json b/package-lock.json index b1b471d5f..1cd74ec25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.9.1", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", @@ -3326,6 +3327,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", + "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", diff --git a/package.json b/package.json index 55f9bd8ab..f6704277f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.9.1", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", From b9927071aaa165982670da606caa31763e4519e6 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 19:02:33 +0100 Subject: [PATCH 06/55] refactor(sidebar): create main and bottom navigation for sidebar --- app/components/icons/icon.tsx | 5 +- app/components/icons/iconHug.tsx | 4 +- app/components/icons/library.tsx | 16 ++- app/components/layout/sidebar/app-sidebar.tsx | 5 +- app/components/layout/sidebar/sidebar-nav.tsx | 124 ++++++++---------- app/hooks/use-sidebar-nav-items.tsx | 121 +++++++++++++++++ 6 files changed, 196 insertions(+), 79 deletions(-) create mode 100644 app/hooks/use-sidebar-nav-items.tsx diff --git a/app/components/icons/icon.tsx b/app/components/icons/icon.tsx index 5922f7a70..32f309438 100644 --- a/app/components/icons/icon.tsx +++ b/app/components/icons/icon.tsx @@ -6,9 +6,10 @@ import iconsMap from "../shared/icons-map"; export interface IconProps { icon: IconType; disableWrap?: true; + size?: React.ComponentProps["size"]; } const Icon = React.forwardRef(function Icon( - { icon, disableWrap }: IconProps, + { icon, disableWrap, size = "sm" }: IconProps, _ref ) { return ( @@ -16,7 +17,7 @@ const Icon = React.forwardRef(function Icon( (disableWrap ? (
    {iconsMap[icon]}
    ) : ( - {iconsMap[icon]} + {iconsMap[icon]} )) ); }); diff --git a/app/components/icons/iconHug.tsx b/app/components/icons/iconHug.tsx index b4484ac14..0602b4f61 100644 --- a/app/components/icons/iconHug.tsx +++ b/app/components/icons/iconHug.tsx @@ -2,7 +2,7 @@ import { tw } from "~/utils/tw"; interface Props { /** Size of the hug. Defualt is sm */ - size: "sm" | "md" | "lg" | "xl" | "2xl"; + size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; children: JSX.Element | JSX.Element[]; @@ -16,6 +16,8 @@ interface Props { const sizeClasses: { [key in Props["size"]]: string; } = { + /** 16 */ + xs: "w-4 h-4", /** 32px */ sm: "w-5 h-5", /** 40px */ diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index 57ff9901e..a240a44df 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -436,8 +436,9 @@ export function XIcon(props: SVGProps) { export const AssetsIcon = (props: SVGProps) => ( ) => ( export const TagsIcon = (props: SVGProps) => ( @@ -923,9 +925,9 @@ export const RemoveTagsIcon = (props: SVGProps) => ( export const LocationMarkerIcon = (props: SVGProps) => ( ; export default function AppSidebar(props: AppSidebarProps) { const { state } = useSidebar(); + const { topMenuItems, bottomMenuItems } = useSidebarNavItems(); return ( @@ -27,10 +29,11 @@ export default function AppSidebar(props: AppSidebarProps) { - + + diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 3df05e32c..80c61e704 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -1,7 +1,6 @@ import { ChevronRight, HomeIcon } from "~/components/icons/library"; import { SidebarGroup, - SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, @@ -14,81 +13,70 @@ import { CollapsibleContent, CollapsibleTrigger, } from "~/components/shared/collapsible"; -import { cloneElement } from "react"; import { NavLink } from "@remix-run/react"; -import { tw } from "~/utils/tw"; +import { NavItem, useSidebarNavItems } from "~/hooks/use-sidebar-nav-items"; +import Icon from "~/components/icons/icon"; -type NavItem = { - title: string; - url: string; - icon: React.ReactElement; - items: Array<{ title: string; url: string }>; +type SidebarNavProps = { + className?: string; + style?: React.CSSProperties; + items: NavItem[]; }; -const NAV_ITEMS: NavItem[] = [ - { - title: "Nav items", - url: "/", - icon: , - items: [ - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - ], - }, -]; - -export default function SidebarNav() { +export default function SidebarNav({ + className, + style, + items, +}: SidebarNavProps) { return ( - - Platform + - {NAV_ITEMS.map((item) => ( - - - - - {item.icon && cloneElement(item.icon)} + {items.map((item) => { + if (item.type === "parent") { + return ( + + + + + + {item.title} + + + + + + {item.children.map((child) => ( + + + + + {child.title} + + + + ))} + + + + + ); + } + + return ( + + + + {item.title} - - - - - - {item.items.map((subItem) => ( - - - - - {subItem.title} - - - - - ))} - - + + - - ))} + ); + })} ); diff --git a/app/hooks/use-sidebar-nav-items.tsx b/app/hooks/use-sidebar-nav-items.tsx new file mode 100644 index 000000000..e6200bd9f --- /dev/null +++ b/app/hooks/use-sidebar-nav-items.tsx @@ -0,0 +1,121 @@ +import { useUserData } from "./use-user-data"; +import { useUserRoleHelper } from "./user-user-role-helper"; +import { IconType } from "~/components/shared/icons-map"; + +type BaseNavItem = { + title: string; + icon: IconType; + hidden?: boolean; +}; + +type ChildNavItem = BaseNavItem & { + type: "child"; + to: string; + target?: string; +}; + +type ParentNavItem = BaseNavItem & { + type: "parent"; + children: Omit[]; +}; + +export type NavItem = ChildNavItem | ParentNavItem; + +export function useSidebarNavItems() { + const user = useUserData(); + const { isBaseOrSelfService } = useUserRoleHelper(); + + const topMenuItems: NavItem[] = [ + { + type: "child", + title: "Dashboard", + to: "/dashboard", + icon: "graph", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Assets", + to: "/assets", + icon: "asset", + }, + { + type: "child", + title: "Kits", + to: "/kits", + icon: "kit", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Categories", + to: "/categories", + icon: "category", + hidden: isBaseOrSelfService, + }, + + { + type: "child", + title: "Tags", + to: "/tags", + icon: "tag", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Locations", + to: "/locations", + icon: "location", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Calendar", + to: "/calendar", + icon: "calendar", + }, + { + type: "child", + title: "Bookings", + to: "/bookings", + icon: "bookings", + }, + { + type: "child", + title: "Team", + to: "/settings/team", + icon: "user", + hidden: isBaseOrSelfService, + }, + ]; + + const bottomMenuItems: NavItem[] = [ + { + type: "child", + title: "Asset labels", + to: `https://www.shelf.nu/order-tags?email=${user?.email}${ + user?.firstName ? `&firstName=${user.firstName}` : "" + }${user?.lastName ? `&lastName=${user.lastName}` : ""}`, + icon: "asset-label", + target: "_blank", + }, + { + type: "child", + title: "QR Scanner", + to: "/scanner", + icon: "scanQR", + }, + { + type: "child", + title: "Workspace settings", + to: "/settings", + icon: "settings", + hidden: isBaseOrSelfService, + }, + ]; + + return { + topMenuItems: topMenuItems.filter((item) => !item.hidden), + bottomMenuItems: bottomMenuItems.filter((item) => !item.hidden), + }; +} From 882e5ae05139bad27983f159b29d462c6366134c Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 19:11:42 +0100 Subject: [PATCH 07/55] refactor(sidebar): fix icons sized for collapsed sidebar issue --- app/components/icons/library.tsx | 20 +++++++++---------- app/components/layout/sidebar/app-sidebar.tsx | 2 +- app/components/layout/sidebar/menu-items.tsx | 4 +--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index a240a44df..f73604f75 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -84,8 +84,8 @@ export function ItemsIcon(props: SVGProps) { export function SettingsIcon(props: SVGProps) { return ( ) => ( export const BookingsIcon = (props: SVGProps) => ( @@ -1493,8 +1493,8 @@ export const CustomFiedIcon = (props: SVGProps) => ( export const KitIcon = (props: SVGProps) => ( ) => ( - + diff --git a/app/components/layout/sidebar/menu-items.tsx b/app/components/layout/sidebar/menu-items.tsx index 157941848..1fabe04b3 100644 --- a/app/components/layout/sidebar/menu-items.tsx +++ b/app/components/layout/sidebar/menu-items.tsx @@ -114,9 +114,7 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents }) => { ) : ( -
  • - -
  • +
  • ) )} From 692bf95fdb33e2d5454c06b500e2a71e7ede0390 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 21:19:22 +0100 Subject: [PATCH 08/55] refactor(sidebar): close dropdown on click --- .../layout/sidebar/organization-selector.tsx | 10 +++++++++- app/components/layout/sidebar/sidebar-user-menu.tsx | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index e300fd923..ba25f1e7f 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -22,8 +22,11 @@ import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; +import { useState } from "react"; export default function OrganizationSelector() { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const { open, isMobile } = useSidebar(); const { organizations, currentOrganizationId } = @@ -50,10 +53,14 @@ export default function OrganizationSelector() { }); } + function closeDropdown() { + setIsDropdownOpen(false); + } + return ( - + Manage workspaces diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx index 9efb2ffa3..2a73fd1a1 100644 --- a/app/components/layout/sidebar/sidebar-user-menu.tsx +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -19,15 +19,21 @@ import { ChevronRight, QuestionsIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { CrispButton } from "~/components/marketing/crisp"; import { Form } from "~/components/custom-form"; +import { useState } from "react"; export default function SidebarUserMenu() { const { user } = useLoaderData(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { isMobile } = useSidebar(); + function closeDropdown() { + setIsDropdownOpen(false); + } + return ( - + Account Details From 44a6f8c3f262557a26accd19e90d26a14808546a Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 22:17:30 +0100 Subject: [PATCH 09/55] refactor(sidebar): fix issue with organization switching atom --- app/components/layout/breadcrumbs/index.tsx | 20 ++++++----- .../layout/sidebar/organization-selector.tsx | 14 ++++++-- app/components/layout/sidebar/sidebar.tsx | 6 ++-- app/routes/_layout+/_layout.tsx | 36 +++++++------------ 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/app/components/layout/breadcrumbs/index.tsx b/app/components/layout/breadcrumbs/index.tsx index 6368662e5..595a1ce1e 100644 --- a/app/components/layout/breadcrumbs/index.tsx +++ b/app/components/layout/breadcrumbs/index.tsx @@ -1,5 +1,6 @@ import { useMatches } from "@remix-run/react"; import { Breadcrumb } from "./breadcrumb"; +import { SidebarTrigger } from "../sidebar/sidebar"; // Define an interface that extends RouteHandle with the 'breadcrumb' property interface HandleWithBreadcrumb { @@ -15,14 +16,17 @@ export function Breadcrumbs() { ); return ( -
    - {breadcrumbs.map((match, index) => ( - - ))} +
    + +
    + {breadcrumbs.map((match, index) => ( + + ))} +
    ); } diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index ba25f1e7f..3c295f9d8 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -13,7 +13,6 @@ import { useSidebar, } from "./sidebar"; import { ChevronDownIcon } from "@radix-ui/react-icons"; -import { useIsMobile } from "~/hooks/use-mobile"; import { useFetcher, useLoaderData } from "@remix-run/react"; import type { loader } from "~/routes/_layout+/_layout"; import ProfilePicture from "~/components/user/profile-picture"; @@ -22,7 +21,9 @@ import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSetAtom } from "jotai"; +import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; export default function OrganizationSelector() { const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -35,6 +36,8 @@ export default function OrganizationSelector() { const fetcher = useFetcher(); const isSwitchingOrg = isFormProcessing(fetcher.state); + const setWorkspaceSwitching = useSetAtom(switchingWorkspaceAtom); + const currentOrganization = organizations.find( (org) => org.id === currentOrganizationId ); @@ -57,6 +60,13 @@ export default function OrganizationSelector() { setIsDropdownOpen(false); } + useEffect( + function setSwitchingWorkspaceAfterFetch() { + setWorkspaceSwitching(isSwitchingOrg); + }, + [isFormProcessing, setWorkspaceSwitching] + ); + return ( diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index be1e71048..95786d9aa 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -11,7 +11,7 @@ import { useState, } from "react"; import { useIsMobile } from "~/hooks/use-mobile"; -import { ArrowLeftIcon, SwitchIcon } from "~/components/icons/library"; +import { SwitchIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { tw } from "~/utils/tw"; import Input from "~/components/forms/input"; @@ -277,14 +277,14 @@ const SidebarTrigger = forwardRef< data-sidebar="trigger" variant="secondary" size="sm" - className={tw("h-7 w-7 border-none", className)} + className={tw("border-none p-0", className)} onClick={(event: any) => { onClick?.(event); toggleSidebar(); }} {...props} > - + Toggle Sidebar ); diff --git a/app/routes/_layout+/_layout.tsx b/app/routes/_layout+/_layout.tsx index 5e38a99c1..f25e9c5d6 100644 --- a/app/routes/_layout+/_layout.tsx +++ b/app/routes/_layout+/_layout.tsx @@ -12,11 +12,8 @@ import AppSidebar from "~/components/layout/sidebar/app-sidebar"; import { SidebarInset, SidebarProvider, - SidebarTrigger, } from "~/components/layout/sidebar/sidebar"; -import Sidebar from "~/components/layout/sidebar/sidebar-old"; import { useCrisp } from "~/components/marketing/crisp"; -import { Separator } from "~/components/shared/separator"; import { Spinner } from "~/components/shared/spinner"; import { Toaster } from "~/components/shared/toast"; import { NoSubscription } from "~/components/subscription/no-subscription"; @@ -152,31 +149,22 @@ export default function App() { -
    -
    - - - Breadcrumb can go here -
    -
    -
    -
    - {disabledTeamOrg ? ( - - ) : workspaceSwitching ? ( -
    - -

    Activating workspace...

    -
    - ) : ( - - )} -
    +
    + {disabledTeamOrg ? ( + + ) : workspaceSwitching ? ( +
    + +

    Activating workspace...

    +
    + ) : ( + + )} {renderInstallPwaPromptOnMobile} -
    +
    ); From d25edff47835e7b4fc732f2c05fd5cae5514ca6f Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 22:52:39 +0100 Subject: [PATCH 10/55] refactor(sidebar): highlight active route in sidebar menu --- app/components/layout/sidebar/sidebar-nav.tsx | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 80c61e704..1fa7efbfe 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -13,9 +13,10 @@ import { CollapsibleContent, CollapsibleTrigger, } from "~/components/shared/collapsible"; -import { NavLink } from "@remix-run/react"; +import { matchRoutes, NavLink, useMatch, useMatches } from "@remix-run/react"; import { NavItem, useSidebarNavItems } from "~/hooks/use-sidebar-nav-items"; import Icon from "~/components/icons/icon"; +import { tw } from "~/utils/tw"; type SidebarNavProps = { className?: string; @@ -28,6 +29,13 @@ export default function SidebarNav({ style, items, }: SidebarNavProps) { + const matches = useMatches(); + + function isRouteActive(route: string) { + const matchesRoutes = matches.map((match) => match.pathname); + return matchesRoutes.some((matchRoute) => matchRoute.includes(route)); + } + return ( @@ -49,16 +57,27 @@ export default function SidebarNav({ - {item.children.map((child) => ( - - - - - {child.title} - - - - ))} + {item.children.map((child) => { + const isChildActive = isRouteActive(child.to); + + return ( + + + + + {child.title} + + + + ); + })}
    @@ -66,10 +85,16 @@ export default function SidebarNav({ ); } + const isActive = isRouteActive(item.to); + return ( - + {item.title} From ae421ac93180c23b6fab666c791ec6cc582310f3 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 22 Nov 2024 15:32:58 +0100 Subject: [PATCH 11/55] refactor(sidebar): fix pr issues --- app/components/icons/icon.tsx | 7 +++++-- app/components/icons/library.tsx | 4 ++-- app/components/layout/breadcrumbs/index.tsx | 20 ++++++++----------- .../layout/sidebar/organization-selector.tsx | 6 +++--- app/components/layout/sidebar/sidebar-nav.tsx | 12 +++++++++-- .../layout/sidebar/sidebar-user-menu.tsx | 4 ++-- app/components/layout/sidebar/sidebar.tsx | 2 +- app/root.tsx | 7 ++----- app/tailwind.css | 13 ++++++++++++ tailwind.config.ts | 10 ++++++++++ 10 files changed, 56 insertions(+), 29 deletions(-) diff --git a/app/components/icons/icon.tsx b/app/components/icons/icon.tsx index 32f309438..5ace388f6 100644 --- a/app/components/icons/icon.tsx +++ b/app/components/icons/icon.tsx @@ -4,12 +4,13 @@ import type { IconType } from "../shared/icons-map"; import iconsMap from "../shared/icons-map"; export interface IconProps { + className?: string; icon: IconType; disableWrap?: true; size?: React.ComponentProps["size"]; } const Icon = React.forwardRef(function Icon( - { icon, disableWrap, size = "sm" }: IconProps, + { className, icon, disableWrap, size = "sm" }: IconProps, _ref ) { return ( @@ -17,7 +18,9 @@ const Icon = React.forwardRef(function Icon( (disableWrap ? (
    {iconsMap[icon]}
    ) : ( - {iconsMap[icon]} + + {iconsMap[icon]} + )) ); }); diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index f73604f75..c75727890 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -1574,9 +1574,9 @@ export const AssetLabel = (props: SVGProps) => ( - -
    - {breadcrumbs.map((match, index) => ( - - ))} -
    +
    + {breadcrumbs.map((match, index) => ( + + ))}
    ); } diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 3c295f9d8..569222113 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -74,7 +74,7 @@ export default function OrganizationSelector() { {currentOrganization.type === "PERSONAL" ? ( @@ -101,7 +101,7 @@ export default function OrganizationSelector() { ( { if (organization.id !== currentOrganizationId) { handleSwitchOrganization(organization.id); diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 1fa7efbfe..34da6f0ee 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -67,11 +67,16 @@ export default function SidebarNav({ to={child.to} target={child.target} className={tw( + "font-medium", isChildActive && "text-primary-500 bg-primary-25" )} > - + {child.title}
    @@ -93,7 +98,10 @@ export default function SidebarNav({ {item.title} diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx index 2a73fd1a1..8e7753e88 100644 --- a/app/components/layout/sidebar/sidebar-user-menu.tsx +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -37,7 +37,7 @@ export default function SidebarUserMenu() { - + Toggle Sidebar ); diff --git a/app/root.tsx b/app/root.tsx index 3afd92311..b170218ae 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -34,6 +34,7 @@ import { data } from "./utils/http.server"; import { useNonce } from "./utils/nonce-provider"; import { PwaManagerProvider } from "./utils/pwa-manager"; import { splashScreenLinks } from "./utils/splash-screen-links"; +import { SidebarTrigger } from "./components/layout/sidebar/sidebar"; export interface RootData { env: typeof getBrowserEnv; @@ -41,11 +42,7 @@ export interface RootData { } export const handle = { - breadcrumb: () => ( - - - - ), + breadcrumb: () => , }; export const links: LinksFunction = () => [ diff --git a/app/tailwind.css b/app/tailwind.css index b5c61c956..ab6af0c90 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -1,3 +1,16 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer base { + :root { + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 37afe7158..5bf0309e6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -221,6 +221,16 @@ export default { inverted: "#000000", // black }, }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, }, boxShadow: { From e5eb1c7a515c3683f73fec629c499af9a3b6e081 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Sat, 23 Nov 2024 12:26:40 +0100 Subject: [PATCH 12/55] refactor(sidebar): fix color issues and implement sidebar with cookie and make it optimistic --- app/components/layout/sidebar/sidebar.tsx | 57 ++++++++++++++++------- app/routes/_layout+/_layout.tsx | 4 +- app/tailwind.css | 1 - tailwind.config.ts | 1 - 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index e6642cf4c..fc29f7234 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -24,9 +24,9 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/shared/tooltip"; +import { useFetcher } from "@remix-run/react"; +import { isFormProcessing } from "~/utils/form"; -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; @@ -40,6 +40,7 @@ type SidebarContext = { setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; + isSidebarToggling: boolean; }; const SidebarContext = createContext(null); @@ -76,6 +77,9 @@ const SidebarProvider = forwardRef< const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = useState(false); + const sidebarTogglerFetcher = useFetcher(); + const isSidebarToggling = isFormProcessing(sidebarTogglerFetcher.state); + // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = useState(defaultOpen); @@ -89,8 +93,14 @@ const SidebarProvider = forwardRef< _setOpen(openState); } - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + /* Setting state of sidebar in cookies (userPrefs) */ + const formData = new FormData(); + formData.append("minimizeSidebar", openState ? "close" : "open"); + + sidebarTogglerFetcher.submit(formData, { + method: "POST", + action: "/api/user/prefs/minimized-sidebar", + }); }, [setOpenProp, open] ); @@ -131,8 +141,18 @@ const SidebarProvider = forwardRef< openMobile, setOpenMobile, toggleSidebar, + isSidebarToggling, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + isSidebarToggling, + ] ); return ( @@ -269,15 +289,19 @@ const SidebarTrigger = forwardRef< React.ElementRef, React.ComponentProps >(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); + const { toggleSidebar, isSidebarToggling } = useSidebar(); return ( ); -} +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + +
    +
    +
    + ); +} From 5a115d86c0561b8f98c78cdb952c731ff5b310f1 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 12:54:20 +0100 Subject: [PATCH 15/55] refactor(sidebar): fix organization-selector on mobile --- app/components/layout/sidebar/app-sidebar.tsx | 3 ++ .../layout/sidebar/organization-selector.tsx | 31 +++++++++++++------ app/components/layout/sidebar/sidebar.tsx | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index ea82de16e..07f7920f1 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -7,6 +7,7 @@ import { SidebarRail, useSidebar, } from "./sidebar"; +import OrganizationSelector from "./organization-selector"; type AppSidebarProps = React.ComponentProps; @@ -19,6 +20,8 @@ export default function AppSidebar(props: AppSidebarProps) {
    + + This is content of sidebar diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 75bfa2e7b..33c122343 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -6,7 +6,12 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from "~/components/shared/dropdown"; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "./sidebar"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./sidebar"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { useIsMobile } from "~/hooks/use-mobile"; import { useFetcher, useLoaderData } from "@remix-run/react"; @@ -16,9 +21,12 @@ import { Image } from "~/components/shared/image"; import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; +import When from "~/components/when/when"; export default function OrganizationSelector() { const isMobile = useIsMobile(); + const { open } = useSidebar(); + const { organizations, currentOrganizationId } = useLoaderData(); @@ -50,7 +58,7 @@ export default function OrganizationSelector() { {currentOrganization.type === "PERSONAL" ? ( @@ -58,16 +66,21 @@ export default function OrganizationSelector() { img )} -
    - - {currentOrganization.name} - -
    - + + + <> +
    + + {currentOrganization.name} + +
    + + +
    diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index 11c6f2b06..be1e71048 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -511,7 +511,7 @@ const SidebarMenuItem = forwardRef>( SidebarMenuItem.displayName = "SidebarMenuItem"; const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: { From 5bac31f2cbdbfcde9a4e134c3cd10e3334684966 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 13:24:53 +0100 Subject: [PATCH 16/55] refactor(sidebar): create user menu for sidebar --- app/components/layout/sidebar/app-sidebar.tsx | 5 +- .../layout/sidebar/organization-selector.tsx | 3 +- .../layout/sidebar/sidebar-user-menu.tsx | 125 ++++++++++++++++++ 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 app/components/layout/sidebar/sidebar-user-menu.tsx diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index 07f7920f1..73aed0613 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -8,6 +8,7 @@ import { useSidebar, } from "./sidebar"; import OrganizationSelector from "./organization-selector"; +import SidebarUserMenu from "./sidebar-user-menu"; type AppSidebarProps = React.ComponentProps; @@ -26,7 +27,9 @@ export default function AppSidebar(props: AppSidebarProps) { This is content of sidebar - this is footer + + + ); diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 33c122343..e300fd923 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -24,8 +24,7 @@ import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; export default function OrganizationSelector() { - const isMobile = useIsMobile(); - const { open } = useSidebar(); + const { open, isMobile } = useSidebar(); const { organizations, currentOrganizationId } = useLoaderData(); diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx new file mode 100644 index 000000000..9efb2ffa3 --- /dev/null +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -0,0 +1,125 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/shared/dropdown"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./sidebar"; +import { useLoaderData } from "@remix-run/react"; +import type { loader } from "~/routes/_layout+/_layout"; +import ProfilePicture from "~/components/user/profile-picture"; +import { ChevronRight, QuestionsIcon } from "~/components/icons/library"; +import { Button } from "~/components/shared/button"; +import { CrispButton } from "~/components/marketing/crisp"; +import { Form } from "~/components/custom-form"; + +export default function SidebarUserMenu() { + const { user } = useLoaderData(); + const { isMobile } = useSidebar(); + + return ( + + + + + + +
    + {user.username} + {user.email} +
    + +
    +
    + + + +
    + +
    + + {user.username} + + {user.email} +
    +
    +
    + + + + + e.preventDefault()} + className="border-b border-gray-200 p-0" + > + + + + + + + Questions/Feedback + + + + + e.preventDefault()} + > +
    + +
    +
    +
    +
    +
    +
    + ); +} From 86ac496d12c4688896baac11da7d72143368e125 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 13:51:28 +0100 Subject: [PATCH 17/55] refactor(sidebar): create basic sidebar nav menu --- app/components/layout/sidebar/app-sidebar.tsx | 5 +- app/components/layout/sidebar/menu-items.tsx | 21 +--- app/components/layout/sidebar/sidebar-nav.tsx | 95 +++++++++++++++++++ app/components/shared/collapsible.tsx | 9 ++ package-lock.json | 31 ++++++ package.json | 1 + 6 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 app/components/layout/sidebar/sidebar-nav.tsx create mode 100644 app/components/shared/collapsible.tsx diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index 73aed0613..89b99a3f3 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -9,6 +9,7 @@ import { } from "./sidebar"; import OrganizationSelector from "./organization-selector"; import SidebarUserMenu from "./sidebar-user-menu"; +import SidebarNav from "./sidebar-nav"; type AppSidebarProps = React.ComponentProps; @@ -25,7 +26,9 @@ export default function AppSidebar(props: AppSidebarProps) { - This is content of sidebar + + + diff --git a/app/components/layout/sidebar/menu-items.tsx b/app/components/layout/sidebar/menu-items.tsx index 22bea70aa..157941848 100644 --- a/app/components/layout/sidebar/menu-items.tsx +++ b/app/components/layout/sidebar/menu-items.tsx @@ -115,26 +115,7 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents }) => { ) : (
  • - - tw( - "my-1 flex items-center gap-3 rounded px-3 py-2.5 text-[16px] font-semibold text-gray-700 transition-all duration-75 hover:bg-primary-50 hover:text-primary-600", - isActive ? "active bg-primary-50 text-primary-600" : "", - workspaceSwitching ? "pointer-events-none" : "" - ) - } - to={item.to} - data-test-id={`${item.title.toLowerCase()}SidebarMenuItem`} - onClick={toggleMobileNav} - title={item.title} - > - - {item.icon} - - - {item.title} - - +
  • ) )} diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx new file mode 100644 index 000000000..3df05e32c --- /dev/null +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -0,0 +1,95 @@ +import { ChevronRight, HomeIcon } from "~/components/icons/library"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "./sidebar"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/shared/collapsible"; +import { cloneElement } from "react"; +import { NavLink } from "@remix-run/react"; +import { tw } from "~/utils/tw"; + +type NavItem = { + title: string; + url: string; + icon: React.ReactElement; + items: Array<{ title: string; url: string }>; +}; + +const NAV_ITEMS: NavItem[] = [ + { + title: "Nav items", + url: "/", + icon: , + items: [ + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + { + title: "Home", + url: "/", + }, + ], + }, +]; + +export default function SidebarNav() { + return ( + + Platform + + {NAV_ITEMS.map((item) => ( + + + + + {item.icon && cloneElement(item.icon)} + {item.title} + + + + + + {item.items.map((subItem) => ( + + + + + {subItem.title} + + + + + ))} + + + + + ))} + + + ); +} diff --git a/app/components/shared/collapsible.tsx b/app/components/shared/collapsible.tsx new file mode 100644 index 000000000..5c28cbcc3 --- /dev/null +++ b/app/components/shared/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/package-lock.json b/package-lock.json index 1b5c7b95e..6f35c9c78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.9.1", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", @@ -3326,6 +3327,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", + "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", diff --git a/package.json b/package.json index 104340400..3a79fd7aa 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.9.1", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", From 396ade067cb56739333a4e3c715605ee98835e6a Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 19:02:33 +0100 Subject: [PATCH 18/55] refactor(sidebar): create main and bottom navigation for sidebar --- app/components/icons/icon.tsx | 5 +- app/components/icons/iconHug.tsx | 4 +- app/components/icons/library.tsx | 16 ++- app/components/layout/sidebar/app-sidebar.tsx | 5 +- app/components/layout/sidebar/sidebar-nav.tsx | 124 ++++++++---------- app/hooks/use-sidebar-nav-items.tsx | 121 +++++++++++++++++ 6 files changed, 196 insertions(+), 79 deletions(-) create mode 100644 app/hooks/use-sidebar-nav-items.tsx diff --git a/app/components/icons/icon.tsx b/app/components/icons/icon.tsx index 5922f7a70..32f309438 100644 --- a/app/components/icons/icon.tsx +++ b/app/components/icons/icon.tsx @@ -6,9 +6,10 @@ import iconsMap from "../shared/icons-map"; export interface IconProps { icon: IconType; disableWrap?: true; + size?: React.ComponentProps["size"]; } const Icon = React.forwardRef(function Icon( - { icon, disableWrap }: IconProps, + { icon, disableWrap, size = "sm" }: IconProps, _ref ) { return ( @@ -16,7 +17,7 @@ const Icon = React.forwardRef(function Icon( (disableWrap ? (
    {iconsMap[icon]}
    ) : ( - {iconsMap[icon]} + {iconsMap[icon]} )) ); }); diff --git a/app/components/icons/iconHug.tsx b/app/components/icons/iconHug.tsx index b4484ac14..0602b4f61 100644 --- a/app/components/icons/iconHug.tsx +++ b/app/components/icons/iconHug.tsx @@ -2,7 +2,7 @@ import { tw } from "~/utils/tw"; interface Props { /** Size of the hug. Defualt is sm */ - size: "sm" | "md" | "lg" | "xl" | "2xl"; + size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; children: JSX.Element | JSX.Element[]; @@ -16,6 +16,8 @@ interface Props { const sizeClasses: { [key in Props["size"]]: string; } = { + /** 16 */ + xs: "w-4 h-4", /** 32px */ sm: "w-5 h-5", /** 40px */ diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index 57ff9901e..a240a44df 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -436,8 +436,9 @@ export function XIcon(props: SVGProps) { export const AssetsIcon = (props: SVGProps) => ( ) => ( export const TagsIcon = (props: SVGProps) => ( @@ -923,9 +925,9 @@ export const RemoveTagsIcon = (props: SVGProps) => ( export const LocationMarkerIcon = (props: SVGProps) => ( ; export default function AppSidebar(props: AppSidebarProps) { const { state } = useSidebar(); + const { topMenuItems, bottomMenuItems } = useSidebarNavItems(); return ( @@ -27,10 +29,11 @@ export default function AppSidebar(props: AppSidebarProps) { - + + diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 3df05e32c..80c61e704 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -1,7 +1,6 @@ import { ChevronRight, HomeIcon } from "~/components/icons/library"; import { SidebarGroup, - SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, @@ -14,81 +13,70 @@ import { CollapsibleContent, CollapsibleTrigger, } from "~/components/shared/collapsible"; -import { cloneElement } from "react"; import { NavLink } from "@remix-run/react"; -import { tw } from "~/utils/tw"; +import { NavItem, useSidebarNavItems } from "~/hooks/use-sidebar-nav-items"; +import Icon from "~/components/icons/icon"; -type NavItem = { - title: string; - url: string; - icon: React.ReactElement; - items: Array<{ title: string; url: string }>; +type SidebarNavProps = { + className?: string; + style?: React.CSSProperties; + items: NavItem[]; }; -const NAV_ITEMS: NavItem[] = [ - { - title: "Nav items", - url: "/", - icon: , - items: [ - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - { - title: "Home", - url: "/", - }, - ], - }, -]; - -export default function SidebarNav() { +export default function SidebarNav({ + className, + style, + items, +}: SidebarNavProps) { return ( - - Platform + - {NAV_ITEMS.map((item) => ( - - - - - {item.icon && cloneElement(item.icon)} + {items.map((item) => { + if (item.type === "parent") { + return ( + + + + + + {item.title} + + + + + + {item.children.map((child) => ( + + + + + {child.title} + + + + ))} + + + + + ); + } + + return ( + + + + {item.title} - - - - - - {item.items.map((subItem) => ( - - - - - {subItem.title} - - - - - ))} - - + + - - ))} + ); + })} ); diff --git a/app/hooks/use-sidebar-nav-items.tsx b/app/hooks/use-sidebar-nav-items.tsx new file mode 100644 index 000000000..e6200bd9f --- /dev/null +++ b/app/hooks/use-sidebar-nav-items.tsx @@ -0,0 +1,121 @@ +import { useUserData } from "./use-user-data"; +import { useUserRoleHelper } from "./user-user-role-helper"; +import { IconType } from "~/components/shared/icons-map"; + +type BaseNavItem = { + title: string; + icon: IconType; + hidden?: boolean; +}; + +type ChildNavItem = BaseNavItem & { + type: "child"; + to: string; + target?: string; +}; + +type ParentNavItem = BaseNavItem & { + type: "parent"; + children: Omit[]; +}; + +export type NavItem = ChildNavItem | ParentNavItem; + +export function useSidebarNavItems() { + const user = useUserData(); + const { isBaseOrSelfService } = useUserRoleHelper(); + + const topMenuItems: NavItem[] = [ + { + type: "child", + title: "Dashboard", + to: "/dashboard", + icon: "graph", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Assets", + to: "/assets", + icon: "asset", + }, + { + type: "child", + title: "Kits", + to: "/kits", + icon: "kit", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Categories", + to: "/categories", + icon: "category", + hidden: isBaseOrSelfService, + }, + + { + type: "child", + title: "Tags", + to: "/tags", + icon: "tag", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Locations", + to: "/locations", + icon: "location", + hidden: isBaseOrSelfService, + }, + { + type: "child", + title: "Calendar", + to: "/calendar", + icon: "calendar", + }, + { + type: "child", + title: "Bookings", + to: "/bookings", + icon: "bookings", + }, + { + type: "child", + title: "Team", + to: "/settings/team", + icon: "user", + hidden: isBaseOrSelfService, + }, + ]; + + const bottomMenuItems: NavItem[] = [ + { + type: "child", + title: "Asset labels", + to: `https://www.shelf.nu/order-tags?email=${user?.email}${ + user?.firstName ? `&firstName=${user.firstName}` : "" + }${user?.lastName ? `&lastName=${user.lastName}` : ""}`, + icon: "asset-label", + target: "_blank", + }, + { + type: "child", + title: "QR Scanner", + to: "/scanner", + icon: "scanQR", + }, + { + type: "child", + title: "Workspace settings", + to: "/settings", + icon: "settings", + hidden: isBaseOrSelfService, + }, + ]; + + return { + topMenuItems: topMenuItems.filter((item) => !item.hidden), + bottomMenuItems: bottomMenuItems.filter((item) => !item.hidden), + }; +} From 95c4d87ce187e6cd8930e6033406460802aad968 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 19:11:42 +0100 Subject: [PATCH 19/55] refactor(sidebar): fix icons sized for collapsed sidebar issue --- app/components/icons/library.tsx | 20 +++++++++---------- app/components/layout/sidebar/app-sidebar.tsx | 2 +- app/components/layout/sidebar/menu-items.tsx | 4 +--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index a240a44df..f73604f75 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -84,8 +84,8 @@ export function ItemsIcon(props: SVGProps) { export function SettingsIcon(props: SVGProps) { return ( ) => ( export const BookingsIcon = (props: SVGProps) => ( @@ -1493,8 +1493,8 @@ export const CustomFiedIcon = (props: SVGProps) => ( export const KitIcon = (props: SVGProps) => ( ) => ( - + diff --git a/app/components/layout/sidebar/menu-items.tsx b/app/components/layout/sidebar/menu-items.tsx index 157941848..1fabe04b3 100644 --- a/app/components/layout/sidebar/menu-items.tsx +++ b/app/components/layout/sidebar/menu-items.tsx @@ -114,9 +114,7 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents }) => { ) : ( -
  • - -
  • +
  • ) )} From dd64b96c9559dd7fd12632354da081f47784121c Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 21:19:22 +0100 Subject: [PATCH 20/55] refactor(sidebar): close dropdown on click --- .../layout/sidebar/organization-selector.tsx | 10 +++++++++- app/components/layout/sidebar/sidebar-user-menu.tsx | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index e300fd923..ba25f1e7f 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -22,8 +22,11 @@ import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; +import { useState } from "react"; export default function OrganizationSelector() { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const { open, isMobile } = useSidebar(); const { organizations, currentOrganizationId } = @@ -50,10 +53,14 @@ export default function OrganizationSelector() { }); } + function closeDropdown() { + setIsDropdownOpen(false); + } + return ( - + Manage workspaces diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx index 9efb2ffa3..2a73fd1a1 100644 --- a/app/components/layout/sidebar/sidebar-user-menu.tsx +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -19,15 +19,21 @@ import { ChevronRight, QuestionsIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { CrispButton } from "~/components/marketing/crisp"; import { Form } from "~/components/custom-form"; +import { useState } from "react"; export default function SidebarUserMenu() { const { user } = useLoaderData(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { isMobile } = useSidebar(); + function closeDropdown() { + setIsDropdownOpen(false); + } + return ( - + Account Details From a7f7aaae55b4447e971f3086a1cb01c1406d9623 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 22:17:30 +0100 Subject: [PATCH 21/55] refactor(sidebar): fix issue with organization switching atom --- app/components/layout/breadcrumbs/index.tsx | 20 ++++++----- .../layout/sidebar/organization-selector.tsx | 14 ++++++-- app/components/layout/sidebar/sidebar.tsx | 6 ++-- app/routes/_layout+/_layout.tsx | 36 +++++++------------ 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/app/components/layout/breadcrumbs/index.tsx b/app/components/layout/breadcrumbs/index.tsx index 6368662e5..595a1ce1e 100644 --- a/app/components/layout/breadcrumbs/index.tsx +++ b/app/components/layout/breadcrumbs/index.tsx @@ -1,5 +1,6 @@ import { useMatches } from "@remix-run/react"; import { Breadcrumb } from "./breadcrumb"; +import { SidebarTrigger } from "../sidebar/sidebar"; // Define an interface that extends RouteHandle with the 'breadcrumb' property interface HandleWithBreadcrumb { @@ -15,14 +16,17 @@ export function Breadcrumbs() { ); return ( -
    - {breadcrumbs.map((match, index) => ( - - ))} +
    + +
    + {breadcrumbs.map((match, index) => ( + + ))} +
    ); } diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index ba25f1e7f..3c295f9d8 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -13,7 +13,6 @@ import { useSidebar, } from "./sidebar"; import { ChevronDownIcon } from "@radix-ui/react-icons"; -import { useIsMobile } from "~/hooks/use-mobile"; import { useFetcher, useLoaderData } from "@remix-run/react"; import type { loader } from "~/routes/_layout+/_layout"; import ProfilePicture from "~/components/user/profile-picture"; @@ -22,7 +21,9 @@ import { Button } from "~/components/shared/button"; import invariant from "tiny-invariant"; import { isFormProcessing } from "~/utils/form"; import When from "~/components/when/when"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSetAtom } from "jotai"; +import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; export default function OrganizationSelector() { const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -35,6 +36,8 @@ export default function OrganizationSelector() { const fetcher = useFetcher(); const isSwitchingOrg = isFormProcessing(fetcher.state); + const setWorkspaceSwitching = useSetAtom(switchingWorkspaceAtom); + const currentOrganization = organizations.find( (org) => org.id === currentOrganizationId ); @@ -57,6 +60,13 @@ export default function OrganizationSelector() { setIsDropdownOpen(false); } + useEffect( + function setSwitchingWorkspaceAfterFetch() { + setWorkspaceSwitching(isSwitchingOrg); + }, + [isFormProcessing, setWorkspaceSwitching] + ); + return ( diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index be1e71048..95786d9aa 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -11,7 +11,7 @@ import { useState, } from "react"; import { useIsMobile } from "~/hooks/use-mobile"; -import { ArrowLeftIcon, SwitchIcon } from "~/components/icons/library"; +import { SwitchIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; import { tw } from "~/utils/tw"; import Input from "~/components/forms/input"; @@ -277,14 +277,14 @@ const SidebarTrigger = forwardRef< data-sidebar="trigger" variant="secondary" size="sm" - className={tw("h-7 w-7 border-none", className)} + className={tw("border-none p-0", className)} onClick={(event: any) => { onClick?.(event); toggleSidebar(); }} {...props} > - + Toggle Sidebar ); diff --git a/app/routes/_layout+/_layout.tsx b/app/routes/_layout+/_layout.tsx index 5e38a99c1..f25e9c5d6 100644 --- a/app/routes/_layout+/_layout.tsx +++ b/app/routes/_layout+/_layout.tsx @@ -12,11 +12,8 @@ import AppSidebar from "~/components/layout/sidebar/app-sidebar"; import { SidebarInset, SidebarProvider, - SidebarTrigger, } from "~/components/layout/sidebar/sidebar"; -import Sidebar from "~/components/layout/sidebar/sidebar-old"; import { useCrisp } from "~/components/marketing/crisp"; -import { Separator } from "~/components/shared/separator"; import { Spinner } from "~/components/shared/spinner"; import { Toaster } from "~/components/shared/toast"; import { NoSubscription } from "~/components/subscription/no-subscription"; @@ -152,31 +149,22 @@ export default function App() { -
    -
    - - - Breadcrumb can go here -
    -
    -
    -
    - {disabledTeamOrg ? ( - - ) : workspaceSwitching ? ( -
    - -

    Activating workspace...

    -
    - ) : ( - - )} -
    +
    + {disabledTeamOrg ? ( + + ) : workspaceSwitching ? ( +
    + +

    Activating workspace...

    +
    + ) : ( + + )} {renderInstallPwaPromptOnMobile} -
    +
    ); From 198fa2c93413e4a0067a991d132c3a1c7a3b1922 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 21 Nov 2024 22:52:39 +0100 Subject: [PATCH 22/55] refactor(sidebar): highlight active route in sidebar menu --- app/components/layout/sidebar/sidebar-nav.tsx | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 80c61e704..1fa7efbfe 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -13,9 +13,10 @@ import { CollapsibleContent, CollapsibleTrigger, } from "~/components/shared/collapsible"; -import { NavLink } from "@remix-run/react"; +import { matchRoutes, NavLink, useMatch, useMatches } from "@remix-run/react"; import { NavItem, useSidebarNavItems } from "~/hooks/use-sidebar-nav-items"; import Icon from "~/components/icons/icon"; +import { tw } from "~/utils/tw"; type SidebarNavProps = { className?: string; @@ -28,6 +29,13 @@ export default function SidebarNav({ style, items, }: SidebarNavProps) { + const matches = useMatches(); + + function isRouteActive(route: string) { + const matchesRoutes = matches.map((match) => match.pathname); + return matchesRoutes.some((matchRoute) => matchRoute.includes(route)); + } + return ( @@ -49,16 +57,27 @@ export default function SidebarNav({ - {item.children.map((child) => ( - - - - - {child.title} - - - - ))} + {item.children.map((child) => { + const isChildActive = isRouteActive(child.to); + + return ( + + + + + {child.title} + + + + ); + })}
    @@ -66,10 +85,16 @@ export default function SidebarNav({ ); } + const isActive = isRouteActive(item.to); + return ( - + {item.title} From 01fb3a0d74cda564946e5f32403b933aa17b950a Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 22 Nov 2024 15:32:58 +0100 Subject: [PATCH 23/55] refactor(sidebar): fix pr issues --- app/components/icons/icon.tsx | 7 +++++-- app/components/icons/library.tsx | 4 ++-- app/components/layout/breadcrumbs/index.tsx | 20 ++++++++----------- .../layout/sidebar/organization-selector.tsx | 6 +++--- app/components/layout/sidebar/sidebar-nav.tsx | 12 +++++++++-- .../layout/sidebar/sidebar-user-menu.tsx | 4 ++-- app/components/layout/sidebar/sidebar.tsx | 2 +- app/root.tsx | 7 ++----- app/tailwind.css | 13 ++++++++++++ tailwind.config.ts | 10 ++++++++++ 10 files changed, 56 insertions(+), 29 deletions(-) diff --git a/app/components/icons/icon.tsx b/app/components/icons/icon.tsx index 32f309438..5ace388f6 100644 --- a/app/components/icons/icon.tsx +++ b/app/components/icons/icon.tsx @@ -4,12 +4,13 @@ import type { IconType } from "../shared/icons-map"; import iconsMap from "../shared/icons-map"; export interface IconProps { + className?: string; icon: IconType; disableWrap?: true; size?: React.ComponentProps["size"]; } const Icon = React.forwardRef(function Icon( - { icon, disableWrap, size = "sm" }: IconProps, + { className, icon, disableWrap, size = "sm" }: IconProps, _ref ) { return ( @@ -17,7 +18,9 @@ const Icon = React.forwardRef(function Icon( (disableWrap ? (
    {iconsMap[icon]}
    ) : ( - {iconsMap[icon]} + + {iconsMap[icon]} + )) ); }); diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index f73604f75..c75727890 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -1574,9 +1574,9 @@ export const AssetLabel = (props: SVGProps) => ( - -
    - {breadcrumbs.map((match, index) => ( - - ))} -
    +
    + {breadcrumbs.map((match, index) => ( + + ))}
    ); } diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index 3c295f9d8..569222113 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -74,7 +74,7 @@ export default function OrganizationSelector() { {currentOrganization.type === "PERSONAL" ? ( @@ -101,7 +101,7 @@ export default function OrganizationSelector() { ( { if (organization.id !== currentOrganizationId) { handleSwitchOrganization(organization.id); diff --git a/app/components/layout/sidebar/sidebar-nav.tsx b/app/components/layout/sidebar/sidebar-nav.tsx index 1fa7efbfe..34da6f0ee 100644 --- a/app/components/layout/sidebar/sidebar-nav.tsx +++ b/app/components/layout/sidebar/sidebar-nav.tsx @@ -67,11 +67,16 @@ export default function SidebarNav({ to={child.to} target={child.target} className={tw( + "font-medium", isChildActive && "text-primary-500 bg-primary-25" )} > - + {child.title}
    @@ -93,7 +98,10 @@ export default function SidebarNav({ {item.title} diff --git a/app/components/layout/sidebar/sidebar-user-menu.tsx b/app/components/layout/sidebar/sidebar-user-menu.tsx index 2a73fd1a1..8e7753e88 100644 --- a/app/components/layout/sidebar/sidebar-user-menu.tsx +++ b/app/components/layout/sidebar/sidebar-user-menu.tsx @@ -37,7 +37,7 @@ export default function SidebarUserMenu() { - + Toggle Sidebar ); diff --git a/app/root.tsx b/app/root.tsx index 3afd92311..b170218ae 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -34,6 +34,7 @@ import { data } from "./utils/http.server"; import { useNonce } from "./utils/nonce-provider"; import { PwaManagerProvider } from "./utils/pwa-manager"; import { splashScreenLinks } from "./utils/splash-screen-links"; +import { SidebarTrigger } from "./components/layout/sidebar/sidebar"; export interface RootData { env: typeof getBrowserEnv; @@ -41,11 +42,7 @@ export interface RootData { } export const handle = { - breadcrumb: () => ( - - - - ), + breadcrumb: () => , }; export const links: LinksFunction = () => [ diff --git a/app/tailwind.css b/app/tailwind.css index b5c61c956..ab6af0c90 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -1,3 +1,16 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer base { + :root { + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 37afe7158..5bf0309e6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -221,6 +221,16 @@ export default { inverted: "#000000", // black }, }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, }, boxShadow: { From 9be30ee5311beeb9634af2c94dd6afb3e62350e8 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Sat, 23 Nov 2024 12:26:40 +0100 Subject: [PATCH 24/55] refactor(sidebar): fix color issues and implement sidebar with cookie and make it optimistic --- app/components/layout/sidebar/sidebar.tsx | 57 ++++++++++++++++------- app/routes/_layout+/_layout.tsx | 4 +- app/tailwind.css | 1 - tailwind.config.ts | 1 - 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index e6642cf4c..fc29f7234 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -24,9 +24,9 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/shared/tooltip"; +import { useFetcher } from "@remix-run/react"; +import { isFormProcessing } from "~/utils/form"; -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; @@ -40,6 +40,7 @@ type SidebarContext = { setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; + isSidebarToggling: boolean; }; const SidebarContext = createContext(null); @@ -76,6 +77,9 @@ const SidebarProvider = forwardRef< const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = useState(false); + const sidebarTogglerFetcher = useFetcher(); + const isSidebarToggling = isFormProcessing(sidebarTogglerFetcher.state); + // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = useState(defaultOpen); @@ -89,8 +93,14 @@ const SidebarProvider = forwardRef< _setOpen(openState); } - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + /* Setting state of sidebar in cookies (userPrefs) */ + const formData = new FormData(); + formData.append("minimizeSidebar", openState ? "close" : "open"); + + sidebarTogglerFetcher.submit(formData, { + method: "POST", + action: "/api/user/prefs/minimized-sidebar", + }); }, [setOpenProp, open] ); @@ -131,8 +141,18 @@ const SidebarProvider = forwardRef< openMobile, setOpenMobile, toggleSidebar, + isSidebarToggling, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + isSidebarToggling, + ] ); return ( @@ -269,15 +289,19 @@ const SidebarTrigger = forwardRef< React.ElementRef, React.ComponentProps >(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); + const { toggleSidebar, isSidebarToggling } = useSidebar(); return ( + + ); + } + + default: { + return null; + } + } + }, []); + + return ( +
    +
    + + + + {content} +
    +
    + ); +} diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts new file mode 100644 index 000000000..e6bb418f8 --- /dev/null +++ b/app/components/errors/utils.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +import { isRouteError } from "~/utils/http"; + +export const specialErrorAdditionalData = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("asset-from-other-org"), + assetOrganization: z.object({ + organization: z.object({ + id: z.string(), + name: z.string(), + }), + }), + }), +]); + +export type SpecialErrorAdditionalData = z.infer< + typeof specialErrorAdditionalData +>; + +export function parseSpecialErrorData(response: unknown): + | { success: false; additionalData: null } + | { + success: true; + additionalData: SpecialErrorAdditionalData; + } { + if (!isRouteError(response)) { + return { success: false, additionalData: null }; + } + + const parsedDataResponse = specialErrorAdditionalData.safeParse( + response.data.error.additionalData + ); + + if (!parsedDataResponse.success) { + return { success: false, additionalData: null }; + } + + return { success: true, additionalData: parsedDataResponse.data }; +} diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index 2f0ff97e7..fc8439913 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -134,7 +134,7 @@ export async function getAsset({ title: "Asset not found", message: "", additionalData: { - model: "asset", + type: "asset-from-other-org", assetOrganization: userOrganizations.find( (org) => org.organizationId === asset.organizationId ), From 9e82e8de21857ccfb0e0d2c091d6c007d709d37e Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Wed, 20 Nov 2024 19:11:36 +0100 Subject: [PATCH 28/55] pr review changes --- .../errors/special-error-handler.tsx | 62 +++++++++++-------- app/components/errors/utils.ts | 17 +++-- app/modules/asset/service.server.ts | 17 ++--- .../api+/user.change-current-organization.ts | 5 +- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/app/components/errors/special-error-handler.tsx b/app/components/errors/special-error-handler.tsx index 20c8f3ebe..1ebf3914c 100644 --- a/app/components/errors/special-error-handler.tsx +++ b/app/components/errors/special-error-handler.tsx @@ -1,8 +1,9 @@ import { tw } from "~/utils/tw"; import { SpecialErrorAdditionalData } from "./utils"; -import { useMemo } from "react"; import { ErrorIcon } from "."; import { Button } from "../shared/button"; +import { useFetcher } from "@remix-run/react"; +import { isFormProcessing } from "~/utils/form"; export type SpecialErrorHandlerProps = { className?: string; @@ -15,31 +16,8 @@ export default function SpecialErrorHandler({ style, additionalData, }: SpecialErrorHandlerProps) { - const content = useMemo(() => { - switch (additionalData.type) { - case "asset-from-other-org": { - return ( -
    -

    Asset belongs to other workspace.

    -

    - The asset you are trying to view belongs to a different workspace - you are part of. Would you like to switch to workspace{" "} - - "{additionalData.assetOrganization.organization.name}" - {" "} - to view the asset? -

    - - -
    - ); - } - - default: { - return null; - } - } - }, []); + const fetcher = useFetcher(); + const disabled = isFormProcessing(fetcher.state); return (
    - {content} +
    +

    + {additionalData.model} belongs + to other workspace. +

    +

    + The {additionalData.model} you are trying to view belongs to a + different workspace you are part of. Would you like to switch to + workspace{" "} + + "{additionalData.organization.organization.name}" + {" "} + to view the {additionalData.model}? +

    + + + + + +
    ); diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index e6bb418f8..a8e804521 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -1,17 +1,16 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; -export const specialErrorAdditionalData = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("asset-from-other-org"), - assetOrganization: z.object({ - organization: z.object({ - id: z.string(), - name: z.string(), - }), +export const specialErrorAdditionalData = z.object({ + model: z.enum(["assets"]), + id: z.string(), + organization: z.object({ + organization: z.object({ + id: z.string(), + name: z.string(), }), }), -]); +}); export type SpecialErrorAdditionalData = z.infer< typeof specialErrorAdditionalData diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index fc8439913..2139004f7 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -106,11 +106,11 @@ export async function getAsset({ include, }: Pick & { organizationId: Asset["organizationId"]; - userOrganizations: Pick[]; + userOrganizations?: Pick[]; include?: T; }): Promise> { try { - const otherOrganizationIds = userOrganizations.map( + const otherOrganizationIds = userOrganizations?.map( (org) => org.organizationId ); @@ -118,7 +118,9 @@ export async function getAsset({ where: { OR: [ { id, organizationId }, - { id, organizationId: { in: otherOrganizationIds } }, + ...(userOrganizations?.length + ? [{ id, organizationId: { in: otherOrganizationIds } }] + : []), ], }, include: { ...include }, @@ -126,16 +128,17 @@ export async function getAsset({ /* User is accessing the asset in the wrong organization. In that case we need special 404 handling. */ if ( + userOrganizations?.length && asset.organizationId !== organizationId && - otherOrganizationIds.includes(asset.organizationId) + otherOrganizationIds?.includes(asset.organizationId) ) { throw new ShelfError({ cause: null, title: "Asset not found", message: "", additionalData: { - type: "asset-from-other-org", - assetOrganization: userOrganizations.find( + model: "assets", + organization: userOrganizations.find( (org) => org.organizationId === asset.organizationId ), }, @@ -153,7 +156,7 @@ export async function getAsset({ additionalData: { id, organizationId, - ...(cause instanceof ShelfError ? cause.additionalData : {}), + ...(isLikeShelfError(cause) ? cause.additionalData : {}), }, label, shouldBeCaptured: !isNotFoundError(cause), diff --git a/app/routes/api+/user.change-current-organization.ts b/app/routes/api+/user.change-current-organization.ts index b0a62c1e3..b7eca4bee 100644 --- a/app/routes/api+/user.change-current-organization.ts +++ b/app/routes/api+/user.change-current-organization.ts @@ -10,14 +10,15 @@ export async function action({ context, request }: ActionFunctionArgs) { const { userId } = authSession; try { - const { organizationId } = parseData( + const { organizationId, redirectTo } = parseData( await request.formData(), z.object({ organizationId: z.string(), + redirectTo: z.string().optional(), }) ); - return redirect("/", { + return redirect(redirectTo ?? "/", { headers: [ setCookie(await setSelectedOrganizationIdCookie(organizationId)), ], From ad2dda3df7ff9478961f0d1bd732729d0ddec349 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 22 Nov 2024 14:25:22 +0100 Subject: [PATCH 29/55] feat(improve-404): fix pr review issues --- ...rror-handler.tsx => error-404-handler.tsx} | 17 ++++++------- app/components/errors/index.tsx | 10 ++++---- app/components/errors/utils.ts | 25 +++++++++---------- app/modules/asset/service.server.ts | 11 +++++++- .../_layout+/assets.$assetId.overview.tsx | 1 + app/routes/api+/asset.scan.ts | 3 +-- .../api+/user.change-current-organization.ts | 4 +-- 7 files changed, 38 insertions(+), 33 deletions(-) rename app/components/errors/{special-error-handler.tsx => error-404-handler.tsx} (79%) diff --git a/app/components/errors/special-error-handler.tsx b/app/components/errors/error-404-handler.tsx similarity index 79% rename from app/components/errors/special-error-handler.tsx rename to app/components/errors/error-404-handler.tsx index 1ebf3914c..b84fc7a76 100644 --- a/app/components/errors/special-error-handler.tsx +++ b/app/components/errors/error-404-handler.tsx @@ -1,21 +1,21 @@ import { tw } from "~/utils/tw"; -import { SpecialErrorAdditionalData } from "./utils"; +import { Error404AdditionalData } from "./utils"; import { ErrorIcon } from "."; import { Button } from "../shared/button"; import { useFetcher } from "@remix-run/react"; import { isFormProcessing } from "~/utils/form"; -export type SpecialErrorHandlerProps = { +export type Error404HandlerProps = { className?: string; style?: React.CSSProperties; - additionalData: SpecialErrorAdditionalData; + additionalData: Error404AdditionalData; }; -export default function SpecialErrorHandler({ +export default function Error404Handler({ className, style, additionalData, -}: SpecialErrorHandlerProps) { +}: Error404HandlerProps) { const fetcher = useFetcher(); const disabled = isFormProcessing(fetcher.state); @@ -25,13 +25,10 @@ export default function SpecialErrorHandler({ style={style} >
    - - -

    {additionalData.model} belongs - to other workspace. + to another workspace.

    The {additionalData.model} you are trying to view belongs to a @@ -54,7 +51,7 @@ export default function SpecialErrorHandler({ diff --git a/app/components/errors/index.tsx b/app/components/errors/index.tsx index 4fb7f58f7..e45f57727 100644 --- a/app/components/errors/index.tsx +++ b/app/components/errors/index.tsx @@ -2,8 +2,8 @@ import { useLocation, useRouteError } from "@remix-run/react"; import { isRouteError } from "~/utils/http"; import { Button } from "../shared/button"; -import { parseSpecialErrorData } from "./utils"; -import SpecialErrorHandler from "./special-error-handler"; +import { parse404ErrorData } from "./utils"; +import Error404Handler from "./error-404-handler"; export const ErrorContent = () => { const loc = useLocation(); @@ -20,9 +20,9 @@ export const ErrorContent = () => { traceId = response.data.error.traceId; } - const specialError = parseSpecialErrorData(response); - if (specialError.success) { - return ; + const error404 = parse404ErrorData(response); + if (error404.isError404) { + return ; } // Creating a string with
    tags for line breaks diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index a8e804521..48249fdea 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -1,9 +1,10 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; -export const specialErrorAdditionalData = z.object({ - model: z.enum(["assets"]), +export const error404AdditionalData = z.object({ + model: z.enum(["asset"]), id: z.string(), + redirectTo: z.string().optional(), organization: z.object({ organization: z.object({ id: z.string(), @@ -12,27 +13,25 @@ export const specialErrorAdditionalData = z.object({ }), }); -export type SpecialErrorAdditionalData = z.infer< - typeof specialErrorAdditionalData ->; +export type Error404AdditionalData = z.infer; -export function parseSpecialErrorData(response: unknown): - | { success: false; additionalData: null } +export function parse404ErrorData(response: unknown): + | { isError404: false; additionalData: null } | { - success: true; - additionalData: SpecialErrorAdditionalData; + isError404: true; + additionalData: Error404AdditionalData; } { if (!isRouteError(response)) { - return { success: false, additionalData: null }; + return { isError404: false, additionalData: null }; } - const parsedDataResponse = specialErrorAdditionalData.safeParse( + const parsedDataResponse = error404AdditionalData.safeParse( response.data.error.additionalData ); if (!parsedDataResponse.success) { - return { success: false, additionalData: null }; + return { isError404: false, additionalData: null }; } - return { success: true, additionalData: parsedDataResponse.data }; + return { isError404: true, additionalData: parsedDataResponse.data }; } diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index 2139004f7..74bcb1734 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -103,10 +103,12 @@ export async function getAsset({ id, organizationId, userOrganizations, + request, include, }: Pick & { organizationId: Asset["organizationId"]; userOrganizations?: Pick[]; + request?: Request; include?: T; }): Promise> { try { @@ -132,17 +134,24 @@ export async function getAsset({ asset.organizationId !== organizationId && otherOrganizationIds?.includes(asset.organizationId) ) { + const redirectTo = + typeof request !== "undefined" + ? new URL(request.url).pathname + : undefined; + throw new ShelfError({ cause: null, title: "Asset not found", message: "", additionalData: { - model: "assets", + model: "asset", organization: userOrganizations.find( (org) => org.organizationId === asset.organizationId ), + redirectTo, }, label, + status: 404, }); } diff --git a/app/routes/_layout+/assets.$assetId.overview.tsx b/app/routes/_layout+/assets.$assetId.overview.tsx index 9e8e224f0..00bb54aa8 100644 --- a/app/routes/_layout+/assets.$assetId.overview.tsx +++ b/app/routes/_layout+/assets.$assetId.overview.tsx @@ -83,6 +83,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id, organizationId, userOrganizations, + request, include: ASSET_OVERVIEW_FIELDS, }); diff --git a/app/routes/api+/asset.scan.ts b/app/routes/api+/asset.scan.ts index 19d8477fe..dbbd7b868 100644 --- a/app/routes/api+/asset.scan.ts +++ b/app/routes/api+/asset.scan.ts @@ -19,7 +19,7 @@ export async function action({ context, request }: ActionFunctionArgs) { const { userId } = authSession; try { - const { organizationId, userOrganizations } = await requirePermission({ + const { organizationId } = await requirePermission({ userId, request, entity: PermissionEntity.asset, @@ -42,7 +42,6 @@ export async function action({ context, request }: ActionFunctionArgs) { const asset = await getAsset({ id: assetId, organizationId, - userOrganizations, include: { qrCodes: true }, }); /** WE get the first qrCode as the app only supports 1 code per asset for now */ diff --git a/app/routes/api+/user.change-current-organization.ts b/app/routes/api+/user.change-current-organization.ts index b7eca4bee..26463381d 100644 --- a/app/routes/api+/user.change-current-organization.ts +++ b/app/routes/api+/user.change-current-organization.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { setCookie } from "~/utils/cookies.server"; import { makeShelfError } from "~/utils/error"; -import { error, parseData } from "~/utils/http.server"; +import { error, parseData, safeRedirect } from "~/utils/http.server"; export async function action({ context, request }: ActionFunctionArgs) { const authSession = context.getSession(); @@ -18,7 +18,7 @@ export async function action({ context, request }: ActionFunctionArgs) { }) ); - return redirect(redirectTo ?? "/", { + return redirect(safeRedirect(redirectTo), { headers: [ setCookie(await setSelectedOrganizationIdCookie(organizationId)), ], From 64b954ae437d99af56dc1ffb8aeea07b0b100d66 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Sat, 23 Nov 2024 12:42:28 +0100 Subject: [PATCH 30/55] feat(improve-404): add request in getAsset for proper redirecting --- app/components/errors/error-404-handler.tsx | 1 - app/routes/_layout+/assets.$assetId.activity.tsx | 1 + app/routes/_layout+/assets.$assetId.overview.duplicate.tsx | 1 + .../_layout+/assets.$assetId.overview.update-location.tsx | 7 ++++++- app/routes/_layout+/assets.$assetId.tsx | 1 + app/routes/_layout+/assets.$assetId_.edit.tsx | 1 + 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/components/errors/error-404-handler.tsx b/app/components/errors/error-404-handler.tsx index b84fc7a76..4b91e4165 100644 --- a/app/components/errors/error-404-handler.tsx +++ b/app/components/errors/error-404-handler.tsx @@ -1,6 +1,5 @@ import { tw } from "~/utils/tw"; import { Error404AdditionalData } from "./utils"; -import { ErrorIcon } from "."; import { Button } from "../shared/button"; import { useFetcher } from "@remix-run/react"; import { isFormProcessing } from "~/utils/form"; diff --git a/app/routes/_layout+/assets.$assetId.activity.tsx b/app/routes/_layout+/assets.$assetId.activity.tsx index 0be205ff5..20b8246f9 100644 --- a/app/routes/_layout+/assets.$assetId.activity.tsx +++ b/app/routes/_layout+/assets.$assetId.activity.tsx @@ -39,6 +39,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id, organizationId, userOrganizations, + request, include: { notes: { orderBy: { createdAt: "desc" }, diff --git a/app/routes/_layout+/assets.$assetId.overview.duplicate.tsx b/app/routes/_layout+/assets.$assetId.overview.duplicate.tsx index b6ff87f53..4e94fc0b1 100644 --- a/app/routes/_layout+/assets.$assetId.overview.duplicate.tsx +++ b/app/routes/_layout+/assets.$assetId.overview.duplicate.tsx @@ -44,6 +44,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id: assetId, organizationId, userOrganizations, + request, }); return json( diff --git a/app/routes/_layout+/assets.$assetId.overview.update-location.tsx b/app/routes/_layout+/assets.$assetId.overview.update-location.tsx index 90f66d248..d87d75f9f 100644 --- a/app/routes/_layout+/assets.$assetId.overview.update-location.tsx +++ b/app/routes/_layout+/assets.$assetId.overview.update-location.tsx @@ -36,7 +36,12 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { entity: PermissionEntity.asset, action: PermissionAction.update, }); - const asset = await getAsset({ organizationId, id, userOrganizations }); + const asset = await getAsset({ + organizationId, + id, + userOrganizations, + request, + }); const { locations } = await getAllEntriesForCreateAndEdit({ organizationId, diff --git a/app/routes/_layout+/assets.$assetId.tsx b/app/routes/_layout+/assets.$assetId.tsx index 6167c79f4..20bfa1da8 100644 --- a/app/routes/_layout+/assets.$assetId.tsx +++ b/app/routes/_layout+/assets.$assetId.tsx @@ -66,6 +66,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id, organizationId, userOrganizations, + request, include: { custody: { include: { custodian: true } }, kit: true, diff --git a/app/routes/_layout+/assets.$assetId_.edit.tsx b/app/routes/_layout+/assets.$assetId_.edit.tsx index 0c8f157ac..de6d7a1af 100644 --- a/app/routes/_layout+/assets.$assetId_.edit.tsx +++ b/app/routes/_layout+/assets.$assetId_.edit.tsx @@ -65,6 +65,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id, include: { tags: true, customFields: true }, userOrganizations, + request, }); const { categories, totalCategories, tags, locations, totalLocations } = From 473f9f5965a3352253251e8ef44dfeba6f43f9d1 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Sat, 23 Nov 2024 19:47:12 +0100 Subject: [PATCH 31/55] feat(improve-404): update getKit function to make it more generic --- app/modules/kit/service.server.ts | 15 ++++++++++++--- app/modules/qr/service.server.ts | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/modules/kit/service.server.ts b/app/modules/kit/service.server.ts index 7a52cd756..6b66a1716 100644 --- a/app/modules/kit/service.server.ts +++ b/app/modules/kit/service.server.ts @@ -341,7 +341,14 @@ export async function getPaginatedAndFilterableKits< } } -export async function getKit({ +type KitWithInclude = + T extends Prisma.KitInclude + ? Prisma.KitGetPayload<{ + include: MergeInclude; + }> + : Prisma.KitGetPayload<{ include: typeof GET_KIT_STATIC_INCLUDES }>; + +export async function getKit({ id, organizationId, extraInclude, @@ -353,10 +360,12 @@ export async function getKit({ ...extraInclude, } as MergeInclude; - return (await db.kit.findUniqueOrThrow({ + const kit = await db.kit.findUniqueOrThrow({ where: { id, organizationId }, include: includes, - })) as Prisma.KitGetPayload<{ include: typeof includes }>; + }); + + return kit as KitWithInclude; } catch (cause) { throw new ShelfError({ cause, diff --git a/app/modules/qr/service.server.ts b/app/modules/qr/service.server.ts index ca01b7bd8..074ca8512 100644 --- a/app/modules/qr/service.server.ts +++ b/app/modules/qr/service.server.ts @@ -37,6 +37,7 @@ export async function getQrByAssetId({ assetId }: Pick) { }); } } + export async function getQrByKitId({ kitId }: Pick) { try { return await db.qr.findFirst({ From c226fcce4ce42d5859dd13faaf3d93c76478591e Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Mon, 25 Nov 2024 13:53:18 +0100 Subject: [PATCH 32/55] feat(improve-404): improve 404 handling for kit --- app/components/errors/utils.ts | 2 +- app/modules/kit/service.server.ts | 66 +++++++++++++++++-- .../_layout+/kits.$kitId.assign-custody.tsx | 16 +++-- .../kits.$kitId.create-new-booking.tsx | 20 ++++-- .../_layout+/kits.$kitId.release-custody.tsx | 9 ++- app/routes/_layout+/kits.$kitId.tsx | 4 +- app/routes/_layout+/kits.$kitId_.edit.tsx | 9 ++- 7 files changed, 101 insertions(+), 25 deletions(-) diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index 48249fdea..9fbe8abff 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; export const error404AdditionalData = z.object({ - model: z.enum(["asset"]), + model: z.enum(["asset", "kit"]), id: z.string(), redirectTo: z.string().optional(), organization: z.object({ diff --git a/app/modules/kit/service.server.ts b/app/modules/kit/service.server.ts index 6b66a1716..571349aea 100644 --- a/app/modules/kit/service.server.ts +++ b/app/modules/kit/service.server.ts @@ -6,6 +6,7 @@ import type { Qr, TeamMember, User, + UserOrganization, } from "@prisma/client"; import { AssetStatus, @@ -21,7 +22,12 @@ import { getDateTimeFormat } from "~/utils/client-hints"; import { updateCookieWithPerPage } from "~/utils/cookies.server"; import { dateTimeInUnix } from "~/utils/date-time-in-unix"; import type { ErrorLabel } from "~/utils/error"; -import { maybeUniqueConstraintViolation, ShelfError } from "~/utils/error"; +import { + isLikeShelfError, + isNotFoundError, + maybeUniqueConstraintViolation, + ShelfError, +} from "~/utils/error"; import { extractImageNameFromSupabaseUrl } from "~/utils/extract-image-name-from-supabase-url"; import { getCurrentSearchParams } from "~/utils/http.server"; import { id } from "~/utils/id/id.server"; @@ -352,19 +358,63 @@ export async function getKit({ id, organizationId, extraInclude, -}: Pick & { extraInclude?: T }) { + userOrganizations, + request, +}: Pick & { + extraInclude?: T; + userOrganizations?: Pick[]; + request?: Request; +}) { try { + const otherOrganizationIds = userOrganizations?.map( + (org) => org.organizationId + ); + // Merge static includes with dynamic includes const includes = { ...GET_KIT_STATIC_INCLUDES, ...extraInclude, } as MergeInclude; - const kit = await db.kit.findUniqueOrThrow({ - where: { id, organizationId }, + const kit = await db.kit.findFirstOrThrow({ + where: { + OR: [ + { id, organizationId }, + ...(userOrganizations?.length + ? [{ id, organizationId: { in: otherOrganizationIds } }] + : []), + ], + }, include: includes, }); + /* User is accessing the asset in the wrong organizations. In that case we need special 404 handlng. */ + if ( + userOrganizations?.length && + kit.organizationId !== organizationId && + otherOrganizationIds?.includes(kit.organizationId) + ) { + const redirectTo = + typeof request !== "undefined" + ? new URL(request.url).pathname + : undefined; + + throw new ShelfError({ + cause: null, + title: "Kit not found", + message: "", + additionalData: { + model: "kit", + organization: userOrganizations.find( + (org) => org.organizationId === kit.organizationId + ), + redirectTo, + }, + label, + status: 404, + }); + } + return kit as KitWithInclude; } catch (cause) { throw new ShelfError({ @@ -372,8 +422,12 @@ export async function getKit({ title: "Kit not found", message: "The kit you are trying to access does not exist or you do not have permission to access it.", - additionalData: { id }, - label, // Adjust the label as needed + additionalData: { + id, + ...(isLikeShelfError(cause) ? cause.additionalData : {}), + }, + label, + shouldBeCaptured: !isNotFoundError(cause), }); } } diff --git a/app/routes/_layout+/kits.$kitId.assign-custody.tsx b/app/routes/_layout+/kits.$kitId.assign-custody.tsx index c96139e46..be5d8981a 100644 --- a/app/routes/_layout+/kits.$kitId.assign-custody.tsx +++ b/app/routes/_layout+/kits.$kitId.assign-custody.tsx @@ -55,12 +55,14 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }); try { - const { organizationId, role } = await requirePermission({ - userId, - request, - entity: PermissionEntity.kit, - action: PermissionAction.custody, - }); + const { organizationId, role, userOrganizations } = await requirePermission( + { + userId, + request, + entity: PermissionEntity.kit, + action: PermissionAction.custody, + } + ); const kit = await getKit({ id: kitId, @@ -81,6 +83,8 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }, }, }, + userOrganizations, + request, }); if (kit.custody) { diff --git a/app/routes/_layout+/kits.$kitId.create-new-booking.tsx b/app/routes/_layout+/kits.$kitId.create-new-booking.tsx index c47d1c1cc..33b4631c7 100644 --- a/app/routes/_layout+/kits.$kitId.create-new-booking.tsx +++ b/app/routes/_layout+/kits.$kitId.create-new-booking.tsx @@ -31,13 +31,17 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }); try { - const { organizationId, currentOrganization, isSelfServiceOrBase } = - await requirePermission({ - userId, - request, - entity: PermissionEntity.booking, - action: PermissionAction.create, - }); + const { + organizationId, + currentOrganization, + isSelfServiceOrBase, + userOrganizations, + } = await requirePermission({ + userId, + request, + entity: PermissionEntity.booking, + action: PermissionAction.create, + }); if (isPersonalOrg(currentOrganization)) { throw new ShelfError({ @@ -54,6 +58,8 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { id: kitId, organizationId, extraInclude: { assets: true }, + userOrganizations, + request, }); /* We need to fetch the team members to be able to display them in the custodian dropdown. */ diff --git a/app/routes/_layout+/kits.$kitId.release-custody.tsx b/app/routes/_layout+/kits.$kitId.release-custody.tsx index b7190eee1..5d266d3e7 100644 --- a/app/routes/_layout+/kits.$kitId.release-custody.tsx +++ b/app/routes/_layout+/kits.$kitId.release-custody.tsx @@ -31,14 +31,19 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }); try { - const { organizationId } = await requirePermission({ + const { organizationId, userOrganizations } = await requirePermission({ userId, request, entity: PermissionEntity.kit, action: PermissionAction.custody, }); - const kit = await getKit({ id: kitId, organizationId }); + const kit = await getKit({ + id: kitId, + organizationId, + userOrganizations, + request, + }); if (!kit.custody) { return redirect(`/kits/${kitId}`); } diff --git a/app/routes/_layout+/kits.$kitId.tsx b/app/routes/_layout+/kits.$kitId.tsx index e2a855199..353d45dc5 100644 --- a/app/routes/_layout+/kits.$kitId.tsx +++ b/app/routes/_layout+/kits.$kitId.tsx @@ -75,7 +75,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { ); try { - const { organizationId } = await requirePermission({ + const { organizationId, userOrganizations } = await requirePermission({ userId, request, entity: PermissionEntity.kit, @@ -131,6 +131,8 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }, qrCodes: true, }, + userOrganizations, + request, }), getAssetsForKits({ request, diff --git a/app/routes/_layout+/kits.$kitId_.edit.tsx b/app/routes/_layout+/kits.$kitId_.edit.tsx index fec0c6ff2..fb983c209 100644 --- a/app/routes/_layout+/kits.$kitId_.edit.tsx +++ b/app/routes/_layout+/kits.$kitId_.edit.tsx @@ -43,14 +43,19 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { }); try { - const { organizationId } = await requirePermission({ + const { organizationId, userOrganizations } = await requirePermission({ userId, request, entity: PermissionEntity.kit, action: PermissionAction.update, }); - const kit = await getKit({ id: kitId, organizationId }); + const kit = await getKit({ + id: kitId, + organizationId, + userOrganizations, + request, + }); const header: HeaderData = { title: `Edit | ${kit.name}`, From 6847db211817faac246845961b5518a160d55c1f Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 26 Nov 2024 15:07:00 +0100 Subject: [PATCH 33/55] feat(improve-404): improve 404 handling for location --- app/components/errors/utils.ts | 2 +- app/modules/location/service.server.ts | 73 +++++++++++++++++-- app/routes/_layout+/locations.$locationId.tsx | 6 +- .../_layout+/locations.$locationId_.edit.tsx | 9 ++- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index 9fbe8abff..4b45330ca 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; export const error404AdditionalData = z.object({ - model: z.enum(["asset", "kit"]), + model: z.enum(["asset", "kit", "location"]), id: z.string(), redirectTo: z.string().optional(), organization: z.object({ diff --git a/app/modules/location/service.server.ts b/app/modules/location/service.server.ts index 70f43678b..bcb1eb755 100644 --- a/app/modules/location/service.server.ts +++ b/app/modules/location/service.server.ts @@ -1,8 +1,19 @@ -import type { Prisma, User, Location, Organization } from "@prisma/client"; +import type { + Prisma, + User, + Location, + Organization, + UserOrganization, +} from "@prisma/client"; import invariant from "tiny-invariant"; import { db } from "~/database/db.server"; import type { ErrorLabel } from "~/utils/error"; -import { ShelfError, maybeUniqueConstraintViolation } from "~/utils/error"; +import { + ShelfError, + isLikeShelfError, + isNotFoundError, + maybeUniqueConstraintViolation, +} from "~/utils/error"; import { ALL_SELECTED_KEY } from "~/utils/list"; import type { CreateAssetFromContentImportPayload } from "../asset/types"; @@ -16,11 +27,25 @@ export async function getLocation( /** Assets to be loaded per page with the location */ perPage?: number; search?: string | null; + userOrganizations?: Pick[]; + request?: Request; } ) { - const { organizationId, id, page = 1, perPage = 8, search } = params; + const { + organizationId, + id, + page = 1, + perPage = 8, + search, + userOrganizations, + request, + } = params; try { + const otherOrganizationIds = userOrganizations?.map( + (org) => org.organizationId + ); + const skip = page > 1 ? (page - 1) * perPage : 0; const take = perPage >= 1 ? perPage : 8; // min 1 and max 25 per page @@ -37,7 +62,14 @@ export async function getLocation( const [location, totalAssetsWithinLocation] = await Promise.all([ /** Get the items */ db.location.findFirstOrThrow({ - where: { id, organizationId }, + where: { + OR: [ + { id, organizationId }, + ...(userOrganizations?.length + ? [{ id, organizationId: { in: otherOrganizationIds } }] + : []), + ], + }, include: { image: { select: { @@ -64,13 +96,44 @@ export async function getLocation( }), ]); + /* User is accessing the asset in the wrong organization. In that case we need special 404 handling. */ + if ( + userOrganizations?.length && + location.organizationId !== organizationId && + otherOrganizationIds?.includes(location.organizationId) + ) { + const redirectTo = + typeof request !== "undefined" + ? new URL(request.url).pathname + : undefined; + + throw new ShelfError({ + cause: null, + title: "Location not found.", + message: "", + additionalData: { + model: "location", + organization: userOrganizations.find( + (org) => org.organizationId === location.organizationId + ), + redirectTo, + }, + label, + status: 404, + }); + } + return { location, totalAssetsWithinLocation }; } catch (cause) { throw new ShelfError({ cause, message: "Something went wrong while fetching location", - additionalData: { ...params }, + additionalData: { + ...params, + ...(isLikeShelfError(cause) ? cause.additionalData : {}), + }, label, + shouldBeCaptured: !isNotFoundError(cause), }); } } diff --git a/app/routes/_layout+/locations.$locationId.tsx b/app/routes/_layout+/locations.$locationId.tsx index 4c2fda497..fba46add2 100644 --- a/app/routes/_layout+/locations.$locationId.tsx +++ b/app/routes/_layout+/locations.$locationId.tsx @@ -62,7 +62,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { ); try { - const { organizationId } = await requirePermission({ + const { organizationId, userOrganizations } = await requirePermission({ userId: authSession.userId, request, entity: PermissionEntity.location, @@ -80,6 +80,8 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { page, perPage, search, + userOrganizations, + request, }); const totalItems = totalAssetsWithinLocation; @@ -358,5 +360,3 @@ const ListItemTagsColumn = ({ tags }: { tags: Tag[] | undefined }) => {

    ) : null; }; - -// export const ErrorBoundary = () => ; diff --git a/app/routes/_layout+/locations.$locationId_.edit.tsx b/app/routes/_layout+/locations.$locationId_.edit.tsx index c40e12387..6ab489b9d 100644 --- a/app/routes/_layout+/locations.$locationId_.edit.tsx +++ b/app/routes/_layout+/locations.$locationId_.edit.tsx @@ -43,14 +43,19 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) { ); try { - const { organizationId } = await requirePermission({ + const { organizationId, userOrganizations } = await requirePermission({ userId: authSession.userId, request, entity: PermissionEntity.location, action: PermissionAction.update, }); - const { location } = await getLocation({ organizationId, id }); + const { location } = await getLocation({ + organizationId, + id, + userOrganizations, + request, + }); const header: HeaderData = { title: `Edit | ${location.name}`, From f36401662ac11a665476dc7d9079d348b8e6a8d4 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 26 Nov 2024 20:08:41 +0100 Subject: [PATCH 34/55] feature(workflow): create separate function for team with 404 handling --- app/components/errors/utils.ts | 2 +- app/modules/user/service.server.ts | 105 +++++++++++++++++- app/modules/user/types.ts | 6 +- .../_layout+/settings.team.users.$userId.tsx | 44 ++++---- 4 files changed, 128 insertions(+), 29 deletions(-) diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index 4b45330ca..8d3772d14 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; export const error404AdditionalData = z.object({ - model: z.enum(["asset", "kit", "location"]), + model: z.enum(["asset", "kit", "location", "user"]), id: z.string(), redirectTo: z.string().optional(), organization: z.object({ diff --git a/app/modules/user/service.server.ts b/app/modules/user/service.server.ts index 50cb91a1c..85f6fb213 100644 --- a/app/modules/user/service.server.ts +++ b/app/modules/user/service.server.ts @@ -1,4 +1,4 @@ -import type { Organization, User } from "@prisma/client"; +import type { Organization, User, UserOrganization } from "@prisma/client"; import { Prisma, Roles, OrganizationRoles } from "@prisma/client"; import type { ITXClientDenyList } from "@prisma/client/runtime/library"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; @@ -20,7 +20,7 @@ import { import { dateTimeInUnix } from "~/utils/date-time-in-unix"; import type { ErrorLabel } from "~/utils/error"; -import { ShelfError, isLikeShelfError } from "~/utils/error"; +import { ShelfError, isLikeShelfError, isNotFoundError } from "~/utils/error"; import type { ValidationError } from "~/utils/http"; import { getCurrentSearchParams } from "~/utils/http.server"; import { id as generateId } from "~/utils/id/id.server"; @@ -34,11 +34,12 @@ import { } from "~/utils/storage.server"; import { randomUsernameFromEmail } from "~/utils/user"; import { INCLUDE_SSO_DETAILS_VIA_USER_ORGANIZATION } from "./fields"; -import type { UpdateUserPayload } from "./types"; +import { type UpdateUserPayload, USER_STATIC_INCLUDE } from "./types"; import { defaultFields } from "../asset-index-settings/helpers"; import { defaultUserCategories } from "../category/default-categories"; import { getOrganizationsBySsoDomain } from "../organization/service.server"; import { createTeamMember } from "../team-member/service.server"; +import { MergeInclude } from "~/utils/utils"; const label: ErrorLabel = "User"; @@ -1238,3 +1239,101 @@ export async function transferEntitiesToNewOwner({ }, }); } + +type UserWithExtraInclude = + T extends Prisma.UserInclude + ? Prisma.UserGetPayload<{ + include: MergeInclude; + }> + : Prisma.UserGetPayload<{ include: typeof USER_STATIC_INCLUDE }>; + +export async function getUserFromOrg({ + id, + organizationId, + userOrganizations, + request, + extraInclude, +}: Pick & { + organizationId: Organization["id"]; + userOrganizations?: Pick[]; + request?: Request; + extraInclude?: T; +}) { + try { + const otherOrganizationIds = userOrganizations?.map( + (org) => org.organizationId + ); + + const mergedInclude = { + ...USER_STATIC_INCLUDE, + ...extraInclude, + } as MergeInclude; + + const user = (await db.user.findFirstOrThrow({ + where: { + OR: [ + { id, userOrganizations: { some: { organizationId } } }, + ...(userOrganizations?.length + ? [ + { + id, + userOrganizations: { + some: { organizationId: { in: otherOrganizationIds } }, + }, + }, + ] + : []), + ], + }, + include: mergedInclude, + })) as UserWithExtraInclude; + + /* User is accessing the User in the wrong organization */ + const isUserInCurrentOrg = !!user.userOrganizations.find( + (userOrg) => userOrg.organizationId === organizationId + ); + + const otherOrgOfUser = userOrganizations?.find( + (org) => + !!user.userOrganizations.find( + (userOrg) => userOrg.organizationId === org.organizationId + ) + ); + + if (userOrganizations?.length && !isUserInCurrentOrg && !!otherOrgOfUser) { + const redirectTo = + typeof request !== "undefined" + ? new URL(request.url).pathname + : undefined; + + throw new ShelfError({ + cause: null, + title: "User not found", + message: "", + additionalData: { + model: "user", + organization: otherOrgOfUser, + redirectTo, + }, + label, + status: 404, + }); + } + + return user; + } catch (cause) { + throw new ShelfError({ + cause, + title: "User not found.", + message: + "The user you are trying to access does not exists or you do not have permission to access it.", + additionalData: { + id, + organizationId, + ...(isLikeShelfError(cause) ? cause.additionalData : {}), + }, + label, + shouldBeCaptured: !isNotFoundError(cause), + }); + } +} diff --git a/app/modules/user/types.ts b/app/modules/user/types.ts index e15dbfcdd..ad75f9488 100644 --- a/app/modules/user/types.ts +++ b/app/modules/user/types.ts @@ -1,4 +1,4 @@ -import type { User } from "@prisma/client"; +import type { Prisma, User } from "@prisma/client"; export interface UpdateUserPayload { id: User["id"]; @@ -24,3 +24,7 @@ export interface UpdateUserResponse { /** Used when sending a pwd reset link for the user */ passwordReset?: boolean; } + +export const USER_STATIC_INCLUDE = { + userOrganizations: true, +} satisfies Prisma.UserInclude; diff --git a/app/routes/_layout+/settings.team.users.$userId.tsx b/app/routes/_layout+/settings.team.users.$userId.tsx index 833b08cde..394a52ee7 100644 --- a/app/routes/_layout+/settings.team.users.$userId.tsx +++ b/app/routes/_layout+/settings.team.users.$userId.tsx @@ -13,7 +13,7 @@ import { Badge } from "~/components/shared/badge"; import { Button } from "~/components/shared/button"; import When from "~/components/when/when"; import { TeamUsersActionsDropdown } from "~/components/workspace/users-actions-dropdown"; -import { getUserByID } from "~/modules/user/service.server"; +import { getUserFromOrg } from "~/modules/user/service.server"; import { resolveUserAction } from "~/modules/user/utils.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { makeShelfError } from "~/utils/error"; @@ -33,12 +33,13 @@ export const loader = async ({ const authSession = context.getSession(); const { userId } = authSession; try { - const { currentOrganization, organizationId } = await requirePermission({ - userId, - request, - entity: PermissionEntity.teamMemberProfile, - action: PermissionAction.read, - }); + const { currentOrganization, organizationId, userOrganizations } = + await requirePermission({ + userId, + request, + entity: PermissionEntity.teamMemberProfile, + action: PermissionAction.read, + }); const { userId: selectedUserId } = getParams( params, @@ -48,28 +49,23 @@ export const loader = async ({ } ); - const user = await getUserByID(selectedUserId, { - userOrganizations: { - where: { - organizationId, - }, - select: { - roles: true, - }, - }, - teamMembers: { - where: { - organizationId, - }, - include: { - receivedInvites: { - where: { - organizationId, + const user = await getUserFromOrg({ + id: selectedUserId, + organizationId, + userOrganizations, + request, + extraInclude: { + teamMembers: { + where: { organizationId }, + include: { + receivedInvites: { + where: { organizationId }, }, }, }, }, }); + const userName = (user.firstName ? user.firstName.trim() : "") + " " + From 0ea567a8dae8f286b32153dde3b60f2794ac5fe6 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 26 Nov 2024 20:42:22 +0100 Subject: [PATCH 35/55] feat(improve-404): handling special case of multiple organization for team member --- app/components/errors/error-404-handler.tsx | 140 +++++++++++++++----- app/components/errors/utils.ts | 31 +++-- app/modules/user/service.server.ts | 23 ++-- 3 files changed, 144 insertions(+), 50 deletions(-) diff --git a/app/components/errors/error-404-handler.tsx b/app/components/errors/error-404-handler.tsx index 4b91e4165..17ca763dc 100644 --- a/app/components/errors/error-404-handler.tsx +++ b/app/components/errors/error-404-handler.tsx @@ -1,8 +1,16 @@ +import { useMemo } from "react"; import { tw } from "~/utils/tw"; import { Error404AdditionalData } from "./utils"; import { Button } from "../shared/button"; import { useFetcher } from "@remix-run/react"; import { isFormProcessing } from "~/utils/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../forms/select"; export type Error404HandlerProps = { className?: string; @@ -18,44 +26,110 @@ export default function Error404Handler({ const fetcher = useFetcher(); const disabled = isFormProcessing(fetcher.state); + const content = useMemo(() => { + switch (additionalData.model) { + case "asset": + case "kit": + case "location": { + return ( +
    +
    +

    + {additionalData.model}{" "} + belongs to another workspace. +

    +

    + The {additionalData.model} you are trying to view belongs to a + different workspace you are part of. Would you like to switch to + workspace{" "} + + "{additionalData.organization.organization.name}" + {" "} + to view the {additionalData.model}? +

    + + + + + +
    +
    + ); + } + + case "teamMember": { + return ( +
    +
    +

    + Team Member belongs to + another workspace(s). +

    +

    + The team member you are trying to view belongs to one/some of + your different workspace you are part of. Would you like to + switch to workspace to view the team member? +

    + + + + + +
    +
    + ); + } + + default: { + return null; + } + } + }, [additionalData]); + return (
    -
    -
    -

    - {additionalData.model} belongs - to another workspace. -

    -

    - The {additionalData.model} you are trying to view belongs to a - different workspace you are part of. Would you like to switch to - workspace{" "} - - "{additionalData.organization.organization.name}" - {" "} - to view the {additionalData.model}? -

    - - - - - -
    -
    + {content}
    ); } diff --git a/app/components/errors/utils.ts b/app/components/errors/utils.ts index 8d3772d14..c075c9232 100644 --- a/app/components/errors/utils.ts +++ b/app/components/errors/utils.ts @@ -1,19 +1,34 @@ import { z } from "zod"; import { isRouteError } from "~/utils/http"; -export const error404AdditionalData = z.object({ - model: z.enum(["asset", "kit", "location", "user"]), +const baseAdditionalDataSchema = z.object({ id: z.string(), redirectTo: z.string().optional(), +}); + +const organizationSchema = z.object({ organization: z.object({ - organization: z.object({ - id: z.string(), - name: z.string(), - }), + id: z.string(), + name: z.string(), }), }); -export type Error404AdditionalData = z.infer; +export const error404AdditionalDataSchema = z.discriminatedUnion("model", [ + /* For common and general use case */ + baseAdditionalDataSchema.extend({ + model: z.enum(["asset", "kit", "location"]), + organization: organizationSchema, + }), + /* A team member (user) can be in multiple organization's of user so we do this. */ + baseAdditionalDataSchema.extend({ + model: z.literal("teamMember"), + organizations: organizationSchema.array(), + }), +]); + +export type Error404AdditionalData = z.infer< + typeof error404AdditionalDataSchema +>; export function parse404ErrorData(response: unknown): | { isError404: false; additionalData: null } @@ -25,7 +40,7 @@ export function parse404ErrorData(response: unknown): return { isError404: false, additionalData: null }; } - const parsedDataResponse = error404AdditionalData.safeParse( + const parsedDataResponse = error404AdditionalDataSchema.safeParse( response.data.error.additionalData ); diff --git a/app/modules/user/service.server.ts b/app/modules/user/service.server.ts index 85f6fb213..5846962f7 100644 --- a/app/modules/user/service.server.ts +++ b/app/modules/user/service.server.ts @@ -1293,14 +1293,19 @@ export async function getUserFromOrg({ (userOrg) => userOrg.organizationId === organizationId ); - const otherOrgOfUser = userOrganizations?.find( - (org) => - !!user.userOrganizations.find( - (userOrg) => userOrg.organizationId === org.organizationId - ) - ); + const otherOrgsForUser = + userOrganizations?.filter( + (org) => + !!user.userOrganizations.find( + (userOrg) => userOrg.organizationId === org.organizationId + ) + ) ?? []; - if (userOrganizations?.length && !isUserInCurrentOrg && !!otherOrgOfUser) { + if ( + userOrganizations?.length && + !isUserInCurrentOrg && + otherOrgsForUser?.length + ) { const redirectTo = typeof request !== "undefined" ? new URL(request.url).pathname @@ -1311,8 +1316,8 @@ export async function getUserFromOrg({ title: "User not found", message: "", additionalData: { - model: "user", - organization: otherOrgOfUser, + model: "teamMember", + organizations: otherOrgsForUser, redirectTo, }, label, From 6d63a76f8eb61990e6966e5fae4f169e3c8a646e Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Wed, 27 Nov 2024 11:45:36 +0100 Subject: [PATCH 36/55] feat(improve-404): fix style issue on ErrorBoundary for team settings --- app/components/errors/error-404-handler.tsx | 2 +- app/components/errors/index.tsx | 19 ++++++++++++++++--- .../_layout+/settings.team.users.$userId.tsx | 17 ++++++++++++----- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/components/errors/error-404-handler.tsx b/app/components/errors/error-404-handler.tsx index 17ca763dc..155331a99 100644 --- a/app/components/errors/error-404-handler.tsx +++ b/app/components/errors/error-404-handler.tsx @@ -88,7 +88,7 @@ export default function Error404Handler({ > - ); -}); +>(({ className, ...props }, ref) => ( + +)); SidebarInput.displayName = "SidebarInput"; const SidebarHeader = forwardRef>( - ({ className, ...props }, ref) => { - return ( -
    - ); - } + ({ className, ...props }, ref) => ( +
    + ) ); SidebarHeader.displayName = "SidebarHeader"; const SidebarFooter = forwardRef>( - ({ className, ...props }, ref) => { - return ( -
    - ); - } + ({ className, ...props }, ref) => ( +
    + ) ); SidebarFooter.displayName = "SidebarFooter"; const SidebarSeparator = forwardRef< React.ElementRef, React.ComponentProps ->(({ className, ...props }, ref) => { - return ( - - ); -}); +>(({ className, ...props }, ref) => ( + +)); SidebarSeparator.displayName = "SidebarSeparator"; const SidebarContent = forwardRef>( - ({ className, ...props }, ref) => { - return ( -
    - ); - } + ({ className, ...props }, ref) => ( +
    + ) ); SidebarContent.displayName = "SidebarContent"; const SidebarGroup = forwardRef>( - ({ className, ...props }, ref) => { - return ( -
    - ); - } + ({ className, ...props }, ref) => ( +
    + ) ); SidebarGroup.displayName = "SidebarGroup"; @@ -464,7 +453,7 @@ const SidebarGroupLabel = forwardRef< ref={ref} data-sidebar="group-label" className={tw( - "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-primary-500 transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-sidebar-foreground/70 outline-none ring-primary-500 transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", className )} @@ -527,7 +516,7 @@ const SidebarMenuItem = forwardRef>(
  • ) @@ -535,11 +524,11 @@ const SidebarMenuItem = forwardRef>( SidebarMenuItem.displayName = "SidebarMenuItem"; const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-primary-500 transition-[width,height,padding] hover:text-primary-500 hover:bg-primary-25 focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-primary-500 transition-[width,height,padding] focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-semibold data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: { - default: "hover:text-primary-500 hover:bg-primary-25", + default: " hover:bg-gray-100", outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", }, @@ -629,7 +618,7 @@ const SidebarMenuAction = forwardRef< ref={ref} data-sidebar="menu-action" className={tw( - "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-primary-500 transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", + "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-primary-500 transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", // Increases the hit area of the button on mobile. "after:absolute after:-inset-2 after:md:hidden", "peer-data-[size=sm]/menu-button:top-1", @@ -654,7 +643,7 @@ const SidebarMenuBadge = forwardRef< ref={ref} data-sidebar="menu-badge" className={tw( - "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none", + "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-semibold tabular-nums text-sidebar-foreground", "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-1.5", @@ -674,15 +663,13 @@ const SidebarMenuSkeleton = forwardRef< } >(({ className, showIcon = false, ...props }, ref) => { // Random width between 50 to 90%. - const width = useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); + const width = useMemo(() => `${Math.floor(Math.random() * 40) + 50}%`, []); return (
    {showIcon && ( @@ -692,7 +679,7 @@ const SidebarMenuSkeleton = forwardRef< /> )} span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-primary-500 aria-disabled:pointer-events-none aria-disabled:opacity-50 focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", size === "sm" && "text-xs", size === "md" && "text-sm", diff --git a/app/hooks/use-main-menu-items.tsx b/app/hooks/use-main-menu-items.tsx index 742ab1d96..bd5abd503 100644 --- a/app/hooks/use-main-menu-items.tsx +++ b/app/hooks/use-main-menu-items.tsx @@ -54,12 +54,16 @@ export function useMainMenuItems() { }, ]; let menuItemsBottom = [ + { + icon: , + to: "settings", + title: "Workspace settings", + end: true, + }, { icon: , - to: `https://www.shelf.nu/order-tags?email=${user?.email}${ - user?.firstName ? `&firstName=${user.firstName}` : "" - }${user?.lastName ? `&lastName=${user.lastName}` : ""}`, - title: "Asset labels", + to: `https://store.shelf.nu/?ref=app_sidebar`, + title: "Buy Asset labels", target: "_blank", isNew: true, }, @@ -69,12 +73,6 @@ export function useMainMenuItems() { title: "QR scanner", end: true, }, - { - icon: , - to: "settings", - title: "Workspace settings", - end: true, - }, ]; if (isBaseOrSelfService) { diff --git a/app/hooks/use-sidebar-nav-items.tsx b/app/hooks/use-sidebar-nav-items.tsx index 4ab66c13c..52af46a93 100644 --- a/app/hooks/use-sidebar-nav-items.tsx +++ b/app/hooks/use-sidebar-nav-items.tsx @@ -1,11 +1,11 @@ -import { useUserData } from "./use-user-data"; +import type { IconType } from "~/components/shared/icons-map"; import { useUserRoleHelper } from "./user-user-role-helper"; -import { IconType } from "~/components/shared/icons-map"; type BaseNavItem = { title: string; hidden?: boolean; icon: IconType; + defaultOpen?: boolean; }; type ChildNavItem = BaseNavItem & { @@ -22,7 +22,6 @@ type ParentNavItem = BaseNavItem & { export type NavItem = ChildNavItem | ParentNavItem; export function useSidebarNavItems() { - const user = useUserData(); const { isBaseOrSelfService } = useUserRoleHelper(); const topMenuItems: NavItem[] = [ @@ -99,24 +98,6 @@ export function useSidebarNavItems() { }, ], }, - ]; - - const bottomMenuItems: NavItem[] = [ - { - type: "child", - title: "Asset labels", - to: `https://www.shelf.nu/order-tags?email=${user?.email}${ - user?.firstName ? `&firstName=${user.firstName}` : "" - }${user?.lastName ? `&lastName=${user.lastName}` : ""}`, - icon: "asset-label", - target: "_blank", - }, - { - type: "child", - title: "QR Scanner", - to: "/scanner", - icon: "scanQR", - }, { type: "parent", title: "Workspace settings", @@ -139,6 +120,22 @@ export function useSidebarNavItems() { }, ]; + const bottomMenuItems: NavItem[] = [ + { + type: "child", + title: "Asset labels", + to: `https://store.shelf.nu/?ref=shelf_webapp_sidebar`, + icon: "asset-label", + target: "_blank", + }, + { + type: "child", + title: "QR Scanner", + to: "/scanner", + icon: "scanQR", + }, + ]; + return { topMenuItems: topMenuItems.filter((item) => !item.hidden), bottomMenuItems: bottomMenuItems.filter((item) => !item.hidden), diff --git a/app/routes/_layout+/settings.team.users.$userId.tsx b/app/routes/_layout+/settings.team.users.$userId.tsx index 929f740aa..77520b80f 100644 --- a/app/routes/_layout+/settings.team.users.$userId.tsx +++ b/app/routes/_layout+/settings.team.users.$userId.tsx @@ -5,6 +5,7 @@ import type { } from "@remix-run/node"; import { json, Outlet, useLoaderData } from "@remix-run/react"; import { z } from "zod"; +import { ErrorContent } from "~/components/errors"; import Header from "~/components/layout/header"; import { AbsolutePositionedHeaderActions } from "~/components/layout/header/absolute-positioned-header-actions"; import HorizontalTabs from "~/components/layout/horizontal-tabs"; @@ -24,7 +25,6 @@ import { } from "~/utils/permissions/permission.data"; import { requirePermission } from "~/utils/roles.server"; import { organizationRolesMap } from "./settings.team"; -import { ErrorContent } from "~/components/errors"; export const loader = async ({ request, diff --git a/app/tailwind.css b/app/tailwind.css index fd9f66f64..994aca140 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -4,7 +4,7 @@ @layer base { :root { - --sidebar-background: 0 0% 98%; + --sidebar-background: theme(colors.white); --sidebar-foreground: 240 5.3% 26.1%; --sidebar-primary: 240 5.9% 10%; --sidebar-primary-foreground: 0 0% 98%; From 873df4dae84c7a7a6a37d4cdafb61d84bbe718ef Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 29 Nov 2024 17:57:18 +0100 Subject: [PATCH 39/55] refactor(sidebar): remove unsed files --- app/components/layout/sidebar/atoms.ts | 31 --- app/components/layout/sidebar/bottom.tsx | 130 ------------ app/components/layout/sidebar/menu-button.tsx | 31 --- app/components/layout/sidebar/menu-items.tsx | 199 ------------------ app/components/layout/sidebar/notice-card.tsx | 53 ----- .../sidebar/organization-select-form.tsx | 51 ----- .../layout/sidebar/organization-selector.tsx | 2 +- app/components/layout/sidebar/overlay.tsx | 18 -- app/components/layout/sidebar/sidebar-old.tsx | 99 --------- app/hooks/use-main-menu-items.tsx | 100 --------- app/routes/_layout+/_layout.tsx | 4 +- 11 files changed, 3 insertions(+), 715 deletions(-) delete mode 100644 app/components/layout/sidebar/atoms.ts delete mode 100644 app/components/layout/sidebar/bottom.tsx delete mode 100644 app/components/layout/sidebar/menu-button.tsx delete mode 100644 app/components/layout/sidebar/menu-items.tsx delete mode 100644 app/components/layout/sidebar/notice-card.tsx delete mode 100644 app/components/layout/sidebar/organization-select-form.tsx delete mode 100644 app/components/layout/sidebar/overlay.tsx delete mode 100644 app/components/layout/sidebar/sidebar-old.tsx delete mode 100644 app/hooks/use-main-menu-items.tsx diff --git a/app/components/layout/sidebar/atoms.ts b/app/components/layout/sidebar/atoms.ts deleted file mode 100644 index 9b93b6bb2..000000000 --- a/app/components/layout/sidebar/atoms.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { atom } from "jotai"; - -export const sidebarCollapseStatusAtom = atom(false); - -export const keepSidebarUncollapsedAtom = atom(true); - -/* Controls the state for whether the sidebar is collapsed or not */ -export const toggleSidebarAtom = atom( - (get) => get(sidebarCollapseStatusAtom), - (get, set) => - !get(keepSidebarUncollapsedAtom) - ? set(sidebarCollapseStatusAtom, !get(sidebarCollapseStatusAtom)) - : null -); - -/* Controls the state for whether the sidebar uncollapsed state will be maintained or not */ -export const maintainUncollapsedAtom = atom( - (get) => get(keepSidebarUncollapsedAtom), - (get, set) => { - set(keepSidebarUncollapsedAtom, !get(keepSidebarUncollapsedAtom)); - set(sidebarCollapseStatusAtom, !get(keepSidebarUncollapsedAtom)); - } -); - -/* Using different atoms for mobile navigation sidebar as they conflict with desktop's sidebar atoms state*/ -export const isMobileNavOpenAtom = atom(false); - -export const toggleMobileNavAtom = atom( - (get) => get(isMobileNavOpenAtom), - (get, set) => set(isMobileNavOpenAtom, !get(isMobileNavOpenAtom)) -); diff --git a/app/components/layout/sidebar/bottom.tsx b/app/components/layout/sidebar/bottom.tsx deleted file mode 100644 index b72e72825..000000000 --- a/app/components/layout/sidebar/bottom.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useState } from "react"; -import type { User } from "@prisma/client"; -import { useAtom } from "jotai"; -import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; -import { Form } from "~/components/custom-form"; -import { ChevronRight, QuestionsIcon } from "~/components/icons/library"; -import { CrispButton } from "~/components/marketing/crisp"; -import { Button } from "~/components/shared/button"; -import { - DropdownMenuItem, - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "~/components/shared/dropdown"; -import ProfilePicture from "~/components/user/profile-picture"; -import { tw } from "~/utils/tw"; - -interface Props { - user: Pick; -} - -export default function SidebarBottom({ user }: Props) { - const [dropdownOpen, setDropdownOpen] = useState(false); - const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); - - return ( -
    - - setDropdownOpen((prev) => !prev)} - className="w-full truncate outline-none focus-visible:border-0" - > -
    - -
    -
    - {user.username} -
    -
    - {user.email} -
    -
    -
    - - - -
    -
    -
    - - - - - - e.preventDefault()} - className="border-b border-gray-200 p-0" - > - - - - - - - Questions/Feedback - - - - - e.preventDefault()} - > -
    - -
    -
    -
    -
    -
    - ); -} diff --git a/app/components/layout/sidebar/menu-button.tsx b/app/components/layout/sidebar/menu-button.tsx deleted file mode 100644 index b939ccca7..000000000 --- a/app/components/layout/sidebar/menu-button.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useAtom } from "jotai"; -import { tw } from "~/utils/tw"; -import { toggleMobileNavAtom } from "./atoms"; - -const MenuButton = () => { - const [isMobileNavOpen, toggleMobileNav] = useAtom(toggleMobileNavAtom); - return ( - - ); -}; - -export default MenuButton; diff --git a/app/components/layout/sidebar/menu-items.tsx b/app/components/layout/sidebar/menu-items.tsx deleted file mode 100644 index 1fabe04b3..000000000 --- a/app/components/layout/sidebar/menu-items.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import type { FetcherWithComponents } from "@remix-run/react"; -import { - NavLink, - useLoaderData, - useLocation, - useMatches, -} from "@remix-run/react"; -import { motion } from "framer-motion"; -import { useAtom } from "jotai"; -import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; -import Icon from "~/components/icons/icon"; -import { UpgradeMessage } from "~/components/marketing/upgrade-message"; -import { Button } from "~/components/shared/button"; -import When from "~/components/when/when"; -import { useMainMenuItems } from "~/hooks/use-main-menu-items"; -import type { loader } from "~/routes/_layout+/_layout"; -import { tw } from "~/utils/tw"; -import { toggleMobileNavAtom } from "./atoms"; -import { SidebarNoticeCard } from "./notice-card"; - -const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents }) => { - const [, toggleMobileNav] = useAtom(toggleMobileNavAtom); - const { isAdmin, minimizedSidebar, canUseBookings, subscription } = - useLoaderData(); - const { menuItemsTop, menuItemsBottom } = useMainMenuItems(); - const location = useLocation(); - const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); - const matches = useMatches(); - const currentRoute = matches.at(-1); - // @ts-expect-error - const handle = currentRoute?.handle?.name; - return ( -
    -
    -
      - {isAdmin ? ( -
    • - - tw( - "my-1 flex items-center gap-3 rounded px-3 py-2.5 text-[16px] font-semibold text-gray-700 transition-all duration-75 hover:bg-primary-50 hover:text-primary-600", - isActive ? "active bg-primary-50 text-primary-600" : "", - workspaceSwitching ? "pointer-events-none" : "" - ) - } - to={"/admin-dashboard/users"} - onClick={toggleMobileNav} - title={"Admin dashboard"} - > - 🛸 - - Admin dashboard - - -
      -
    • - ) : null} - - {menuItemsTop.map((item) => - item.to === "bookings" || item.to === "calendar" ? ( -
    • - -
    • - ) : ( -
    • - ) - )} -
    - -
    - {/* Sidebar notice card component will be visible when uncollapsed sidebar is selected and hidden when minimizing sidebar form is processing */} - {fetcher.state == "idle" ? ( - - - - ) : null} -
      - {menuItemsBottom.map((item) => ( -
    • - - tw( - " my-1 flex items-center gap-3 rounded px-3 py-2.5 text-[16px] font-semibold text-gray-700 transition-all duration-75 hover:bg-primary-50 hover:text-primary-600", - isActive && handle !== "settings.team.users" - ? "active bg-primary-50 text-primary-600" - : "", - workspaceSwitching ? "pointer-events-none" : "" - ) - } - to={item.to} - data-test-id={`${item.title.toLowerCase()}SidebarMenuItem`} - onClick={toggleMobileNav} - title={item.title} - target={item?.target || undefined} - > - - {item.icon} - - - {item.title} - - - - New - - - -
    • - ))} -
    • - - - - -
    • -
    -
    -
    -
    - ); -}; - -export default MenuItems; diff --git a/app/components/layout/sidebar/notice-card.tsx b/app/components/layout/sidebar/notice-card.tsx deleted file mode 100644 index fed91f4cb..000000000 --- a/app/components/layout/sidebar/notice-card.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useFetcher, useLoaderData } from "@remix-run/react"; -import { XIcon } from "~/components/icons/library"; -import { Button } from "~/components/shared/button"; -import type { loader } from "~/routes/_layout+/_layout"; - -export const SidebarNoticeCard = () => { - const { hideNoticeCard } = useLoaderData(); - const fetcher = useFetcher(); - - let optimisticHideNoticeCard = hideNoticeCard; - if (fetcher.formData) { - optimisticHideNoticeCard = - fetcher.formData.get("noticeCardVisibility") === "hidden"; - } - - return optimisticHideNoticeCard ? null : ( -
    -
    -
    - Install Shelf for Mobile -
    -
    - - - - -
    -
    - -

    - Always available access to shelf, with all features you have on desktop. -

    - Carlos support shelf.nu -

    - -

    -
    - ); -}; diff --git a/app/components/layout/sidebar/organization-select-form.tsx b/app/components/layout/sidebar/organization-select-form.tsx deleted file mode 100644 index 265f970c3..000000000 --- a/app/components/layout/sidebar/organization-select-form.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useEffect } from "react"; -import { useFetcher } from "@remix-run/react"; -import { useAtom } from "jotai"; -import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; - -import { SelectSeparator } from "~/components/forms/select"; -import { Button } from "~/components/shared/button"; -import { isFormProcessing } from "~/utils/form"; -import { OrganizationSelect } from "./organization-select"; - -export const OrganizationSelectForm = () => { - const fetcher = useFetcher(); - - /** We track if the fetcher is submitting */ - const isProcessing = isFormProcessing(fetcher.state); - const [_workspaceSwitching, setWorkspaceSwitching] = useAtom( - switchingWorkspaceAtom - ); - useEffect(() => { - setWorkspaceSwitching(isProcessing); - }, [isProcessing, setWorkspaceSwitching]); - - return ( - { - const form = e.currentTarget; - fetcher.submit(form); - }} - > - - - - - ), - }} - /> - - ); -}; diff --git a/app/components/layout/sidebar/organization-selector.tsx b/app/components/layout/sidebar/organization-selector.tsx index c4f8f9037..7f8859c70 100644 --- a/app/components/layout/sidebar/organization-selector.tsx +++ b/app/components/layout/sidebar/organization-selector.tsx @@ -64,7 +64,7 @@ export default function OrganizationSelector() { function setSwitchingWorkspaceAfterFetch() { setWorkspaceSwitching(isSwitchingOrg); }, - [isFormProcessing, setWorkspaceSwitching] + [isSwitchingOrg, setWorkspaceSwitching] ); return ( diff --git a/app/components/layout/sidebar/overlay.tsx b/app/components/layout/sidebar/overlay.tsx deleted file mode 100644 index 015c654c7..000000000 --- a/app/components/layout/sidebar/overlay.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useAtom } from "jotai"; -import { tw } from "~/utils/tw"; -import { toggleMobileNavAtom } from "./atoms"; - -const Overlay = () => { - const [isMobileNavOpen, toggleMobileNav] = useAtom(toggleMobileNavAtom); - return ( -
    - ); -}; - -export default Overlay; diff --git a/app/components/layout/sidebar/sidebar-old.tsx b/app/components/layout/sidebar/sidebar-old.tsx deleted file mode 100644 index b32d72635..000000000 --- a/app/components/layout/sidebar/sidebar-old.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useRef } from "react"; -import { Link, NavLink, useFetcher, useLoaderData } from "@remix-run/react"; -import { useAtom } from "jotai"; -import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; -import { ScanQRIcon } from "~/components/icons/library"; -import type { loader } from "~/routes/_layout+/_layout"; -import { tw } from "~/utils/tw"; - -import { toggleMobileNavAtom } from "./atoms"; -import SidebarBottom from "./bottom"; -import MenuButton from "./menu-button"; -import MenuItems from "./menu-items"; -import { OrganizationSelectForm } from "./organization-select-form"; -import Overlay from "./overlay"; -import { ShelfMobileLogo, ShelfSidebarLogo } from "../../marketing/logos"; - -export default function Sidebar() { - const { user, minimizedSidebar, currentOrganizationId } = - useLoaderData(); - const [isMobileNavOpen, toggleMobileNav] = useAtom(toggleMobileNavAtom); - const mainNavigationRef = useRef(null); - - /** We use optimistic UI for folding of the scanner.tsx sidebar - * As we are making a request to the server to store the cookie, - * we need to use this approach, otherwise the sidebar will close/open - * only once the response is received from the server - */ - const sidebarFetcher = useFetcher(); - let optimisticMinimizedSidebar = minimizedSidebar; - if (sidebarFetcher.formData) { - optimisticMinimizedSidebar = - sidebarFetcher.formData.get("minimizeSidebar") === "open"; - } - const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); - return ( - <> - {/* this component is named sidebar as of now but also serves as a mobile navigation header in mobile device */} - - - - - - ); -} diff --git a/app/hooks/use-main-menu-items.tsx b/app/hooks/use-main-menu-items.tsx deleted file mode 100644 index bd5abd503..000000000 --- a/app/hooks/use-main-menu-items.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import Icon from "~/components/icons/icon"; -import { useUserData } from "./use-user-data"; -import { useUserRoleHelper } from "./user-user-role-helper"; - -export function useMainMenuItems() { - const user = useUserData(); - const { isBaseOrSelfService } = useUserRoleHelper(); - - let menuItemsTop = [ - { - icon: , - to: "dashboard", - title: "Dashboard", - }, - { - icon: , - to: "assets", - title: "Assets", - }, - { - icon: , - to: "kits", - title: "Kits", - }, - { - icon: , - to: "categories", - title: "Categories", - }, - { - icon: , - to: "tags", - title: "Tags", - }, - { - icon: , - to: "locations", - title: "Locations", - }, - { - icon: , - to: "calendar", - title: "Calendar", - }, - { - icon: , - to: "bookings", - title: "Bookings", - }, - { - icon: , - to: "/settings/team", - title: "Team", - }, - ]; - let menuItemsBottom = [ - { - icon: , - to: "settings", - title: "Workspace settings", - end: true, - }, - { - icon: , - to: `https://store.shelf.nu/?ref=app_sidebar`, - title: "Buy Asset labels", - target: "_blank", - isNew: true, - }, - { - icon: , - to: "scanner", - title: "QR scanner", - end: true, - }, - ]; - - if (isBaseOrSelfService) { - /** Deleting the Dashboard menu item as its not needed for self_service users. */ - const itemsToRemove = [ - "dashboard", - "categories", - "tags", - "locations", - "settings", - "/settings/team", - ]; - menuItemsTop = menuItemsTop.filter( - (item) => !itemsToRemove.includes(item.to) - ); - menuItemsBottom = menuItemsBottom.filter( - (item) => !itemsToRemove.includes(item.to) - ); - } - - return { - menuItemsTop, - menuItemsBottom, - }; -} diff --git a/app/routes/_layout+/_layout.tsx b/app/routes/_layout+/_layout.tsx index 7c84d1e38..1b94cee89 100644 --- a/app/routes/_layout+/_layout.tsx +++ b/app/routes/_layout+/_layout.tsx @@ -2,7 +2,7 @@ import { Roles } from "@prisma/client"; import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Outlet, useLoaderData } from "@remix-run/react"; -import { useAtom } from "jotai"; +import { useAtomValue } from "jotai"; import { ClientOnly } from "remix-utils/client-only"; import { switchingWorkspaceAtom } from "~/atoms/switching-workspace"; import { ErrorContent } from "~/components/errors"; @@ -136,7 +136,7 @@ export async function loader({ context, request }: LoaderFunctionArgs) { export default function App() { useCrisp(); const { disabledTeamOrg, minimizedSidebar } = useLoaderData(); - const [workspaceSwitching] = useAtom(switchingWorkspaceAtom); + const workspaceSwitching = useAtomValue(switchingWorkspaceAtom); const renderInstallPwaPromptOnMobile = () => // returns InstallPwaPromptModal if the device width is lesser than 640px and the app is being accessed from browser not PWA From b7c064298d80d79654b1de24fd30d6486108076b Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 29 Nov 2024 18:03:21 +0100 Subject: [PATCH 40/55] refactor(sidebar): fix eslint issues --- .../assets/assets-index/asset-index-pagination.tsx | 7 +++---- app/components/errors/error-404-handler.tsx | 12 ++++++------ app/components/errors/index.tsx | 8 ++++---- app/components/shared/separator.tsx | 6 +++--- app/components/shared/sheet.tsx | 10 +++++----- app/modules/booking/service.server.ts | 2 +- app/modules/user/service.server.ts | 2 +- app/root.tsx | 4 +--- app/routes/_layout+/_layout.tsx | 2 +- 9 files changed, 25 insertions(+), 28 deletions(-) diff --git a/app/components/assets/assets-index/asset-index-pagination.tsx b/app/components/assets/assets-index/asset-index-pagination.tsx index 18c53f3c5..abf869ea8 100644 --- a/app/components/assets/assets-index/asset-index-pagination.tsx +++ b/app/components/assets/assets-index/asset-index-pagination.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; -import { useFetcher, useRouteLoaderData } from "@remix-run/react"; +import { useFetcher } from "@remix-run/react"; import { AlertIcon, ChevronRight } from "~/components/icons/library"; +import { useSidebar } from "~/components/layout/sidebar/sidebar"; import { AlertDialog, AlertDialogCancel, @@ -16,7 +17,6 @@ import { useAssetIndexViewState } from "~/hooks/use-asset-index-view-state"; import { useViewportHeight } from "~/hooks/use-viewport-height"; import { useUserRoleHelper } from "~/hooks/user-user-role-helper"; -import type { loader as layoutLoader } from "~/routes/_layout+/_layout"; import { PermissionAction, PermissionEntity, @@ -26,7 +26,6 @@ import { tw } from "~/utils/tw"; import { Pagination } from "../../list/pagination"; import { Button } from "../../shared/button"; import { ButtonGroup } from "../../shared/button-group"; -import { useSidebar } from "~/components/layout/sidebar/sidebar"; export function AssetIndexPagination() { const { roles } = useUserRoleHelper(); @@ -56,7 +55,7 @@ export function AssetIndexPagination() { return (
    + + +
    +
    + +

    + Always available access to shelf, with all features you have on desktop. +

    + Carlos support shelf.nu +

    + +

    +
  • + ); +}; From 725515d130b7eaa52808e97a90bb9efc2cf34ac6 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Mon, 9 Dec 2024 16:11:34 +0100 Subject: [PATCH 53/55] refactor(sidebar): update use-mobile hook --- app/hooks/use-mobile.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/hooks/use-mobile.ts b/app/hooks/use-mobile.ts index 081e23da6..b024d97b8 100644 --- a/app/hooks/use-mobile.ts +++ b/app/hooks/use-mobile.ts @@ -6,17 +6,21 @@ export function useIsMobile() { const [isMobile, setIsMobile] = useState(undefined); useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + let mql: MediaQueryList; - const onChange = () => { + if (window) { + mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + + mql.addEventListener("change", onChange); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; + } - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + function onChange() { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + } return () => { - mql.removeEventListener("change", onChange); + mql?.removeEventListener("change", onChange); }; }, []); From 2049e6afd770c4d632bdf7ec81a1f0ed6b997d10 Mon Sep 17 00:00:00 2001 From: Nikolay Bonev Date: Tue, 10 Dec 2024 11:43:31 +0200 Subject: [PATCH 54/55] ui fixes --- app/components/layout/sidebar/app-sidebar.tsx | 2 +- app/components/layout/sidebar/notice-card.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/layout/sidebar/app-sidebar.tsx b/app/components/layout/sidebar/app-sidebar.tsx index 664999ecd..998554955 100644 --- a/app/components/layout/sidebar/app-sidebar.tsx +++ b/app/components/layout/sidebar/app-sidebar.tsx @@ -31,10 +31,10 @@ export default function AppSidebar(props: AppSidebarProps) { - + diff --git a/app/components/layout/sidebar/notice-card.tsx b/app/components/layout/sidebar/notice-card.tsx index 3f6d3646f..9ee7ea1f5 100644 --- a/app/components/layout/sidebar/notice-card.tsx +++ b/app/components/layout/sidebar/notice-card.tsx @@ -6,6 +6,7 @@ import type { loader } from "~/routes/_layout+/_layout"; export const SidebarNoticeCard = () => { const { hideNoticeCard } = useLoaderData(); const fetcher = useFetcher(); + console.log("hideNoticeCard", hideNoticeCard); let optimisticHideNoticeCard = hideNoticeCard; if (fetcher.formData) { @@ -13,8 +14,8 @@ export const SidebarNoticeCard = () => { fetcher.formData.get("noticeCardVisibility") === "hidden"; } - return !optimisticHideNoticeCard ? null : ( -
    + return optimisticHideNoticeCard ? null : ( +
    Install Shelf for Mobile From 1756f7bf459469c46e709c1c38f77d803acde216 Mon Sep 17 00:00:00 2001 From: Nikolay Bonev Date: Tue, 10 Dec 2024 11:46:35 +0200 Subject: [PATCH 55/55] mend --- app/components/layout/sidebar/notice-card.tsx | 1 - app/components/layout/sidebar/sidebar.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/layout/sidebar/notice-card.tsx b/app/components/layout/sidebar/notice-card.tsx index 9ee7ea1f5..5e06faf79 100644 --- a/app/components/layout/sidebar/notice-card.tsx +++ b/app/components/layout/sidebar/notice-card.tsx @@ -6,7 +6,6 @@ import type { loader } from "~/routes/_layout+/_layout"; export const SidebarNoticeCard = () => { const { hideNoticeCard } = useLoaderData(); const fetcher = useFetcher(); - console.log("hideNoticeCard", hideNoticeCard); let optimisticHideNoticeCard = hideNoticeCard; if (fetcher.formData) { diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index a3402b5a3..1e215763d 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -498,7 +498,7 @@ const SidebarMenu = forwardRef>(
      )