From f6068f9f76c771a4f7900f37deb5a88435e26ef2 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 7 Oct 2025 12:53:49 -0400 Subject: [PATCH 1/8] WIP folder updates --- .../(dashboard)/[slug]/links/page-client.tsx | 31 ++- .../(dashboard)/[slug]/links/page.tsx | 19 +- apps/web/lib/folder/get-folder-or-throw.ts | 2 + apps/web/lib/folder/get-folders.ts | 1 + apps/web/lib/swr/use-folder-users.ts | 45 +++++ apps/web/lib/zod/schemas/folders.ts | 8 + apps/web/ui/folders/add-folder-form.tsx | 57 ++++-- apps/web/ui/folders/folder-actions.tsx | 176 ++++++++---------- apps/web/ui/folders/folder-info-panel.tsx | 144 ++++++++++++++ .../ui/folders/folder-permissions-panel.tsx | 19 +- apps/web/ui/layout/page-content/index.tsx | 87 ++------- .../page-content/page-content-header.tsx | 78 ++++++++ .../page-content-with-side-panel.tsx | 116 ++++++++++++ .../page-content/toggle-side-panel-button.tsx | 66 +++++++ apps/web/ui/links/links-toolbar.tsx | 2 +- apps/web/ui/shared/max-characters-counter.tsx | 2 +- packages/prisma/schema/folder.prisma | 1 + 17 files changed, 631 insertions(+), 223 deletions(-) create mode 100644 apps/web/lib/swr/use-folder-users.ts create mode 100644 apps/web/ui/folders/folder-info-panel.tsx create mode 100644 apps/web/ui/layout/page-content/page-content-header.tsx create mode 100644 apps/web/ui/layout/page-content/page-content-with-side-panel.tsx create mode 100644 apps/web/ui/layout/page-content/toggle-side-panel-button.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx index 5390ea33791..1448add3f93 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx @@ -9,7 +9,13 @@ import { useIsMegaFolder } from "@/lib/swr/use-is-mega-folder"; import useLinks from "@/lib/swr/use-links"; import useWorkspace from "@/lib/swr/use-workspace"; import { useWorkspaceStore } from "@/lib/swr/use-workspace-store"; +import { FolderDropdown } from "@/ui/folders/folder-dropdown"; +import { + FolderInfoPanel, + FolderInfoPanelControls, +} from "@/ui/folders/folder-info-panel"; import { RequestFolderEditAccessButton } from "@/ui/folders/request-edit-button"; +import { PageContentWithSidePanel } from "@/ui/layout/page-content/page-content-with-side-panel"; import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; import LinkDisplay from "@/ui/links/link-display"; import LinksContainer from "@/ui/links/links-container"; @@ -38,6 +44,7 @@ import { ReactNode, useEffect, useState } from "react"; export default function WorkspaceLinksClient() { const { data: session } = useSession(); + const { folderId } = useCurrentFolderId(); useEffect(() => { if (session?.user) { @@ -49,9 +56,27 @@ export default function WorkspaceLinksClient() { }, [session?.user]); return ( - - - + + + + } + controls={} + sidePanel={ + folderId + ? { + title: "Folder", + content: , + controls: , + } + : undefined + } + > + + + + ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page.tsx index de27c882974..266c6765608 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/page.tsx @@ -1,20 +1,5 @@ -import { FolderDropdown } from "@/ui/folders/folder-dropdown"; -import { PageContent } from "@/ui/layout/page-content"; -import WorkspaceLinksClient, { - WorkspaceLinksPageControls, -} from "./page-client"; +import WorkspaceLinksClient from "./page-client"; export default function WorkspaceLinks() { - return ( - - - - } - controls={} - > - - - ); + return ; } diff --git a/apps/web/lib/folder/get-folder-or-throw.ts b/apps/web/lib/folder/get-folder-or-throw.ts index 190418a5e7f..76e27a01203 100644 --- a/apps/web/lib/folder/get-folder-or-throw.ts +++ b/apps/web/lib/folder/get-folder-or-throw.ts @@ -17,6 +17,7 @@ export const getFolderOrThrow = async ({ select: { id: true, name: true, + description: true, type: true, accessLevel: true, createdAt: true, @@ -48,6 +49,7 @@ export const getFolderOrThrow = async ({ return { id: folder.id, name: folder.name, + description: folder.description, type: folder.type, accessLevel: folder.accessLevel, createdAt: folder.createdAt, diff --git a/apps/web/lib/folder/get-folders.ts b/apps/web/lib/folder/get-folders.ts index 84dd13dff22..f1f0158626a 100644 --- a/apps/web/lib/folder/get-folders.ts +++ b/apps/web/lib/folder/get-folders.ts @@ -53,6 +53,7 @@ export const getFolders = async ({ select: { id: true, name: true, + description: true, type: true, accessLevel: true, createdAt: true, diff --git a/apps/web/lib/swr/use-folder-users.ts b/apps/web/lib/swr/use-folder-users.ts new file mode 100644 index 00000000000..36c5f3096e7 --- /dev/null +++ b/apps/web/lib/swr/use-folder-users.ts @@ -0,0 +1,45 @@ +import { FolderUser } from "@/lib/types"; +import { fetcher } from "@dub/utils"; +import useSWR, { SWRConfiguration } from "swr"; +import { getPlanCapabilities } from "../plan-capabilities"; +import useWorkspace from "./use-workspace"; + +export function useFolderUsers( + { + folderId, + enabled = true, + }: { + folderId?: string | null; + enabled?: boolean; + }, + swrOpts?: SWRConfiguration, +) { + const { id: workspaceId, plan } = useWorkspace(); + const { canManageFolderPermissions } = getPlanCapabilities(plan); + + const { + data: users, + isValidating, + isLoading, + } = useSWR( + enabled && + workspaceId && + canManageFolderPermissions && + folderId && + folderId !== "unsorted" + ? `/api/folders/${folderId}/users?workspaceId=${workspaceId}` + : undefined, + fetcher, + { + revalidateOnFocus: false, + keepPreviousData: true, + ...swrOpts, + }, + ); + + return { + users, + isValidating, + isLoading, + }; +} diff --git a/apps/web/lib/zod/schemas/folders.ts b/apps/web/lib/zod/schemas/folders.ts index 8881f42a91a..4da03a4b0d3 100644 --- a/apps/web/lib/zod/schemas/folders.ts +++ b/apps/web/lib/zod/schemas/folders.ts @@ -25,14 +25,22 @@ export const folderUserRoleSchema = z export const FolderSchema = z.object({ id: z.string().describe("The unique ID of the folder."), name: z.string().describe("The name of the folder."), + description: z.string().nullable().describe("The description of the folder."), type: z.enum(Object.keys(FolderType) as [FolderType, ...FolderType[]]), accessLevel: workspaceFolderAccess, createdAt: z.date().describe("The date the folder was created."), updatedAt: z.date().describe("The date the folder was updated."), }); +export const FOLDER_MAX_DESCRIPTION_LENGTH = 500; + export const createFolderSchema = z.object({ name: z.string().describe("The name of the folder.").max(190), + description: z + .string() + .max(FOLDER_MAX_DESCRIPTION_LENGTH) + .nullish() + .describe("The description of the folder."), accessLevel: workspaceFolderAccess, }); diff --git a/apps/web/ui/folders/add-folder-form.tsx b/apps/web/ui/folders/add-folder-form.tsx index ac6a4ce2b9e..3cc047ef380 100644 --- a/apps/web/ui/folders/add-folder-form.tsx +++ b/apps/web/ui/folders/add-folder-form.tsx @@ -2,6 +2,7 @@ import { FOLDER_WORKSPACE_ACCESS } from "@/lib/folder/constants"; import { getPlanCapabilities } from "@/lib/plan-capabilities"; import useWorkspace from "@/lib/swr/use-workspace"; import { FolderAccessLevel, FolderSummary } from "@/lib/types"; +import { FOLDER_MAX_DESCRIPTION_LENGTH } from "@/lib/zod/schemas/folders"; import { BlurImage, Button, @@ -26,6 +27,7 @@ export const AddFolderForm = ({ onSuccess, onCancel }: AddFolderFormProps) => { const { isMobile } = useMediaQuery(); const [isCreating, setIsCreating] = useState(false); const [name, setName] = useState(undefined); + const [description, setDescription] = useState(undefined); const [accessLevel, setAccessLevel] = useState("write"); // Create new folder @@ -37,6 +39,7 @@ export const AddFolderForm = ({ onSuccess, onCancel }: AddFolderFormProps) => { method: "POST", body: JSON.stringify({ name, + description, ...(accessLevel && { accessLevel }), }), }); @@ -97,32 +100,56 @@ export const AddFolderForm = ({ onSuccess, onCancel }: AddFolderFormProps) => {
{step === 1 ? (
-