From 4800de402e85ea53e31de8d5e78a35c2ab394518 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Sat, 1 Mar 2025 13:09:49 -0500 Subject: [PATCH 01/12] Add `totalLinks` to `Project` --- packages/prisma/schema/workspace.prisma | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/prisma/schema/workspace.prisma b/packages/prisma/schema/workspace.prisma index ae8b725a48..9fcd8550e8 100644 --- a/packages/prisma/schema/workspace.prisma +++ b/packages/prisma/schema/workspace.prisma @@ -14,6 +14,8 @@ model Project { shopifyStoreId String? @unique // for Shopify Integration invoicePrefix String? @unique // The prefix for the customer used to generate unique invoice numbers + totalLinks Int @default(0) // Total number of links in the workspace + usage Int @default(0) usageLimit Int @default(1000) linksUsage Int @default(0) From 7907b1bc98a30bc1713e3bf99689340c8816993e Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Sat, 1 Mar 2025 13:47:07 -0500 Subject: [PATCH 02/12] Disable links counts for workspaces with total links > 500,000 --- .../settings/library/folders/page-client.tsx | 13 +------- apps/web/lib/swr/use-workspace.ts | 9 +++++- apps/web/lib/zod/schemas/workspaces.ts | 3 ++ apps/web/ui/folders/folder-card.tsx | 17 ++++++++--- .../toolbar/onboarding/onboarding-button.tsx | 7 +++-- apps/web/ui/links/link-controls.tsx | 2 +- apps/web/ui/links/links-container.tsx | 12 ++++++-- apps/web/ui/links/links-toolbar.tsx | 18 ++++++----- apps/web/ui/links/use-link-filters.tsx | 30 +++++++++++++++---- packages/ui/src/pagination-controls.tsx | 6 ++-- packages/utils/src/constants/misc.ts | 2 ++ 11 files changed, 82 insertions(+), 37 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx index 358c26d13e..26fa486771 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx @@ -2,7 +2,6 @@ import useFolders from "@/lib/swr/use-folders"; import useFoldersCount from "@/lib/swr/use-folders-count"; -import useLinksCount from "@/lib/swr/use-links-count"; import useWorkspace from "@/lib/swr/use-workspace"; import { Folder } from "@/lib/types"; import { FOLDERS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/folders"; @@ -37,12 +36,6 @@ export const FoldersPageClient = () => { includeParams: true, }); - const { data: allLinksCount } = useLinksCount({ - query: { - showArchived: true, - }, - }); - const showAllLinkFolder = !searchParams.get("search") || folders?.length === 0; @@ -79,11 +72,7 @@ export const FoldersPageClient = () => { )) ) : ( <> - {showAllLinkFolder && ( - - )} + {showAllLinkFolder && } {folders?.map((folder) => ( ))} diff --git a/apps/web/lib/swr/use-workspace.ts b/apps/web/lib/swr/use-workspace.ts index cfb5fedbb3..cc8369fd72 100644 --- a/apps/web/lib/swr/use-workspace.ts +++ b/apps/web/lib/swr/use-workspace.ts @@ -1,5 +1,10 @@ import { ExpandedWorkspaceProps } from "@/lib/types"; -import { PRO_PLAN, fetcher, getNextPlan } from "@dub/utils"; +import { + PRO_PLAN, + WORKSPACE_EXTREME_LINKS_LIMIT, + fetcher, + getNextPlan, +} from "@dub/utils"; import { useParams, useSearchParams } from "next/navigation"; import useSWR, { SWRConfiguration } from "swr"; @@ -37,6 +42,8 @@ export default function useWorkspace({ exceededAI: workspace && workspace.aiUsage >= workspace.aiLimit, exceededDomains: workspace?.domains && workspace.domains.length >= workspace.domainsLimit, + hasExtremeLinks: + workspace && workspace.totalLinks > WORKSPACE_EXTREME_LINKS_LIMIT, error, mutate, loading: slug && !workspace && !error ? true : false, diff --git a/apps/web/lib/zod/schemas/workspaces.ts b/apps/web/lib/zod/schemas/workspaces.ts index 0e66ed8a48..a5cd2c393e 100644 --- a/apps/web/lib/zod/schemas/workspaces.ts +++ b/apps/web/lib/zod/schemas/workspaces.ts @@ -42,6 +42,9 @@ export const WorkspaceSchema = z .string() .nullable() .describe("The Stripe Connect ID of the workspace."), + totalLinks: z + .number() + .describe("The total number of links in the workspace."), usage: z.number().describe("The usage of the workspace."), usageLimit: z.number().describe("The usage limit of the workspace."), linksUsage: z.number().describe("The links usage of the workspace."), diff --git a/apps/web/ui/folders/folder-card.tsx b/apps/web/ui/folders/folder-card.tsx index 147f5acac4..91683d7c29 100644 --- a/apps/web/ui/folders/folder-card.tsx +++ b/apps/web/ui/folders/folder-card.tsx @@ -9,7 +9,7 @@ import useWorkspace from "@/lib/swr/use-workspace"; import { Folder } from "@/lib/types"; import { useIntersectionObserver } from "@dub/ui"; import { Globe } from "@dub/ui/icons"; -import { nFormatter } from "@dub/utils"; +import { cn, nFormatter } from "@dub/utils"; import Link from "next/link"; import { useRef } from "react"; import { FolderActions } from "./folder-actions"; @@ -17,7 +17,11 @@ import { FolderIcon } from "./folder-icon"; import { RequestFolderEditAccessButton } from "./request-edit-button"; export const FolderCard = ({ folder }: { folder: Folder }) => { - const { id: workspaceId, slug: workspaceSlug } = useWorkspace(); + const { + id: workspaceId, + slug: workspaceSlug, + hasExtremeLinks, + } = useWorkspace(); const { isLoading: isPermissionsLoading } = useFolderPermissions(); const canCreateLinks = useCheckFolderPermission( @@ -28,7 +32,12 @@ export const FolderCard = ({ folder }: { folder: Folder }) => { const unsortedLinks = folder.id === "unsorted"; return ( -
+
{ )} - + {!hasExtremeLinks && }
); diff --git a/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx b/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx index 14c192026c..4d16c4f7a8 100644 --- a/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx +++ b/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx @@ -3,6 +3,7 @@ import useDomainsCount from "@/lib/swr/use-domains-count"; import useLinksCount from "@/lib/swr/use-links-count"; import useUsers from "@/lib/swr/use-users"; +import useWorkspace from "@/lib/swr/use-workspace"; import { CheckCircleFill, ThreeDots } from "@/ui/shared/icons"; import { Button, Popover, useLocalStorage, useMediaQuery } from "@dub/ui"; import { CircleDotted, ExpandingArrow } from "@dub/ui/icons"; @@ -29,11 +30,13 @@ function OnboardingButtonInner({ onHideForever: () => void; }) { const { slug } = useParams() as { slug: string }; + const { hasExtremeLinks } = useWorkspace(); const { data: domainsCount, loading: domainsLoading } = useDomainsCount({ ignoreParams: true, }); const { data: linksCount, loading: linksLoading } = useLinksCount({ + enabled: !hasExtremeLinks, ignoreParams: true, }); const { users, loading: usersLoading } = useUsers(); @@ -49,7 +52,7 @@ function OnboardingButtonInner({ { display: "Create a new Dub link", cta: `/${slug}`, - checked: linksCount > 0, + checked: hasExtremeLinks || linksCount > 0, }, { display: "Set up your custom domain", @@ -62,7 +65,7 @@ function OnboardingButtonInner({ checked: (users && users.length > 1) || (invites && invites.length > 0), }, ]; - }, [slug, domainsCount, linksCount, users, invites]); + }, [slug, domainsCount, hasExtremeLinks, linksCount, users, invites]); const [isOpen, setIsOpen] = useState(false); diff --git a/apps/web/ui/links/link-controls.tsx b/apps/web/ui/links/link-controls.tsx index 6b1f7f930b..97fb89a624 100644 --- a/apps/web/ui/links/link-controls.tsx +++ b/apps/web/ui/links/link-controls.tsx @@ -239,7 +239,7 @@ export function LinkControls({ link }: { link: ResponseLink }) {
- {flags?.linkFolders && foldersCount && ( + {Boolean(flags?.linkFolders && foldersCount) && (
- - ), - domain: ( -
-
- + ), + domain: ( +
+
+ +
+

+ No domains found +

+

+ Add a custom domain to match your brand +

+
+
-

- No domains found -

-

- Add a custom domain to match your brand -

-
-
-
- ), - }} - /> -
+ ), + }} + /> + + )}
diff --git a/apps/web/lib/analytics/get-folder-ids-to-filter.ts b/apps/web/lib/analytics/get-folder-ids-to-filter.ts index 485eff7b02..a4d9b50623 100644 --- a/apps/web/lib/analytics/get-folder-ids-to-filter.ts +++ b/apps/web/lib/analytics/get-folder-ids-to-filter.ts @@ -21,6 +21,7 @@ export const getFolderIdsToFilter = async ({ const folders = await getFolders({ workspaceId: workspace.id, userId, + excludeBulkFolders: true, }); folderIds = folders.map((folder) => folder.id).concat(""); diff --git a/apps/web/lib/api/links/get-links-count.ts b/apps/web/lib/api/links/get-links-count.ts index 4809efad12..75fad3957c 100644 --- a/apps/web/lib/api/links/get-links-count.ts +++ b/apps/web/lib/api/links/get-links-count.ts @@ -37,7 +37,7 @@ export async function getLinksCount({ OR: [ { folderId: { - in: folderIds.filter((id) => id !== ""), + in: folderIds, }, }, { diff --git a/apps/web/lib/api/links/get-links-for-workspace.ts b/apps/web/lib/api/links/get-links-for-workspace.ts index cfcc77467d..2fca5a3f1e 100644 --- a/apps/web/lib/api/links/get-links-for-workspace.ts +++ b/apps/web/lib/api/links/get-links-for-workspace.ts @@ -48,7 +48,7 @@ export async function getLinksForWorkspace({ OR: [ { folderId: { - in: folderIds.filter((id) => id !== ""), + in: folderIds, }, }, { diff --git a/apps/web/lib/folder/get-folder-or-throw.ts b/apps/web/lib/folder/get-folder-or-throw.ts index d44684e6d4..a4a9ef10d7 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, + type: true, accessLevel: true, createdAt: true, updatedAt: true, @@ -44,6 +45,7 @@ export const getFolderOrThrow = async ({ return { id: folder.id, name: folder.name, + type: folder.type, accessLevel: folder.accessLevel, createdAt: folder.createdAt, updatedAt: folder.updatedAt, diff --git a/apps/web/lib/folder/get-folders.ts b/apps/web/lib/folder/get-folders.ts index d017b2265d..74fcf9b826 100644 --- a/apps/web/lib/folder/get-folders.ts +++ b/apps/web/lib/folder/get-folders.ts @@ -6,12 +6,14 @@ export const getFolders = async ({ userId, search, includeLinkCount = false, + excludeBulkFolders = false, pageSize = FOLDERS_MAX_PAGE_SIZE, page = 1, }: { workspaceId: string; userId: string; includeLinkCount?: boolean; + excludeBulkFolders?: boolean; search?: string; pageSize?: number; page?: number; @@ -47,10 +49,16 @@ export const getFolders = async ({ contains: search, }, }), + ...(excludeBulkFolders && { + type: { + not: "mega", + }, + }), }, select: { id: true, name: true, + type: true, accessLevel: true, createdAt: true, updatedAt: true, diff --git a/apps/web/lib/swr/use-workspace.ts b/apps/web/lib/swr/use-workspace.ts index cc8369fd72..cfb5fedbb3 100644 --- a/apps/web/lib/swr/use-workspace.ts +++ b/apps/web/lib/swr/use-workspace.ts @@ -1,10 +1,5 @@ import { ExpandedWorkspaceProps } from "@/lib/types"; -import { - PRO_PLAN, - WORKSPACE_EXTREME_LINKS_LIMIT, - fetcher, - getNextPlan, -} from "@dub/utils"; +import { PRO_PLAN, fetcher, getNextPlan } from "@dub/utils"; import { useParams, useSearchParams } from "next/navigation"; import useSWR, { SWRConfiguration } from "swr"; @@ -42,8 +37,6 @@ export default function useWorkspace({ exceededAI: workspace && workspace.aiUsage >= workspace.aiLimit, exceededDomains: workspace?.domains && workspace.domains.length >= workspace.domainsLimit, - hasExtremeLinks: - workspace && workspace.totalLinks > WORKSPACE_EXTREME_LINKS_LIMIT, error, mutate, loading: slug && !workspace && !error ? true : false, diff --git a/apps/web/lib/zod/schemas/folders.ts b/apps/web/lib/zod/schemas/folders.ts index 16091f4f6a..545af96d16 100644 --- a/apps/web/lib/zod/schemas/folders.ts +++ b/apps/web/lib/zod/schemas/folders.ts @@ -4,7 +4,7 @@ import { } from "@/lib/folder/constants"; import { FolderAccessLevel } from "@/lib/types"; import z from "@/lib/zod"; -import { FolderUserRole } from "@dub/prisma/client"; +import { FolderType, FolderUserRole } from "@dub/prisma/client"; import { booleanQuerySchema, getPaginationQuerySchema } from "./misc"; const workspaceFolderAccess = z @@ -25,6 +25,7 @@ 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."), + type: z.enum(Object.keys(FolderType) as [FolderType, ...FolderType[]]), accessLevel: workspaceFolderAccess, linkCount: z .number() diff --git a/apps/web/ui/folders/folder-card.tsx b/apps/web/ui/folders/folder-card.tsx index 91683d7c29..dc3c25c209 100644 --- a/apps/web/ui/folders/folder-card.tsx +++ b/apps/web/ui/folders/folder-card.tsx @@ -17,11 +17,7 @@ import { FolderIcon } from "./folder-icon"; import { RequestFolderEditAccessButton } from "./request-edit-button"; export const FolderCard = ({ folder }: { folder: Folder }) => { - const { - id: workspaceId, - slug: workspaceSlug, - hasExtremeLinks, - } = useWorkspace(); + const { id: workspaceId, slug: workspaceSlug } = useWorkspace(); const { isLoading: isPermissionsLoading } = useFolderPermissions(); const canCreateLinks = useCheckFolderPermission( @@ -35,7 +31,7 @@ export const FolderCard = ({ folder }: { folder: Folder }) => {
{ )} - {!hasExtremeLinks && } + {folder.type !== "mega" && }
); diff --git a/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx b/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx index 65f229fb5a..3d4fca87a8 100644 --- a/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx +++ b/apps/web/ui/layout/toolbar/onboarding/onboarding-button.tsx @@ -46,7 +46,7 @@ function OnboardingButtonInner({ { display: "Create a new Dub link", cta: `/${slug}`, - checked: totalLinks ? totalLinks > 0 : true, + checked: totalLinks === 0 ? false : true, }, { display: "Set up your custom domain", diff --git a/apps/web/ui/links/link-card.tsx b/apps/web/ui/links/link-card.tsx index a7b76ad9c1..edffebcd3d 100644 --- a/apps/web/ui/links/link-card.tsx +++ b/apps/web/ui/links/link-card.tsx @@ -20,6 +20,7 @@ export function LinkCard({ link }: { link: ResponseLink }) { const searchParams = useSearchParams(); const { slug } = useWorkspace(); + // TODO: only enable this when the link card is in view const { folder } = useFolder({ folderId: link.folderId }); return ( diff --git a/apps/web/ui/links/links-container.tsx b/apps/web/ui/links/links-container.tsx index b30bd8cfe7..16efb3a69e 100644 --- a/apps/web/ui/links/links-container.tsx +++ b/apps/web/ui/links/links-container.tsx @@ -1,8 +1,8 @@ "use client"; +import useFolder from "@/lib/swr/use-folder"; import useLinks from "@/lib/swr/use-links"; import useLinksCount from "@/lib/swr/use-links-count"; -import useWorkspace from "@/lib/swr/use-workspace"; import { ExpandedLinkProps, UserProps } from "@/lib/types"; import { CardList, MaxWidthWrapper } from "@dub/ui"; import { CursorRays, Hyperlink } from "@dub/ui/icons"; @@ -31,11 +31,15 @@ export default function LinksContainer({ CreateLinkButton: () => JSX.Element; }) { const { viewMode, sortBy, showArchived } = useContext(LinksDisplayContext); + const searchParams = useSearchParams(); + const folderId = searchParams.get("folderId"); + const { folder: currentFolder } = useFolder({ + folderId, + }); - const { hasExtremeLinks } = useWorkspace(); const { links, isValidating } = useLinks({ sortBy, showArchived }); const { data: count } = useLinksCount({ - enabled: !hasExtremeLinks, + enabled: currentFolder && currentFolder?.type !== "mega", query: { showArchived }, }); @@ -73,8 +77,11 @@ function LinksList({ loading?: boolean; compact: boolean; }) { - const { hasExtremeLinks } = useWorkspace(); const searchParams = useSearchParams(); + const folderId = searchParams.get("folderId"); + const { folder: currentFolder } = useFolder({ + folderId, + }); const [openMenuLinkId, setOpenMenuLinkId] = useState(null); @@ -142,7 +149,9 @@ function LinksList({ loading={!!loading} links={links} linksCount={ - hasExtremeLinks ? Infinity : count ?? links?.length ?? 0 + currentFolder?.type === "mega" + ? Infinity + : count ?? links?.length ?? 0 } /> )} diff --git a/apps/web/ui/links/links-toolbar.tsx b/apps/web/ui/links/links-toolbar.tsx index 5f23645590..3bc7695b4a 100644 --- a/apps/web/ui/links/links-toolbar.tsx +++ b/apps/web/ui/links/links-toolbar.tsx @@ -1,3 +1,4 @@ +import useFolder from "@/lib/swr/use-folder"; import { useFolderPermissions } from "@/lib/swr/use-folder-permissions"; import useWorkspace from "@/lib/swr/use-workspace"; import { @@ -17,6 +18,7 @@ import { usePagination, } from "@dub/ui"; import { cn } from "@dub/utils"; +import { useSearchParams } from "next/navigation"; import { memo, ReactNode, useContext, useMemo } from "react"; import { useArchiveLinkModal } from "../modals/archive-link-modal"; import { useDeleteLinkModal } from "../modals/delete-link-modal"; @@ -47,7 +49,14 @@ export const LinksToolbar = memo( links: ResponseLink[]; linksCount: number; }) => { - const { flags, slug, plan, hasExtremeLinks } = useWorkspace(); + const { flags, slug, plan } = useWorkspace(); + + const searchParams = useSearchParams(); + const folderId = searchParams.get("folderId"); + const { folder: currentFolder } = useFolder({ + folderId, + }); + const { folders } = useFolderPermissions(); const conversionsEnabled = !!plan && plan !== "free" && plan !== "pro"; @@ -224,8 +233,9 @@ export const LinksToolbar = memo( setPagination={setPagination} totalCount={linksCount} unit={(plural) => `${plural ? "links" : "link"}`} + showTotalCount={currentFolder?.type !== "mega"} > - {!hasExtremeLinks && ( + {currentFolder?.type !== "mega" && ( <> {loading ? ( diff --git a/apps/web/ui/links/use-link-filters.tsx b/apps/web/ui/links/use-link-filters.tsx index 26f24a7838..f42c182b8e 100644 --- a/apps/web/ui/links/use-link-filters.tsx +++ b/apps/web/ui/links/use-link-filters.tsx @@ -3,19 +3,16 @@ import useLinksCount from "@/lib/swr/use-links-count"; import useTags from "@/lib/swr/use-tags"; import useTagsCount from "@/lib/swr/use-tags-count"; import useUsers from "@/lib/swr/use-users"; -import useWorkspace from "@/lib/swr/use-workspace"; import { TagProps } from "@/lib/types"; import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags"; import { Avatar, BlurImage, Globe, Tag, User, useRouterStuff } from "@dub/ui"; -import { GOOGLE_FAVICON_URL, nFormatter } from "@dub/utils"; +import { GOOGLE_FAVICON_URL } from "@dub/utils"; import { useContext, useMemo, useState } from "react"; import { useDebounce } from "use-debounce"; import { LinksDisplayContext } from "./links-display-provider"; import TagBadge from "./tag-badge"; export function useLinkFilters() { - const { hasExtremeLinks } = useWorkspace(); - const [selectedFilter, setSelectedFilter] = useState(null); const [search, setSearch] = useState(""); const [debouncedSearch] = useDebounce(search, 500); @@ -50,7 +47,7 @@ export function useLinkFilters() { icon: , label: name, data: { color }, - right: !hasExtremeLinks ? count : undefined, + right: count, hideDuringSearch, })) ?? null, }, @@ -70,9 +67,7 @@ export function useLinkFilters() { options: domains.map(({ slug, count }) => ({ value: slug, label: slug, - right: !hasExtremeLinks - ? nFormatter(count, { full: true }) - : undefined, + right: count, })), }, { @@ -94,11 +89,11 @@ export function useLinkFilters() { className="h-4 w-4" /> ), - right: !hasExtremeLinks ? count : undefined, + right: count, })) ?? null, }, ]; - }, [domains, tags, users, hasExtremeLinks]); + }, [domains, tags, users]); const selectedTagIds = useMemo( () => searchParamsObj["tagIds"]?.split(",")?.filter(Boolean) ?? [], @@ -168,7 +163,6 @@ export function useLinkFilters() { } function useTagFilterOptions(search: string) { - const { hasExtremeLinks } = useWorkspace(); const { searchParamsObj } = useRouterStuff(); const tagIds = useMemo( @@ -193,7 +187,7 @@ function useTagFilterOptions(search: string) { tagId: string; _count: number; }[] - >({ enabled: !hasExtremeLinks, query: { groupBy: "tagId", showArchived } }); + >({ query: { groupBy: "tagId", showArchived } }); const tagsResult = useMemo(() => { return loadingTags || @@ -227,7 +221,6 @@ function useTagFilterOptions(search: string) { } function useDomainFilterOptions() { - const { hasExtremeLinks } = useWorkspace(); const { showArchived } = useContext(LinksDisplayContext); const { data: domainsCount } = useLinksCount< @@ -236,7 +229,6 @@ function useDomainFilterOptions() { _count: number; }[] >({ - enabled: !hasExtremeLinks, query: { groupBy: "domain", showArchived, @@ -246,10 +238,7 @@ function useDomainFilterOptions() { const { allActiveDomains } = useDomains(); return useMemo(() => { - if (hasExtremeLinks) - return allActiveDomains && allActiveDomains.length > 0 - ? allActiveDomains.map(({ slug }) => ({ slug, count: 0 })) - : []; + if (!domainsCount || domainsCount.length === 0) return []; if (!domainsCount || domainsCount.length === 0) return []; @@ -259,11 +248,10 @@ function useDomainFilterOptions() { count: _count, })) .sort((a, b) => b.count - a.count); - }, [hasExtremeLinks, allActiveDomains, domainsCount]); + }, [allActiveDomains, domainsCount]); } function useUserFilterOptions() { - const { hasExtremeLinks } = useWorkspace(); const { users } = useUsers(); const { showArchived } = useContext(LinksDisplayContext); @@ -273,7 +261,6 @@ function useUserFilterOptions() { _count: number; }[] >({ - enabled: !hasExtremeLinks, query: { groupBy: "userId", showArchived, diff --git a/packages/prisma/client.ts b/packages/prisma/client.ts index 8f4c52b877..f28624b972 100644 --- a/packages/prisma/client.ts +++ b/packages/prisma/client.ts @@ -5,6 +5,7 @@ export { CommissionStatus, CommissionType, EventType, + FolderType, FolderUserRole, InvoiceStatus, PartnerRole, diff --git a/packages/prisma/schema/folder.prisma b/packages/prisma/schema/folder.prisma index 9e90e3700d..9b7fcf3012 100644 --- a/packages/prisma/schema/folder.prisma +++ b/packages/prisma/schema/folder.prisma @@ -1,3 +1,8 @@ +enum FolderType { + default + mega +} + enum FolderAccessLevel { read // can view the links write // can view and move links @@ -13,6 +18,7 @@ model Folder { id String @id @default(cuid()) name String projectId String + type FolderType @default(default) accessLevel FolderAccessLevel? // Access level of the folder within the workspace createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/packages/ui/src/pagination-controls.tsx b/packages/ui/src/pagination-controls.tsx index 68ec80a3ff..d243883797 100644 --- a/packages/ui/src/pagination-controls.tsx +++ b/packages/ui/src/pagination-controls.tsx @@ -1,4 +1,4 @@ -import { cn, nFormatter, WORKSPACE_EXTREME_LINKS_LIMIT } from "@dub/utils"; +import { cn, nFormatter } from "@dub/utils"; import { PaginationState } from "@tanstack/react-table"; import { PropsWithChildren } from "react"; @@ -15,12 +15,14 @@ export function PaginationControls({ unit = (p) => `item${p ? "s" : ""}`, className, children, + showTotalCount = true, }: PropsWithChildren<{ pagination: PaginationState; setPagination: (pagination: PaginationState) => void; totalCount: number; unit?: string | ((plural: boolean) => string); className?: string; + showTotalCount?: boolean; }>) { return (
{" "} - of{" "} + {showTotalCount && "of "} )} - - {isFinite(totalCount) - ? nFormatter(totalCount, { full: true }) - : nFormatter(WORKSPACE_EXTREME_LINKS_LIMIT) + "+"} - {" "} + {showTotalCount && ( + + {nFormatter(totalCount, { full: true })} + + )}{" "} {typeof unit === "function" ? unit(totalCount !== 1) : unit}
{children} diff --git a/packages/utils/src/constants/misc.ts b/packages/utils/src/constants/misc.ts index 13f4760046..9c01e881aa 100644 --- a/packages/utils/src/constants/misc.ts +++ b/packages/utils/src/constants/misc.ts @@ -36,5 +36,3 @@ export const TWO_WEEKS_IN_SECONDS = 60 * 60 * 24 * 14; export const DUB_FOUNDING_DATE = new Date("2022-09-22T00:00:00.000Z"); export const INFINITY_NUMBER = 1000000000; - -export const WORKSPACE_EXTREME_LINKS_LIMIT = 500_000; From 79f307725f03e3f30dd9db185f1e2ca56ed66159 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sat, 1 Mar 2025 22:44:28 -0800 Subject: [PATCH 08/12] fix useLinksCount, hide link display, etc. --- .../settings/library/folders/page-client.tsx | 1 + apps/web/lib/swr/use-links-count.ts | 44 ++++++++++--------- apps/web/ui/links/link-display.tsx | 26 +++++++---- apps/web/ui/links/links-container.tsx | 1 - 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx index 26fa486771..8d2c17160c 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/library/folders/page-client.tsx @@ -15,6 +15,7 @@ import { useRouter, useSearchParams } from "next/navigation"; const allLinkFolder: Folder = { id: "unsorted", name: "Links", + type: "default", accessLevel: null, linkCount: 0, createdAt: new Date(), diff --git a/apps/web/lib/swr/use-links-count.ts b/apps/web/lib/swr/use-links-count.ts index 65deb82e3b..78f5898323 100644 --- a/apps/web/lib/swr/use-links-count.ts +++ b/apps/web/lib/swr/use-links-count.ts @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import useSWR from "swr"; import z from "../zod"; import { getLinksCountQuerySchema } from "../zod/schemas/links"; +import useFolder from "./use-folder"; import useWorkspace from "./use-workspace"; const partialQuerySchema = getLinksCountQuerySchema.partial(); @@ -11,14 +12,14 @@ const partialQuerySchema = getLinksCountQuerySchema.partial(); export default function useLinksCount({ query, ignoreParams, - enabled = true, + enabled, }: { query?: z.infer; ignoreParams?: boolean; enabled?: boolean; } = {}) { const { id: workspaceId } = useWorkspace(); - const { getQueryString } = useRouterStuff(); + const { searchParamsObj, getQueryString } = useRouterStuff(); const [admin, setAdmin] = useState(false); useEffect(() => { @@ -26,27 +27,28 @@ export default function useLinksCount({ setAdmin(true); } }, []); + const { folder: currentFolder } = useFolder({ + folderId: searchParamsObj.folderId, + }); const { data, error } = useSWR( - !enabled - ? null - : workspaceId - ? `/api/links/count${getQueryString( - { - workspaceId, - ...query, - }, - ignoreParams - ? { include: [] } - : { - exclude: ["import", "upgrade", "newLink"], - }, - )}` - : admin - ? `/api/admin/links/count${getQueryString({ - ...query, - })}` - : null, + workspaceId && currentFolder?.type !== "mega" && enabled + ? `/api/links/count${getQueryString( + { + workspaceId, + ...query, + }, + ignoreParams + ? { include: [] } + : { + exclude: ["import", "upgrade", "newLink"], + }, + )}` + : admin + ? `/api/admin/links/count${getQueryString({ + ...query, + })}` + : null, fetcher, { dedupingInterval: 60000, diff --git a/apps/web/ui/links/link-display.tsx b/apps/web/ui/links/link-display.tsx index 39ad7f1adc..098d29180f 100644 --- a/apps/web/ui/links/link-display.tsx +++ b/apps/web/ui/links/link-display.tsx @@ -1,3 +1,4 @@ +import useFolder from "@/lib/swr/use-folder"; import { Button, Popover, @@ -15,6 +16,7 @@ import { import { cn } from "@dub/utils"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronDown } from "lucide-react"; +import { useSearchParams } from "next/navigation"; import { useContext, useState } from "react"; import LinkSort from "./link-sort"; import { @@ -36,6 +38,12 @@ export default function LinkDisplay() { reset, } = useContext(LinksDisplayContext); + const searchParams = useSearchParams(); + const folderId = searchParams.get("folderId"); + const { folder: currentFolder } = useFolder({ + folderId, + }); + const [openPopover, setOpenPopover] = useState(false); const { queryParams } = useRouterStuff(); @@ -74,15 +82,17 @@ export default function LinkDisplay() { ); })} -
- - - Ordering - -
- + {currentFolder?.type !== "mega" && ( +
+ + + Ordering + +
+ +
-
+ )}
diff --git a/apps/web/ui/links/links-container.tsx b/apps/web/ui/links/links-container.tsx index 16efb3a69e..8799ccdcbc 100644 --- a/apps/web/ui/links/links-container.tsx +++ b/apps/web/ui/links/links-container.tsx @@ -39,7 +39,6 @@ export default function LinksContainer({ const { links, isValidating } = useLinks({ sortBy, showArchived }); const { data: count } = useLinksCount({ - enabled: currentFolder && currentFolder?.type !== "mega", query: { showArchived }, }); From 0e264038663679a80c2dfa068c8bc053a3b4ddbc Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sat, 1 Mar 2025 23:01:54 -0800 Subject: [PATCH 09/12] useIsMegaFolder, fix tests --- .../(dashboard)/[slug]/page-client.tsx | 8 +++----- apps/web/lib/swr/use-is-mega-folder.ts | 14 ++++++++++++++ apps/web/lib/swr/use-links-count.ts | 12 +++++------- apps/web/tests/folders/index.test.ts | 2 ++ apps/web/ui/links/link-display.tsx | 11 +++-------- apps/web/ui/links/links-container.tsx | 18 +++--------------- apps/web/ui/links/links-toolbar.tsx | 13 ++++--------- 7 files changed, 34 insertions(+), 44 deletions(-) create mode 100644 apps/web/lib/swr/use-is-mega-folder.ts diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx index 67b63eb547..c1519d8005 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx @@ -1,10 +1,10 @@ "use client"; -import useFolder from "@/lib/swr/use-folder"; import { useCheckFolderPermission, useFolderPermissions, } from "@/lib/swr/use-folder-permissions"; +import { useIsMegaFolder } from "@/lib/swr/use-is-mega-folder"; import useLinks from "@/lib/swr/use-links"; import useWorkspace from "@/lib/swr/use-workspace"; import { RequestFolderEditAccessButton } from "@/ui/folders/request-edit-button"; @@ -77,9 +77,7 @@ function WorkspaceLinks() { } = useLinkFilters(); const folderId = searchParams.get("folderId"); - const { folder: currentFolder } = useFolder({ - folderId, - }); + const { isMegaFolder } = useIsMegaFolder(); const { isLoading } = useFolderPermissions(); const canCreateLinks = useCheckFolderPermission( @@ -95,7 +93,7 @@ function WorkspaceLinks() {
- {currentFolder?.type !== "mega" && ( + {!isMegaFolder && (
({ query, ignoreParams, - enabled, + enabled = true, }: { query?: z.infer; ignoreParams?: boolean; enabled?: boolean; } = {}) { const { id: workspaceId } = useWorkspace(); - const { searchParamsObj, getQueryString } = useRouterStuff(); + const { getQueryString } = useRouterStuff(); const [admin, setAdmin] = useState(false); useEffect(() => { @@ -27,12 +27,10 @@ export default function useLinksCount({ setAdmin(true); } }, []); - const { folder: currentFolder } = useFolder({ - folderId: searchParamsObj.folderId, - }); + const { isMegaFolder } = useIsMegaFolder(); const { data, error } = useSWR( - workspaceId && currentFolder?.type !== "mega" && enabled + workspaceId && !isMegaFolder && enabled ? `/api/links/count${getQueryString( { workspaceId, diff --git a/apps/web/tests/folders/index.test.ts b/apps/web/tests/folders/index.test.ts index c8fa169ea7..649914ae37 100644 --- a/apps/web/tests/folders/index.test.ts +++ b/apps/web/tests/folders/index.test.ts @@ -1,5 +1,6 @@ import z from "@/lib/zod"; import { FolderSchema } from "@/lib/zod/schemas/folders"; +import { FolderType } from "@prisma/client"; import { randomId } from "tests/utils/helpers"; import { describe, expect, test } from "vitest"; import { IntegrationHarness } from "../utils/integration"; @@ -8,6 +9,7 @@ type FolderRecord = z.infer; const expectedFolder = { id: expect.any(String), + type: FolderType.default, linkCount: expect.any(Number), createdAt: expect.any(String), updatedAt: expect.any(String), diff --git a/apps/web/ui/links/link-display.tsx b/apps/web/ui/links/link-display.tsx index 098d29180f..d14f91e4b4 100644 --- a/apps/web/ui/links/link-display.tsx +++ b/apps/web/ui/links/link-display.tsx @@ -1,4 +1,4 @@ -import useFolder from "@/lib/swr/use-folder"; +import { useIsMegaFolder } from "@/lib/swr/use-is-mega-folder"; import { Button, Popover, @@ -16,7 +16,6 @@ import { import { cn } from "@dub/utils"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronDown } from "lucide-react"; -import { useSearchParams } from "next/navigation"; import { useContext, useState } from "react"; import LinkSort from "./link-sort"; import { @@ -38,11 +37,7 @@ export default function LinkDisplay() { reset, } = useContext(LinksDisplayContext); - const searchParams = useSearchParams(); - const folderId = searchParams.get("folderId"); - const { folder: currentFolder } = useFolder({ - folderId, - }); + const { isMegaFolder } = useIsMegaFolder(); const [openPopover, setOpenPopover] = useState(false); const { queryParams } = useRouterStuff(); @@ -82,7 +77,7 @@ export default function LinkDisplay() { ); })}
- {currentFolder?.type !== "mega" && ( + {!isMegaFolder && (
diff --git a/apps/web/ui/links/links-container.tsx b/apps/web/ui/links/links-container.tsx index 8799ccdcbc..c4676dd272 100644 --- a/apps/web/ui/links/links-container.tsx +++ b/apps/web/ui/links/links-container.tsx @@ -1,6 +1,6 @@ "use client"; -import useFolder from "@/lib/swr/use-folder"; +import { useIsMegaFolder } from "@/lib/swr/use-is-mega-folder"; import useLinks from "@/lib/swr/use-links"; import useLinksCount from "@/lib/swr/use-links-count"; import { ExpandedLinkProps, UserProps } from "@/lib/types"; @@ -31,11 +31,6 @@ export default function LinksContainer({ CreateLinkButton: () => JSX.Element; }) { const { viewMode, sortBy, showArchived } = useContext(LinksDisplayContext); - const searchParams = useSearchParams(); - const folderId = searchParams.get("folderId"); - const { folder: currentFolder } = useFolder({ - folderId, - }); const { links, isValidating } = useLinks({ sortBy, showArchived }); const { data: count } = useLinksCount({ @@ -77,10 +72,7 @@ function LinksList({ compact: boolean; }) { const searchParams = useSearchParams(); - const folderId = searchParams.get("folderId"); - const { folder: currentFolder } = useFolder({ - folderId, - }); + const { isMegaFolder } = useIsMegaFolder(); const [openMenuLinkId, setOpenMenuLinkId] = useState(null); @@ -147,11 +139,7 @@ function LinksList({ )} diff --git a/apps/web/ui/links/links-toolbar.tsx b/apps/web/ui/links/links-toolbar.tsx index 3bc7695b4a..5212770148 100644 --- a/apps/web/ui/links/links-toolbar.tsx +++ b/apps/web/ui/links/links-toolbar.tsx @@ -1,5 +1,5 @@ -import useFolder from "@/lib/swr/use-folder"; import { useFolderPermissions } from "@/lib/swr/use-folder-permissions"; +import { useIsMegaFolder } from "@/lib/swr/use-is-mega-folder"; import useWorkspace from "@/lib/swr/use-workspace"; import { AnimatedSizeContainer, @@ -18,7 +18,6 @@ import { usePagination, } from "@dub/ui"; import { cn } from "@dub/utils"; -import { useSearchParams } from "next/navigation"; import { memo, ReactNode, useContext, useMemo } from "react"; import { useArchiveLinkModal } from "../modals/archive-link-modal"; import { useDeleteLinkModal } from "../modals/delete-link-modal"; @@ -51,11 +50,7 @@ export const LinksToolbar = memo( }) => { const { flags, slug, plan } = useWorkspace(); - const searchParams = useSearchParams(); - const folderId = searchParams.get("folderId"); - const { folder: currentFolder } = useFolder({ - folderId, - }); + const { isMegaFolder } = useIsMegaFolder(); const { folders } = useFolderPermissions(); const conversionsEnabled = !!plan && plan !== "free" && plan !== "pro"; @@ -233,9 +228,9 @@ export const LinksToolbar = memo( setPagination={setPagination} totalCount={linksCount} unit={(plural) => `${plural ? "links" : "link"}`} - showTotalCount={currentFolder?.type !== "mega"} + showTotalCount={!isMegaFolder} > - {currentFolder?.type !== "mega" && ( + {!isMegaFolder && ( <> {loading ? ( From 8b114ac9eaa658337b171c1ab676e101e1f45232 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sat, 1 Mar 2025 23:14:16 -0800 Subject: [PATCH 10/12] fix tests --- apps/web/tests/folders/index.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/tests/folders/index.test.ts b/apps/web/tests/folders/index.test.ts index 649914ae37..d624ca4428 100644 --- a/apps/web/tests/folders/index.test.ts +++ b/apps/web/tests/folders/index.test.ts @@ -1,6 +1,5 @@ import z from "@/lib/zod"; import { FolderSchema } from "@/lib/zod/schemas/folders"; -import { FolderType } from "@prisma/client"; import { randomId } from "tests/utils/helpers"; import { describe, expect, test } from "vitest"; import { IntegrationHarness } from "../utils/integration"; @@ -9,7 +8,7 @@ type FolderRecord = z.infer; const expectedFolder = { id: expect.any(String), - type: FolderType.default, + type: "default", linkCount: expect.any(Number), createdAt: expect.any(String), updatedAt: expect.any(String), From 352589c368a4db32d4c30b1c4e704eeeb045d84b Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sat, 1 Mar 2025 23:24:17 -0800 Subject: [PATCH 11/12] hide archive on mega folder --- apps/web/ui/links/link-display.tsx | 52 ++++++++++++++++-------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/apps/web/ui/links/link-display.tsx b/apps/web/ui/links/link-display.tsx index d14f91e4b4..08f13b0551 100644 --- a/apps/web/ui/links/link-display.tsx +++ b/apps/web/ui/links/link-display.tsx @@ -42,7 +42,9 @@ export default function LinkDisplay() { const [openPopover, setOpenPopover] = useState(false); const { queryParams } = useRouterStuff(); - useKeyboardShortcut("a", () => setShowArchived((o) => !o)); + useKeyboardShortcut("a", () => setShowArchived((o) => !o), { + enabled: !isMegaFolder, + }); return (
)} -
-
-
- - - A - + {!isMegaFolder && ( +
+
+
+ + + A + +
+ Show archived links +
+
+ { + setShowArchived(checked); + queryParams({ + del: [ + "showArchived", // Remove legacy query param + "page", // Reset pagination + ], + }); + }} + />
- Show archived links -
-
- { - setShowArchived(checked); - queryParams({ - del: [ - "showArchived", // Remove legacy query param - "page", // Reset pagination - ], - }); - }} - />
-
+ )}
Display Properties From e36acdcf85142fa9b237b0a564449b29a9382ad8 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Sun, 2 Mar 2025 18:40:22 +0530 Subject: [PATCH 12/12] Update use-link-filters.tsx --- apps/web/ui/links/use-link-filters.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/ui/links/use-link-filters.tsx b/apps/web/ui/links/use-link-filters.tsx index f42c182b8e..c080235baf 100644 --- a/apps/web/ui/links/use-link-filters.tsx +++ b/apps/web/ui/links/use-link-filters.tsx @@ -1,4 +1,3 @@ -import useDomains from "@/lib/swr/use-domains"; import useLinksCount from "@/lib/swr/use-links-count"; import useTags from "@/lib/swr/use-tags"; import useTagsCount from "@/lib/swr/use-tags-count"; @@ -235,20 +234,16 @@ function useDomainFilterOptions() { }, }); - const { allActiveDomains } = useDomains(); - return useMemo(() => { if (!domainsCount || domainsCount.length === 0) return []; - if (!domainsCount || domainsCount.length === 0) return []; - return domainsCount .map(({ domain, _count }) => ({ slug: domain, count: _count, })) .sort((a, b) => b.count - a.count); - }, [allActiveDomains, domainsCount]); + }, [domainsCount]); } function useUserFilterOptions() {