Skip to content

Set default folder #2156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
328feb1
default folder
devkiran Mar 13, 2025
ed0dbf8
set default modal
devkiran Mar 13, 2025
82af0d1
fetch links from default folder
devkiran Mar 13, 2025
74beae1
folder fixes
devkiran Mar 13, 2025
e5d34a4
hide popover if no actions exists
devkiran Mar 13, 2025
4b25af0
add default badge
devkiran Mar 13, 2025
9604805
simplify default folder check
devkiran Mar 13, 2025
a19d2c2
Update constants.ts
devkiran Mar 13, 2025
ac1215e
Update route.ts
devkiran Mar 13, 2025
827963b
Merge branch 'main' into workspace-default-folder
steven-tey Mar 14, 2025
b7ed79d
update folder dropdown + link cards to show default folder ID
steven-tey Mar 14, 2025
b4822bd
Merge branch 'main' into workspace-default-folder
devkiran Mar 14, 2025
84b7c06
Update folder-actions.tsx
devkiran Mar 14, 2025
a2cc7f2
Throw an error when they try to change a default workspace folder's p…
devkiran Mar 14, 2025
4cfde48
Update folder-card.tsx
devkiran Mar 14, 2025
a264e83
revert API changes
devkiran Mar 14, 2025
e9f9e09
Merge branch 'main' into workspace-default-folder
steven-tey Mar 15, 2025
cc1534b
Merge branch 'main' into workspace-default-folder
steven-tey Mar 15, 2025
ae488f3
Merge branch 'main' into workspace-default-folder
steven-tey Mar 16, 2025
94a01cc
Merge branch 'workspace-default-folder' of https://github.com/dubinc/…
devkiran Mar 17, 2025
fd8ac0b
Merge branch 'main' into workspace-default-folder
devkiran Mar 17, 2025
af0907b
Merge branch 'main' into workspace-default-folder
devkiran Mar 18, 2025
2e50050
update folder switcher
devkiran Mar 18, 2025
9c1cdae
Update folder-dropdown.tsx
devkiran Mar 18, 2025
96be3d3
Merge branch 'main' into workspace-default-folder
steven-tey Mar 19, 2025
20b9762
Merge branch 'main' into workspace-default-folder
steven-tey Mar 19, 2025
7302779
Merge branch 'main' into workspace-default-folder
steven-tey Mar 19, 2025
a14e600
Merge branch 'main' into workspace-default-folder
devkiran Mar 21, 2025
ce9a58d
move the defaultFolderId to the workspace users level
devkiran Mar 21, 2025
948e4d3
links changes
devkiran Mar 21, 2025
2302f80
load the unsorted folder
devkiran Mar 21, 2025
9c7e0fd
Refactor folderId handling in useLinks and related components
devkiran Mar 21, 2025
63d1dc5
Update set-default-folder-modal.tsx
devkiran Mar 21, 2025
d7280e9
revert API changes
devkiran Mar 21, 2025
4da97d2
Merge branch 'main' into workspace-default-folder
devkiran Mar 22, 2025
3e59d15
Merge branch 'main' into workspace-default-folder
steven-tey Mar 23, 2025
192339b
Merge branch 'main' into workspace-default-folder
steven-tey Mar 23, 2025
e2e23d6
Merge branch 'main' into workspace-default-folder
steven-tey Mar 24, 2025
2e485d7
Merge branch 'main' into workspace-default-folder
steven-tey Mar 25, 2025
507ae63
final touches
steven-tey Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/app/api/links/count/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const GET = withWorkspace(
folderId,
requiredPermission: "folders.read",
});

if (selectedFolder.type === "mega") {
throw new DubApiError({
code: "bad_request",
Expand Down Expand Up @@ -61,6 +62,7 @@ export const GET = withWorkspace(
folderIds = undefined;
}
}

const count = await getLinksCount({
searchParams: params,
workspaceId: workspace.id,
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const GET = withSession(async ({ session }) => {
},
select: {
role: true,
defaultFolderId: true,
},
},
domains: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { unsortedLinks } from "@/lib/folder/constants";
import useFolders from "@/lib/swr/use-folders";
import useFoldersCount from "@/lib/swr/use-folders-count";
import useWorkspace from "@/lib/swr/use-workspace";
Expand All @@ -13,13 +14,10 @@ import { PaginationControls, usePagination, useRouterStuff } from "@dub/ui";
import { useRouter, useSearchParams } from "next/navigation";

const allLinkFolder: Folder = {
id: "unsorted",
name: "Links",
type: "default",
accessLevel: null,
linkCount: 0,
createdAt: new Date(),
updatedAt: new Date(),
...unsortedLinks,
};

export const FoldersPageClient = () => {
Expand Down
40 changes: 40 additions & 0 deletions apps/web/lib/actions/folders/set-default-folder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use server";

import { verifyFolderAccess } from "@/lib/folder/permissions";
import { prisma } from "@dub/prisma";
import { z } from "zod";
import { authActionClient } from "../safe-action";

const setDefaultFolderSchema = z.object({
workspaceId: z.string(),
folderId: z.string().nullable(),
});

// Set the default folder for a workspace for a user
export const setDefaultFolderAction = authActionClient
.schema(setDefaultFolderSchema)
.action(async ({ ctx, parsedInput }) => {
const { user, workspace } = ctx;
const { folderId } = parsedInput;

if (folderId) {
await verifyFolderAccess({
workspace,
userId: user.id,
folderId,
requiredPermission: "folders.read",
});
}

await prisma.projectUsers.update({
where: {
userId_projectId: {
userId: user.id,
projectId: workspace.id,
},
},
data: {
defaultFolderId: folderId,
},
});
});
1 change: 1 addition & 0 deletions apps/web/lib/auth/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export const withWorkspace = (
},
select: {
role: true,
defaultFolderId: true,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/folder/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export const FOLDER_USER_ROLE_TO_PERMISSIONS: Record<
export const unsortedLinks: FolderSummary = {
id: "unsorted",
name: "Links",
accessLevel: null,
accessLevel: "write",
linkCount: -1,
};
2 changes: 1 addition & 1 deletion apps/web/lib/swr/use-folder-permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function useCheckFolderPermission(
return true;
}

if (!folderId) {
if (!folderId || folderId === "unsorted") {
return true;
}

Expand Down
11 changes: 8 additions & 3 deletions apps/web/lib/swr/use-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import useWorkspace from "./use-workspace";
export default function useFolder({ folderId }: { folderId?: string | null }) {
const { id: workspaceId, plan, flags } = useWorkspace();

const enabled =
folderId &&
folderId !== "unsorted" &&
workspaceId &&
flags?.linkFolders &&
plan !== "free";

const {
data: folder,
isValidating,
isLoading,
} = useSWR<Folder>(
folderId && workspaceId && flags?.linkFolders && plan !== "free"
? `/api/folders/${folderId}?workspaceId=${workspaceId}`
: null,
enabled ? `/api/folders/${folderId}?workspaceId=${workspaceId}` : null,
fetcher,
{
dedupingInterval: 60000,
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/swr/use-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function useWorkspace({
exceededDomains:
workspace?.domains && workspace.domains.length >= workspace.domainsLimit,
error,
defaultFolderId: workspace?.users && workspace.users[0].defaultFolderId,
mutate,
loading: slug && !workspace && !error ? true : false,
};
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface WorkspaceProps extends Project {
}[];
users: {
role: RoleProps;
defaultFolderId: string | null;
}[];
flags?: {
[key in BetaFeatures]: boolean;
Expand Down
6 changes: 6 additions & 0 deletions apps/web/lib/zod/schemas/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export const WorkspaceSchema = z
.array(
z.object({
role: roleSchema,
defaultFolderId: z
.string()
.nullable()
.describe(
"The ID of the default folder for the user in the workspace.",
),
}),
)
.describe("The role of the authenticated user in the workspace."),
Expand Down
114 changes: 86 additions & 28 deletions apps/web/ui/folders/folder-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import {
Users,
} from "@dub/ui";
import { cn } from "@dub/utils";
import { Bookmark } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { useDeleteFolderModal } from "../modals/delete-folder-modal";
import { useRenameFolderModal } from "../modals/rename-folder-modal";
import { useDefaultFolderModal } from "../modals/set-default-folder-modal";
import { Chart, Delete, ThreeDots } from "../shared/icons";
import { useFolderPermissionsPanel } from "./folder-permissions-panel";
import { isDefaultFolder } from "./utils";

export const FolderActions = ({
folder,
Expand All @@ -28,9 +31,8 @@ export const FolderActions = ({
onDelete?: () => void;
}) => {
const router = useRouter();
const { slug: workspaceSlug } = useWorkspace();
const [openPopover, setOpenPopover] = useState(false);
const canUpdateFolder = useCheckFolderPermission(folder.id, "folders.write");
const { slug: workspaceSlug, defaultFolderId } = useWorkspace();

const { RenameFolderModal, setShowRenameFolderModal } =
useRenameFolderModal(folder);
Expand All @@ -40,30 +42,50 @@ export const FolderActions = ({
onDelete,
);

const { DefaultFolderModal, setShowDefaultFolderModal } =
useDefaultFolderModal({
folder,
});

const { folderPermissionsPanel, setShowFolderPermissionsPanel } =
useFolderPermissionsPanel(folder);

const [copiedFolderId, copyToClipboard] = useCopyToClipboard();

const copyFolderId = () => {
toast.promise(copyToClipboard(folder.id), {
success: "Folder ID copied!",
});
};

const [copiedFolderId, copyToClipboard] = useCopyToClipboard();
const canUpdateFolder = useCheckFolderPermission(folder.id, "folders.write");

const isDefault = isDefaultFolder({ folder, defaultFolderId });
const unsortedLinks = folder.id === "unsorted";

useKeyboardShortcut(
["r", "m", "i", "x", "a"],
["r", "m", "i", "x", "a", "d"],
(e) => {
setOpenPopover(false);
switch (e.key) {
case "a":
router.push(`/${workspaceSlug}/analytics?folderId=${folder.id}`);
if (!unsortedLinks) {
router.push(`/${workspaceSlug}/analytics?folderId=${folder.id}`);
}
break;
case "m":
setShowFolderPermissionsPanel(true);
if (!unsortedLinks) {
setShowFolderPermissionsPanel(true);
}
break;
case "i":
copyFolderId();
if (!unsortedLinks) {
copyFolderId();
}
break;
case "d":
if (!isDefault) {
setShowDefaultFolderModal(true);
}
break;
case "r":
if (canUpdateFolder) {
Expand All @@ -86,6 +108,7 @@ export const FolderActions = ({
<>
<RenameFolderModal />
<DeleteFolderModal />
<DefaultFolderModal />
{folderPermissionsPanel}
<Popover
content={
Expand All @@ -104,35 +127,57 @@ export const FolderActions = ({
shortcut="A"
className="h-9 px-2 font-medium"
/>

{!unsortedLinks && (
<Button
text="Members"
variant="outline"
onClick={() => {
setOpenPopover(false);
setShowFolderPermissionsPanel(true);
}}
icon={<Users className="h-4 w-4" />}
shortcut="M"
className="h-9 px-2 font-medium"
/>
)}
</div>

<div className="grid gap-px p-2">
{!unsortedLinks && (
<Button
text="Copy Folder ID"
variant="outline"
onClick={() => copyFolderId()}
icon={
copiedFolderId ? (
<CircleCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)
}
shortcut="I"
className="h-9 px-2 font-medium"
/>
)}

<Button
text="Members"
text="Set as Default"
variant="outline"
onClick={() => {
setOpenPopover(false);
setShowFolderPermissionsPanel(true);
setShowDefaultFolderModal(true);
}}
icon={<Users className="h-4 w-4" />}
shortcut="M"
icon={<Bookmark className="h-4 w-4" />}
shortcut="D"
className="h-9 px-2 font-medium"
/>
</div>
<div className="grid gap-px p-2">
<Button
text="Copy Folder ID"
variant="outline"
onClick={() => copyFolderId()}
icon={
copiedFolderId ? (
<CircleCheck className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)
disabled={isDefault}
disabledTooltip={
isDefault ? "This is your default folder." : undefined
}
shortcut="I"
className="h-9 px-2 font-medium"
/>

{canUpdateFolder && (
{!unsortedLinks && (
<>
<Button
text="Rename"
Expand All @@ -144,7 +189,14 @@ export const FolderActions = ({
icon={<PenWriting className="h-4 w-4" />}
shortcut="R"
className="h-9 px-2 font-medium"
disabled={!canUpdateFolder}
disabledTooltip={
!canUpdateFolder
? "Only folder owners can rename a folder."
: undefined
}
/>

<Button
text="Delete"
variant="danger-outline"
Expand All @@ -155,6 +207,12 @@ export const FolderActions = ({
icon={<Delete className="h-4 w-4" />}
shortcut="X"
className="h-9 px-2 font-medium"
disabled={!canUpdateFolder}
disabledTooltip={
!canUpdateFolder
? "Only folder owners can delete a folder."
: undefined
}
/>
</>
)}
Expand Down
Loading