From 0c0292901919a8e8e1c2a90414c36dd06ce96849 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 4 Jan 2026 17:54:58 +0000 Subject: [PATCH] feat(fronend): wip org prefixed routing --- apps/drive-web/package.json | 2 +- apps/drive-web/src/auth/api.ts | 9 +- .../src/dashboard/dashboard-sidebar.tsx | 100 +++---- .../directory-content-table.tsx | 13 +- .../directory-page/new-directory-dialog.tsx | 8 +- .../directory-page/rename-file-dialog.tsx | 13 +- apps/drive-web/src/drive/api.ts | 20 +- apps/drive-web/src/drive/drive.ts | 4 +- apps/drive-web/src/entry.tsx | 10 +- .../src/files/upload-file-dialog.tsx | 7 +- apps/drive-web/src/files/upload.ts | 20 +- apps/drive-web/src/lib/api.ts | 48 +-- apps/drive-web/src/organization/api.ts | 32 ++ apps/drive-web/src/organization/context.ts | 16 + .../src/organization/organization.ts | 18 ++ apps/drive-web/src/query-client.ts | 14 + apps/drive-web/src/routeTree.gen.ts | 172 +++++++---- apps/drive-web/src/router.ts | 14 + apps/drive-web/src/routes/__root.tsx | 21 +- apps/drive-web/src/routes/_authenticated.tsx | 35 +-- .../$orgSlug/_sidebar-layout.tsx | 52 ++++ .../directories.$directoryId.tsx | 49 +-- .../_sidebar-layout/drives.$driveId.tsx | 25 ++ .../$orgSlug/_sidebar-layout/index.tsx | 13 + .../routes/_authenticated/_sidebar-layout.tsx | 20 -- .../_authenticated/_sidebar-layout/home.tsx | 9 - .../src/routes/_authenticated/index.tsx | 33 +- apps/drive-web/src/routes/login.tsx | 20 +- apps/drive-web/src/sharing/api.ts | 113 ++++--- .../src/sharing/item-share-dialog.tsx | 144 ++++----- apps/drive-web/src/vfs/api.ts | 281 ++++++++++-------- apps/drive-web/src/vfs/hooks.ts | 19 +- 32 files changed, 835 insertions(+), 519 deletions(-) create mode 100644 apps/drive-web/src/organization/api.ts create mode 100644 apps/drive-web/src/organization/context.ts create mode 100644 apps/drive-web/src/organization/organization.ts create mode 100644 apps/drive-web/src/query-client.ts create mode 100644 apps/drive-web/src/router.ts create mode 100644 apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout.tsx rename apps/drive-web/src/routes/_authenticated/{ => $orgSlug}/_sidebar-layout/directories.$directoryId.tsx (92%) create mode 100644 apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/drives.$driveId.tsx create mode 100644 apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/index.tsx delete mode 100644 apps/drive-web/src/routes/_authenticated/_sidebar-layout.tsx delete mode 100644 apps/drive-web/src/routes/_authenticated/_sidebar-layout/home.tsx diff --git a/apps/drive-web/package.json b/apps/drive-web/package.json index dc0e971..808199b 100644 --- a/apps/drive-web/package.json +++ b/apps/drive-web/package.json @@ -29,7 +29,7 @@ "@tanstack/react-router": "^1.131.41", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.13", - "@tanstack/router-devtools": "^1.131.42", + "@tanstack/router-devtools": "^1.144.0", "arktype": "^2.1.28", "better-auth": "1.3.8", "class-variance-authority": "^0.7.1", diff --git a/apps/drive-web/src/auth/api.ts b/apps/drive-web/src/auth/api.ts index e9e0776..60e5275 100644 --- a/apps/drive-web/src/auth/api.ts +++ b/apps/drive-web/src/auth/api.ts @@ -3,13 +3,16 @@ import { type } from "arktype" import { Account } from "@/account/account" import { accountsQuery } from "@/account/api" import { fetchApi } from "@/lib/api" +import { Organization, ORGANIZATION_KIND } from "@/organization/organization" import { currentUserQuery } from "@/user/api" import { User } from "@/user/user" import { drivesQuery } from "@/drive/api" import { Drive } from "@/drive/drive" +import { listOrganizationsQuery } from "@/organization/api" const LoginResponseSchema = type({ user: User, + organizations: Organization.array(), }) const SignUpResponse = type({ @@ -30,9 +33,12 @@ export const loginMutation = mutationOptions({ return result }, onSuccess: (data, _, __, context) => { + context.client.setQueryData( + listOrganizationsQuery.queryKey, + data.organizations, + ) context.client.setQueryData(currentUserQuery.queryKey, data.user) context.client.invalidateQueries(accountsQuery) - context.client.invalidateQueries(drivesQuery) }, }) @@ -54,6 +60,5 @@ export const signUpMutation = mutationOptions({ onSuccess: (data, _, __, context) => { context.client.setQueryData(currentUserQuery.queryKey, data.user) context.client.setQueryData(accountsQuery.queryKey, [data.account]) - context.client.setQueryData(drivesQuery.queryKey, [data.drive]) }, }) diff --git a/apps/drive-web/src/dashboard/dashboard-sidebar.tsx b/apps/drive-web/src/dashboard/dashboard-sidebar.tsx index dc1fe87..5cdabe2 100644 --- a/apps/drive-web/src/dashboard/dashboard-sidebar.tsx +++ b/apps/drive-web/src/dashboard/dashboard-sidebar.tsx @@ -1,15 +1,19 @@ import { useMutation, useQuery } from "@tanstack/react-query" -import { Link, useLocation, useParams } from "@tanstack/react-router" +import { + getRouteApi, + Link, + useParams, + useRouterState, +} from "@tanstack/react-router" import { useAtom, useAtomValue, useSetAtom } from "jotai" import { CircleXIcon, - ClockIcon, FilesIcon, FolderInputIcon, + HouseIcon, LogOutIcon, ScissorsIcon, SettingsIcon, - TrashIcon, User2Icon, } from "lucide-react" import { toast } from "sonner" @@ -31,10 +35,10 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar" import { formatError } from "@/lib/error" -import { - moveDirectoryItemsMutationAtom, - rootDirectoryQueryAtom, -} from "@/vfs/api" +import { currentDriveAtom } from "@/drive/drive" +import { listOrganizationDrivesQuery } from "@/organization/api" +import { useCurrentOrganization } from "@/organization/context" +import { moveDirectoryItemsMutationOptions } from "@/vfs/api" import { Button } from "../components/ui/button" import { LoadingSpinner } from "../components/ui/loading-spinner" import { clearCutItemsAtom, cutItemsAtom } from "../files/store" @@ -66,46 +70,52 @@ export function DashboardSidebar() { } function MainSidebarMenu() { - const location = useLocation() - - const isActive = (path: string) => { - if (path === "/") { - return location.pathname === "/" - } - return location.pathname.startsWith(path) - } + const org = useCurrentOrganization() + const matchingRoute = getRouteApi( + "/_authenticated/$orgSlug/_sidebar-layout/", + ) + const isActive = useRouterState({ + select: (state) => + state.matches.some((match) => match.routeId === matchingRoute.id), + }) return ( - - - - Recent + + + + Home - ) } function AllFilesItem() { - const location = useLocation() - const { data: rootDirectory } = useQuery( - useAtomValue(rootDirectoryQueryAtom), + const org = useCurrentOrganization() + const matchingRoute = getRouteApi( + "/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId", ) + const isActive = useRouterState({ + select: (state) => + state.matches.some((match) => match.routeId === matchingRoute.id), + }) - if (!rootDirectory) return null + const { data: drives } = useQuery(listOrganizationDrivesQuery(org.slug)) + const drive = drives?.at(0) + + if (!drive) return null return ( - - + + All Files @@ -114,29 +124,6 @@ function AllFilesItem() { ) } -function TrashItem() { - const location = useLocation() - const { data: rootDirectory } = useQuery( - useAtomValue(rootDirectoryQueryAtom), - ) - - if (!rootDirectory) return null - - return ( - - - - - Trash - - - - ) -} - function BackgroundTaskProgressItem() { const backgroundTaskProgress = useAtomValue(backgroundTaskProgressAtom) @@ -160,9 +147,12 @@ function CutItemsCard() { const clearCutItems = useSetAtom(clearCutItemsAtom) const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) - const moveDirectoryItemsMutation = useAtomValue( - moveDirectoryItemsMutationAtom, - ) + const org = useCurrentOrganization() + const drive = useAtomValue(currentDriveAtom) + const moveDirectoryItemsMutation = moveDirectoryItemsMutationOptions({ + org, + drive, + }) const { mutate: moveItems } = useMutation({ ...moveDirectoryItemsMutation, diff --git a/apps/drive-web/src/directories/directory-page/directory-content-table.tsx b/apps/drive-web/src/directories/directory-page/directory-content-table.tsx index 2bdde28..b0f0d50 100644 --- a/apps/drive-web/src/directories/directory-page/directory-content-table.tsx +++ b/apps/drive-web/src/directories/directory-page/directory-content-table.tsx @@ -40,9 +40,11 @@ import { type DirectoryContentOrderDirection, type DirectoryContentQuery, type MoveDirectoryItemsResult, - moveDirectoryItemsMutationAtom, + moveDirectoryItemsMutationOptions, } from "@/vfs/api" import type { DirectoryInfo, DirectoryItem, FileInfo } from "@/vfs/vfs" +import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" import { DEFAULT_DIRECTORY_CONTENT_ORDER_BY, DEFAULT_DIRECTORY_CONTENT_ORDER_DIRECTION, @@ -205,9 +207,12 @@ export function DirectoryContentTable({ const store = useStore() const navigate = useNavigate() - const moveDirectoryItemsMutation = useAtomValue( - moveDirectoryItemsMutationAtom, - ) + const org = useCurrentOrganization() + const drive = useAtomValue(currentDriveAtom) + const moveDirectoryItemsMutation = moveDirectoryItemsMutationOptions({ + org, + drive, + }) const { mutate: moveDroppedItems } = useMutation({ ...moveDirectoryItemsMutation, diff --git a/apps/drive-web/src/directories/directory-page/new-directory-dialog.tsx b/apps/drive-web/src/directories/directory-page/new-directory-dialog.tsx index 3304972..ea59788 100644 --- a/apps/drive-web/src/directories/directory-page/new-directory-dialog.tsx +++ b/apps/drive-web/src/directories/directory-page/new-directory-dialog.tsx @@ -3,6 +3,8 @@ import { useAtomValue } from "jotai" import { useId } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" +import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" import { Dialog, DialogClose, @@ -12,7 +14,7 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" -import { createDirectoryMutationAtom } from "@/vfs/api" +import { createDirectoryMutationOptions } from "@/vfs/api" import type { DirectoryInfo } from "@/vfs/vfs" export function NewDirectoryDialog({ @@ -26,7 +28,9 @@ export function NewDirectoryDialog({ }) { const formId = useId() - const createDirectoryMutation = useAtomValue(createDirectoryMutationAtom) + const org = useCurrentOrganization() + const drive = useAtomValue(currentDriveAtom) + const createDirectoryMutation = createDirectoryMutationOptions({ org, drive }) const { mutate: createDirectory, isPending: isCreating } = useMutation({ ...createDirectoryMutation, diff --git a/apps/drive-web/src/directories/directory-page/rename-file-dialog.tsx b/apps/drive-web/src/directories/directory-page/rename-file-dialog.tsx index 43daf6b..f04973b 100644 --- a/apps/drive-web/src/directories/directory-page/rename-file-dialog.tsx +++ b/apps/drive-web/src/directories/directory-page/rename-file-dialog.tsx @@ -2,6 +2,8 @@ import { useMutation } from "@tanstack/react-query" import { useAtomValue } from "jotai" import { useId } from "react" import { Button } from "@/components/ui/button" +import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" import { Dialog, DialogClose, @@ -11,7 +13,10 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" -import { renameDirectoryMutationAtom, renameFileMutationAtom } from "@/vfs/api" +import { + renameDirectoryMutationOptions, + renameFileMutationOptions, +} from "@/vfs/api" import type { DirectoryItem } from "@/vfs/vfs" type RenameFileDialogProps = { @@ -27,8 +32,10 @@ export function RenameFileDialog({ }: RenameFileDialogProps) { const formId = useId() - const renameFileMutation = useAtomValue(renameFileMutationAtom) - const renameDirectoryMutation = useAtomValue(renameDirectoryMutationAtom) + const org = useCurrentOrganization() + const drive = useAtomValue(currentDriveAtom) + const renameFileMutation = renameFileMutationOptions({ org, drive }) + const renameDirectoryMutation = renameDirectoryMutationOptions({ org, drive }) const { mutate: renameFile, isPending: isRenamingFile } = useMutation({ ...renameFileMutation, diff --git a/apps/drive-web/src/drive/api.ts b/apps/drive-web/src/drive/api.ts index faccd85..b89bcdf 100644 --- a/apps/drive-web/src/drive/api.ts +++ b/apps/drive-web/src/drive/api.ts @@ -1,11 +1,15 @@ -import { queryOptions } from "@tanstack/react-query" +import { queryOptions, skipToken } from "@tanstack/react-query" import { fetchApi } from "@/lib/api" import { Drive } from "./drive" +import type { Organization } from "@/organization/organization" -export const drivesQuery = queryOptions({ - queryKey: ["drives"], - queryFn: async () => - fetchApi("GET", "/drives", { - returns: Drive.array(), - }).then(([_, result]) => result), -}) +export const drivesQuery = (org: Organization | null) => + queryOptions({ + queryKey: ["organizations", org?.slug, "drives"], + queryFn: org + ? () => + fetchApi("GET", `/${org.slug}/drives`, { + returns: Drive.array(), + }) + : skipToken, + }) diff --git a/apps/drive-web/src/drive/drive.ts b/apps/drive-web/src/drive/drive.ts index 900a2b0..c5a4738 100644 --- a/apps/drive-web/src/drive/drive.ts +++ b/apps/drive-web/src/drive/drive.ts @@ -3,12 +3,10 @@ import { atom } from "jotai" export const Drive = type({ id: "string", - publicId: "string", - orgId: "string", - "ownerAccountId?": "string", name: "string", createdAt: "string.date.iso.parse", updatedAt: "string.date.iso.parse", + rootDirId: "string", storageUsageBytes: "number", storageQuotaBytes: "number", }) diff --git a/apps/drive-web/src/entry.tsx b/apps/drive-web/src/entry.tsx index 48a0d9e..becb2de 100644 --- a/apps/drive-web/src/entry.tsx +++ b/apps/drive-web/src/entry.tsx @@ -5,21 +5,19 @@ * It is included in `src/index.html`. */ -import { createRouter, RouterProvider } from "@tanstack/react-router" +import { RouterProvider } from "@tanstack/react-router" +import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { ThemeProvider } from "@/components/theme-provider" -// Import the generated route tree -import { routeTree } from "./routeTree.gen" - -// Create a new router instance -const router = createRouter({ routeTree }) +import { router } from "./router" const elem = document.getElementById("root")! const app = ( + ) diff --git a/apps/drive-web/src/files/upload-file-dialog.tsx b/apps/drive-web/src/files/upload-file-dialog.tsx index 5a9ac9e..ef93af9 100644 --- a/apps/drive-web/src/files/upload-file-dialog.tsx +++ b/apps/drive-web/src/files/upload-file-dialog.tsx @@ -32,6 +32,7 @@ import { formatError } from "@/lib/error" import { directoryContentQueryKey } from "@/vfs/api" import type { DirectoryInfoWithPath } from "@/vfs/vfs" import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" import { clearAllFileUploadStatusesAtom, clearFileUploadStatusesAtom, @@ -63,6 +64,7 @@ function useUploadFilesAtom({ targetDirectory: DirectoryInfoWithPath }) { const store = useStore() + const org = useCurrentOrganization() const options = useMemo( () => @@ -70,9 +72,11 @@ function useUploadFilesAtom({ mutationFn: async (files: PickedFile[]) => { const drive = store.get(currentDriveAtom) if (!drive) throw new Error("No drive selected") + const orgSlug = org.slug const promises = files.map((pickedFile) => uploadFile({ + orgSlug, drive, file: pickedFile.file, targetDirectory, @@ -140,6 +144,7 @@ function useUploadFilesAtom({ if (drive) { client.invalidateQueries({ queryKey: directoryContentQueryKey( + org.slug, drive.id, targetDirectory.id, ), @@ -151,7 +156,7 @@ function useUploadFilesAtom({ toast.error(formatError(error)) }, }), - [store, targetDirectory], + [org.slug, store, targetDirectory], ) return useMemo(() => atomWithMutation(() => options), [options]) diff --git a/apps/drive-web/src/files/upload.ts b/apps/drive-web/src/files/upload.ts index 772ed33..c0f8b85 100644 --- a/apps/drive-web/src/files/upload.ts +++ b/apps/drive-web/src/files/upload.ts @@ -14,12 +14,14 @@ export const Upload = type({ export type Upload = typeof Upload.infer export async function uploadFile({ + orgSlug, drive, file, targetDirectory, onStart, onProgress, }: { + orgSlug: string drive: Drive file: File targetDirectory: DirectoryInfoWithPath @@ -28,7 +30,7 @@ export async function uploadFile({ }) { const [, upload] = await fetchApi( "POST", - `/drives/${drive.id}/uploads`, + `/${orgSlug}/drives/${drive.id}/uploads`, { body: JSON.stringify({ name: file.name, @@ -45,12 +47,16 @@ export async function uploadFile({ onProgress, }) - await fetchApi("PATCH", `/drives/${drive.id}/uploads/${upload.id}`, { - body: JSON.stringify({ - status: "completed", - }), - returns: Upload, - }) + await fetchApi( + "PATCH", + `/${orgSlug}/drives/${drive.id}/uploads/${upload.id}`, + { + body: JSON.stringify({ + status: "completed", + }), + returns: Upload, + }, + ) return upload } diff --git a/apps/drive-web/src/lib/api.ts b/apps/drive-web/src/lib/api.ts index c14dcb5..9812de2 100644 --- a/apps/drive-web/src/lib/api.ts +++ b/apps/drive-web/src/lib/api.ts @@ -5,25 +5,27 @@ export type ApiRoute = | "/auth/tokens" | "/accounts" | `/accounts/${string}` - | "/drives" - | `/drives/${string}` - | `/drives/${string}/uploads` - | `/drives/${string}/uploads/${string}/content` - | `/drives/${string}/uploads/${string}` - | `/drives/${string}/files${string}` - | `/drives/${string}/files/${string}` - | `/drives/${string}/files/${string}/content` - | `/drives/${string}/files/${string}/shares${string}` - | `/drives/${string}/directories` - | `/drives/${string}/directories/${string}` - | `/drives/${string}/directories/${string}/content` - | `/drives/${string}/directories/${string}/shares${string}` - | `/drives/${string}/shares` - | `/drives/${string}/shares/${string}` + | `/${string}/drives` + | `/${string}/drives/${string}` + | `/${string}/drives/${string}/uploads` + | `/${string}/drives/${string}/uploads/${string}/content` + | `/${string}/drives/${string}/uploads/${string}` + | `/${string}/drives/${string}/files${string}` + | `/${string}/drives/${string}/files/${string}` + | `/${string}/drives/${string}/files/${string}/content` + | `/${string}/drives/${string}/files/${string}/shares${string}` + | `/${string}/drives/${string}/directories${string}` + | `/${string}/drives/${string}/directories/${string}` + | `/${string}/drives/${string}/directories/${string}/content` + | `/${string}/drives/${string}/directories/${string}/shares${string}` + | `/${string}/drives/${string}/shares` + | `/${string}/drives/${string}/shares/${string}` | `/shares/${string}` | `/shares/${string}/directories${string}` | `/shares/${string}/files${string}` + | `/organizations/${string}` | "/users/me" + | "/users/me/organizations" export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" @@ -44,11 +46,7 @@ export class ApiError extends Error { export const Nothing = type({}) export type Nothing = typeof Nothing.infer -export async function fetchApi( - method: HttpMethod, - route: ApiRoute, - init: RequestInit & { returns: Schema }, -): Promise<[response: Response, data: Schema["inferOut"]]> { +export function buildApiUrl(route: ApiRoute): URL { let path: string if (baseApiUrl.pathname) { if (baseApiUrl.pathname.endsWith("/")) { @@ -59,7 +57,15 @@ export async function fetchApi( } else { path = route } - const url = new URL(path, baseApiUrl) + return new URL(path, baseApiUrl) +} + +export async function fetchApi( + method: HttpMethod, + route: ApiRoute, + init: RequestInit & { returns: Schema }, +): Promise<[response: Response, data: Schema["inferOut"]]> { + const url = buildApiUrl(route) const response = await fetch(url, { credentials: "include", ...init, diff --git a/apps/drive-web/src/organization/api.ts b/apps/drive-web/src/organization/api.ts new file mode 100644 index 0000000..262b2c0 --- /dev/null +++ b/apps/drive-web/src/organization/api.ts @@ -0,0 +1,32 @@ +import { queryOptions, skipToken } from "@tanstack/react-query" +import { fetchApi } from "@/lib/api" +import { Organization } from "./organization" +import { Drive } from "@/drive/drive" + +export const organizationQuery = (orgSlug: string | null) => + queryOptions({ + queryKey: ["organizations", orgSlug], + queryFn: orgSlug + ? () => + fetchApi("GET", `/organizations/${orgSlug}`, { + returns: Organization, + }).then(([, data]) => data) + : skipToken, + }) + +export const listOrganizationsQuery = queryOptions({ + queryKey: ["organizations"], + queryFn: () => + fetchApi("GET", "/users/me/organizations", { + returns: Organization.array(), + }).then(([, data]) => data), +}) + +export const listOrganizationDrivesQuery = (orgSlug: string) => + queryOptions({ + queryKey: ["organizations", orgSlug, "drives"], + queryFn: () => + fetchApi("GET", `/${orgSlug}/drives`, { + returns: Drive.array(), + }).then(([, data]) => data), + }) diff --git a/apps/drive-web/src/organization/context.ts b/apps/drive-web/src/organization/context.ts new file mode 100644 index 0000000..afa82e6 --- /dev/null +++ b/apps/drive-web/src/organization/context.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react" +import type { Organization } from "./organization" + +export const OrganizationContext = createContext( + null as unknown as Organization, +) + +export function useCurrentOrganization() { + const org = useContext(OrganizationContext) + if (!org) { + throw new Error( + "useCurrentOrganization must be used under /$orgSlug routes", + ) + } + return org +} diff --git a/apps/drive-web/src/organization/organization.ts b/apps/drive-web/src/organization/organization.ts new file mode 100644 index 0000000..7024448 --- /dev/null +++ b/apps/drive-web/src/organization/organization.ts @@ -0,0 +1,18 @@ +import { type } from "arktype" + +export const ORGANIZATION_KIND = { + personal: "personal", + team: "team", +} as const +export type OrganizationKind = + (typeof ORGANIZATION_KIND)[keyof typeof ORGANIZATION_KIND] + +export const Organization = type({ + id: "string", + kind: type.valueOf(ORGANIZATION_KIND), + name: "string", + slug: "string", + createdAt: "string.date.iso.parse", + updatedAt: "string.date.iso.parse", +}) +export type Organization = typeof Organization.infer diff --git a/apps/drive-web/src/query-client.ts b/apps/drive-web/src/query-client.ts new file mode 100644 index 0000000..34c8b1f --- /dev/null +++ b/apps/drive-web/src/query-client.ts @@ -0,0 +1,14 @@ +import { QueryClient } from "@tanstack/react-query" +import { defaultOnError } from "./lib/error" + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + throwOnError: false, + }, + mutations: { + onError: defaultOnError, + throwOnError: false, + }, + }, +}) diff --git a/apps/drive-web/src/routeTree.gen.ts b/apps/drive-web/src/routeTree.gen.ts index a962981..82ca3e4 100644 --- a/apps/drive-web/src/routeTree.gen.ts +++ b/apps/drive-web/src/routeTree.gen.ts @@ -8,16 +8,23 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. +import { createFileRoute } from '@tanstack/react-router' + import { Route as rootRouteImport } from './routes/__root' import { Route as SignUpRouteImport } from './routes/sign-up' import { Route as LoginRouteImport } from './routes/login' import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index' import { Route as LoginCallbackRouteImport } from './routes/login_.callback' -import { Route as AuthenticatedSidebarLayoutRouteImport } from './routes/_authenticated/_sidebar-layout' -import { Route as AuthenticatedSidebarLayoutHomeRouteImport } from './routes/_authenticated/_sidebar-layout/home' +import { Route as AuthenticatedOrgSlugSidebarLayoutRouteImport } from './routes/_authenticated/$orgSlug/_sidebar-layout' +import { Route as AuthenticatedOrgSlugSidebarLayoutIndexRouteImport } from './routes/_authenticated/$orgSlug/_sidebar-layout/index' import { Route as SharesShareIdDirectoriesDirectoryIdRouteImport } from './routes/shares/$shareId.directories.$directoryId' -import { Route as AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/directories.$directoryId' +import { Route as AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRouteImport } from './routes/_authenticated/$orgSlug/_sidebar-layout/drives.$driveId' +import { Route as AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/$orgSlug/_sidebar-layout/directories.$directoryId' + +const AuthenticatedOrgSlugRouteImport = createFileRoute( + '/_authenticated/$orgSlug', +)() const SignUpRoute = SignUpRouteImport.update({ id: '/sign-up', @@ -33,6 +40,11 @@ const AuthenticatedRoute = AuthenticatedRouteImport.update({ id: '/_authenticated', getParentRoute: () => rootRouteImport, } as any) +const AuthenticatedOrgSlugRoute = AuthenticatedOrgSlugRouteImport.update({ + id: '/$orgSlug', + path: '/$orgSlug', + getParentRoute: () => AuthenticatedRoute, +} as any) const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({ id: '/', path: '/', @@ -43,16 +55,16 @@ const LoginCallbackRoute = LoginCallbackRouteImport.update({ path: '/login/callback', getParentRoute: () => rootRouteImport, } as any) -const AuthenticatedSidebarLayoutRoute = - AuthenticatedSidebarLayoutRouteImport.update({ +const AuthenticatedOrgSlugSidebarLayoutRoute = + AuthenticatedOrgSlugSidebarLayoutRouteImport.update({ id: '/_sidebar-layout', - getParentRoute: () => AuthenticatedRoute, + getParentRoute: () => AuthenticatedOrgSlugRoute, } as any) -const AuthenticatedSidebarLayoutHomeRoute = - AuthenticatedSidebarLayoutHomeRouteImport.update({ - id: '/home', - path: '/home', - getParentRoute: () => AuthenticatedSidebarLayoutRoute, +const AuthenticatedOrgSlugSidebarLayoutIndexRoute = + AuthenticatedOrgSlugSidebarLayoutIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AuthenticatedOrgSlugSidebarLayoutRoute, } as any) const SharesShareIdDirectoriesDirectoryIdRoute = SharesShareIdDirectoriesDirectoryIdRouteImport.update({ @@ -60,11 +72,17 @@ const SharesShareIdDirectoriesDirectoryIdRoute = path: '/shares/$shareId/directories/$directoryId', getParentRoute: () => rootRouteImport, } as any) -const AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute = - AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport.update({ +const AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute = + AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRouteImport.update({ + id: '/drives/$driveId', + path: '/drives/$driveId', + getParentRoute: () => AuthenticatedOrgSlugSidebarLayoutRoute, + } as any) +const AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute = + AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRouteImport.update({ id: '/directories/$directoryId', path: '/directories/$directoryId', - getParentRoute: () => AuthenticatedSidebarLayoutRoute, + getParentRoute: () => AuthenticatedOrgSlugSidebarLayoutRoute, } as any) export interface FileRoutesByFullPath { @@ -72,30 +90,35 @@ export interface FileRoutesByFullPath { '/sign-up': typeof SignUpRoute '/login/callback': typeof LoginCallbackRoute '/': typeof AuthenticatedIndexRoute - '/home': typeof AuthenticatedSidebarLayoutHomeRoute - '/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute + '/$orgSlug': typeof AuthenticatedOrgSlugSidebarLayoutRouteWithChildren '/shares/$shareId/directories/$directoryId': typeof SharesShareIdDirectoriesDirectoryIdRoute + '/$orgSlug/': typeof AuthenticatedOrgSlugSidebarLayoutIndexRoute + '/$orgSlug/directories/$directoryId': typeof AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute + '/$orgSlug/drives/$driveId': typeof AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute } export interface FileRoutesByTo { '/login': typeof LoginRoute '/sign-up': typeof SignUpRoute '/login/callback': typeof LoginCallbackRoute '/': typeof AuthenticatedIndexRoute - '/home': typeof AuthenticatedSidebarLayoutHomeRoute - '/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute + '/$orgSlug': typeof AuthenticatedOrgSlugSidebarLayoutIndexRoute '/shares/$shareId/directories/$directoryId': typeof SharesShareIdDirectoriesDirectoryIdRoute + '/$orgSlug/directories/$directoryId': typeof AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute + '/$orgSlug/drives/$driveId': typeof AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/_authenticated': typeof AuthenticatedRouteWithChildren '/login': typeof LoginRoute '/sign-up': typeof SignUpRoute - '/_authenticated/_sidebar-layout': typeof AuthenticatedSidebarLayoutRouteWithChildren '/login_/callback': typeof LoginCallbackRoute '/_authenticated/': typeof AuthenticatedIndexRoute - '/_authenticated/_sidebar-layout/home': typeof AuthenticatedSidebarLayoutHomeRoute - '/_authenticated/_sidebar-layout/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute + '/_authenticated/$orgSlug': typeof AuthenticatedOrgSlugRouteWithChildren + '/_authenticated/$orgSlug/_sidebar-layout': typeof AuthenticatedOrgSlugSidebarLayoutRouteWithChildren '/shares/$shareId/directories/$directoryId': typeof SharesShareIdDirectoriesDirectoryIdRoute + '/_authenticated/$orgSlug/_sidebar-layout/': typeof AuthenticatedOrgSlugSidebarLayoutIndexRoute + '/_authenticated/$orgSlug/_sidebar-layout/directories/$directoryId': typeof AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute + '/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId': typeof AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -104,29 +127,34 @@ export interface FileRouteTypes { | '/sign-up' | '/login/callback' | '/' - | '/home' - | '/directories/$directoryId' + | '/$orgSlug' | '/shares/$shareId/directories/$directoryId' + | '/$orgSlug/' + | '/$orgSlug/directories/$directoryId' + | '/$orgSlug/drives/$driveId' fileRoutesByTo: FileRoutesByTo to: | '/login' | '/sign-up' | '/login/callback' | '/' - | '/home' - | '/directories/$directoryId' + | '/$orgSlug' | '/shares/$shareId/directories/$directoryId' + | '/$orgSlug/directories/$directoryId' + | '/$orgSlug/drives/$driveId' id: | '__root__' | '/_authenticated' | '/login' | '/sign-up' - | '/_authenticated/_sidebar-layout' | '/login_/callback' | '/_authenticated/' - | '/_authenticated/_sidebar-layout/home' - | '/_authenticated/_sidebar-layout/directories/$directoryId' + | '/_authenticated/$orgSlug' + | '/_authenticated/$orgSlug/_sidebar-layout' | '/shares/$shareId/directories/$directoryId' + | '/_authenticated/$orgSlug/_sidebar-layout/' + | '/_authenticated/$orgSlug/_sidebar-layout/directories/$directoryId' + | '/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -160,6 +188,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedRouteImport parentRoute: typeof rootRouteImport } + '/_authenticated/$orgSlug': { + id: '/_authenticated/$orgSlug' + path: '/$orgSlug' + fullPath: '/$orgSlug' + preLoaderRoute: typeof AuthenticatedOrgSlugRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/': { id: '/_authenticated/' path: '/' @@ -174,19 +209,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginCallbackRouteImport parentRoute: typeof rootRouteImport } - '/_authenticated/_sidebar-layout': { - id: '/_authenticated/_sidebar-layout' - path: '' - fullPath: '' - preLoaderRoute: typeof AuthenticatedSidebarLayoutRouteImport - parentRoute: typeof AuthenticatedRoute + '/_authenticated/$orgSlug/_sidebar-layout': { + id: '/_authenticated/$orgSlug/_sidebar-layout' + path: '/$orgSlug' + fullPath: '/$orgSlug' + preLoaderRoute: typeof AuthenticatedOrgSlugSidebarLayoutRouteImport + parentRoute: typeof AuthenticatedOrgSlugRoute } - '/_authenticated/_sidebar-layout/home': { - id: '/_authenticated/_sidebar-layout/home' - path: '/home' - fullPath: '/home' - preLoaderRoute: typeof AuthenticatedSidebarLayoutHomeRouteImport - parentRoute: typeof AuthenticatedSidebarLayoutRoute + '/_authenticated/$orgSlug/_sidebar-layout/': { + id: '/_authenticated/$orgSlug/_sidebar-layout/' + path: '/' + fullPath: '/$orgSlug/' + preLoaderRoute: typeof AuthenticatedOrgSlugSidebarLayoutIndexRouteImport + parentRoute: typeof AuthenticatedOrgSlugSidebarLayoutRoute } '/shares/$shareId/directories/$directoryId': { id: '/shares/$shareId/directories/$directoryId' @@ -195,41 +230,64 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SharesShareIdDirectoriesDirectoryIdRouteImport parentRoute: typeof rootRouteImport } - '/_authenticated/_sidebar-layout/directories/$directoryId': { - id: '/_authenticated/_sidebar-layout/directories/$directoryId' + '/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId': { + id: '/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId' + path: '/drives/$driveId' + fullPath: '/$orgSlug/drives/$driveId' + preLoaderRoute: typeof AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRouteImport + parentRoute: typeof AuthenticatedOrgSlugSidebarLayoutRoute + } + '/_authenticated/$orgSlug/_sidebar-layout/directories/$directoryId': { + id: '/_authenticated/$orgSlug/_sidebar-layout/directories/$directoryId' path: '/directories/$directoryId' - fullPath: '/directories/$directoryId' - preLoaderRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport - parentRoute: typeof AuthenticatedSidebarLayoutRoute + fullPath: '/$orgSlug/directories/$directoryId' + preLoaderRoute: typeof AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRouteImport + parentRoute: typeof AuthenticatedOrgSlugSidebarLayoutRoute } } } -interface AuthenticatedSidebarLayoutRouteChildren { - AuthenticatedSidebarLayoutHomeRoute: typeof AuthenticatedSidebarLayoutHomeRoute - AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute +interface AuthenticatedOrgSlugSidebarLayoutRouteChildren { + AuthenticatedOrgSlugSidebarLayoutIndexRoute: typeof AuthenticatedOrgSlugSidebarLayoutIndexRoute + AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute + AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute: typeof AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute } -const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteChildren = +const AuthenticatedOrgSlugSidebarLayoutRouteChildren: AuthenticatedOrgSlugSidebarLayoutRouteChildren = { - AuthenticatedSidebarLayoutHomeRoute: AuthenticatedSidebarLayoutHomeRoute, - AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: - AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute, + AuthenticatedOrgSlugSidebarLayoutIndexRoute: + AuthenticatedOrgSlugSidebarLayoutIndexRoute, + AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute: + AuthenticatedOrgSlugSidebarLayoutDirectoriesDirectoryIdRoute, + AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute: + AuthenticatedOrgSlugSidebarLayoutDrivesDriveIdRoute, } -const AuthenticatedSidebarLayoutRouteWithChildren = - AuthenticatedSidebarLayoutRoute._addFileChildren( - AuthenticatedSidebarLayoutRouteChildren, +const AuthenticatedOrgSlugSidebarLayoutRouteWithChildren = + AuthenticatedOrgSlugSidebarLayoutRoute._addFileChildren( + AuthenticatedOrgSlugSidebarLayoutRouteChildren, ) +interface AuthenticatedOrgSlugRouteChildren { + AuthenticatedOrgSlugSidebarLayoutRoute: typeof AuthenticatedOrgSlugSidebarLayoutRouteWithChildren +} + +const AuthenticatedOrgSlugRouteChildren: AuthenticatedOrgSlugRouteChildren = { + AuthenticatedOrgSlugSidebarLayoutRoute: + AuthenticatedOrgSlugSidebarLayoutRouteWithChildren, +} + +const AuthenticatedOrgSlugRouteWithChildren = + AuthenticatedOrgSlugRoute._addFileChildren(AuthenticatedOrgSlugRouteChildren) + interface AuthenticatedRouteChildren { - AuthenticatedSidebarLayoutRoute: typeof AuthenticatedSidebarLayoutRouteWithChildren AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute + AuthenticatedOrgSlugRoute: typeof AuthenticatedOrgSlugRouteWithChildren } const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { - AuthenticatedSidebarLayoutRoute: AuthenticatedSidebarLayoutRouteWithChildren, AuthenticatedIndexRoute: AuthenticatedIndexRoute, + AuthenticatedOrgSlugRoute: AuthenticatedOrgSlugRouteWithChildren, } const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( diff --git a/apps/drive-web/src/router.ts b/apps/drive-web/src/router.ts new file mode 100644 index 0000000..94de71b --- /dev/null +++ b/apps/drive-web/src/router.ts @@ -0,0 +1,14 @@ +import { createRouter } from "@tanstack/react-router" +import { queryClient } from "./query-client" +import { routeTree } from "./routeTree.gen" + +export type RouterContext = { + queryClient: typeof queryClient +} + +export const router = createRouter({ + routeTree, + context: { + queryClient, + }, +}) diff --git a/apps/drive-web/src/routes/__root.tsx b/apps/drive-web/src/routes/__root.tsx index c97f0ee..0472251 100644 --- a/apps/drive-web/src/routes/__root.tsx +++ b/apps/drive-web/src/routes/__root.tsx @@ -1,30 +1,19 @@ import "@/styles/globals.css" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { createRootRoute, Outlet } from "@tanstack/react-router" +import { QueryClientProvider } from "@tanstack/react-query" +import { createRootRouteWithContext, Outlet } from "@tanstack/react-router" import { Provider } from "jotai" import { useHydrateAtoms } from "jotai/utils" import { queryClientAtom } from "jotai-tanstack-query" import type React from "react" import { Toaster } from "@/components/ui/sonner" -import { defaultOnError } from "@/lib/error" import { useKeyboardModifierListener } from "@/lib/keyboard" +import { queryClient } from "@/query-client" +import type { RouterContext } from "@/router" -export const Route = createRootRoute({ +export const Route = createRootRouteWithContext()({ component: RootLayout, }) -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - throwOnError: false, - }, - mutations: { - onError: defaultOnError, - throwOnError: false, - }, - }, -}) - function HydrateAtoms({ children }: React.PropsWithChildren) { useHydrateAtoms(new Map([[queryClientAtom, queryClient]])) return children diff --git a/apps/drive-web/src/routes/_authenticated.tsx b/apps/drive-web/src/routes/_authenticated.tsx index 985dded..5fb24a8 100644 --- a/apps/drive-web/src/routes/_authenticated.tsx +++ b/apps/drive-web/src/routes/_authenticated.tsx @@ -1,31 +1,22 @@ +import { useQuery } from "@tanstack/react-query" import { createFileRoute, Navigate, Outlet } from "@tanstack/react-router" -import { useAtomValue } from "jotai" -import { atomEffect } from "jotai-effect" -import { atomWithQuery } from "jotai-tanstack-query" import { LoadingSpinner } from "@/components/ui/loading-spinner" -import { drivesQuery } from "@/drive/api" -import { currentDriveAtom } from "@/drive/drive" +import { listOrganizationsQuery } from "@/organization/api" +import { ORGANIZATION_KIND } from "@/organization/organization" export const Route = createFileRoute("/_authenticated")({ component: AuthenticatedLayout, -}) - -const drivesAtom = atomWithQuery(() => drivesQuery) -const selectFirstDriveEffect = atomEffect((get, set) => { - const { data: drives } = get(drivesAtom) - const firstDrive = drives?.[0] - if (firstDrive && get.peek(currentDriveAtom) === null) { - set(currentDriveAtom, firstDrive) - } + loader: ({ context }) => { + context.queryClient.ensureQueryData(listOrganizationsQuery) + }, }) function AuthenticatedLayout() { - const { data: drives, isLoading: isLoadingDrives } = - useAtomValue(drivesAtom) + const { data: orgs, isLoading: isLoadingOrgs } = useQuery( + listOrganizationsQuery, + ) - useAtomValue(selectFirstDriveEffect) - - if (isLoadingDrives) { + if (isLoadingOrgs) { return (
@@ -33,7 +24,11 @@ function AuthenticatedLayout() { ) } - if (!drives) { + const personalOrg = orgs?.find( + (org) => org.kind === ORGANIZATION_KIND.personal, + ) + + if (!personalOrg) { return } diff --git a/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout.tsx b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout.tsx new file mode 100644 index 0000000..48d5255 --- /dev/null +++ b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout.tsx @@ -0,0 +1,52 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { createFileRoute, Outlet } from "@tanstack/react-router" +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { DashboardSidebar } from "@/dashboard/dashboard-sidebar" +import { listOrganizationsQuery, organizationQuery } from "@/organization/api" +import { OrganizationContext } from "@/organization/context" + +export const Route = createFileRoute( + "/_authenticated/$orgSlug/_sidebar-layout", +)({ + component: RouteComponent, + loader: ({ context, params }) => { + context.queryClient.ensureQueryData(organizationQuery(params.orgSlug)) + }, +}) + +function RouteComponent() { + const { orgSlug } = Route.useParams() + + const client = useQueryClient() + const { data: org, isLoading } = useQuery({ + ...organizationQuery(orgSlug), + initialData: () => + client + .getQueryData(listOrganizationsQuery.queryKey) + ?.find((org) => org.slug === orgSlug), + }) + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (!org) return null + + return ( + + +
+ + + + +
+
+
+ ) +} diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/directories.$directoryId.tsx similarity index 92% rename from apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx rename to apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/directories.$directoryId.tsx index 571b33b..af92b46 100644 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx +++ b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/directories.$directoryId.tsx @@ -15,6 +15,7 @@ import { import { lazy, Suspense, useCallback, useContext } from "react" import { toast } from "sonner" import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" import { DirectoryIcon } from "@/components/icons/directory-icon" import { TextFileIcon } from "@/components/icons/text-file-icon" import { Button } from "@/components/ui/button" @@ -54,10 +55,10 @@ import type { import { DIRECTORY_CONTENT_ORDER_BY, DIRECTORY_CONTENT_ORDER_DIRECTION, - directoryContentQueryAtom, + directoryContentQuery, directoryContentQueryKey, - directoryInfoQueryAtom, - moveToTrashMutationAtom, + directoryInfoQuery, + moveToTrashMutationOptions, } from "@/vfs/api" import { optimisticallyRemoveDirectoryItems, @@ -69,7 +70,7 @@ import type { DirectoryItem, FileInfo, } from "@/vfs/vfs" -import { ItemShareDialog } from "../../../sharing/item-share-dialog" +import { ItemShareDialog } from "@/sharing/item-share-dialog" // Conditional lazy import - Vite will tree-shake this entire import in production // because import.meta.env.DEV is evaluated at build time @@ -93,7 +94,7 @@ const DirectoryContentPageParams = type({ }) export const Route = createFileRoute( - "/_authenticated/_sidebar-layout/directories/$directoryId", + "/_authenticated/$orgSlug/_sidebar-layout/directories/$directoryId", )({ validateSearch: DirectoryContentPageParams, component: RouteComponent, @@ -144,14 +145,16 @@ const mockTableAtom = import.meta.env.DEV // MARK: page entry function RouteComponent() { - const { directoryId } = Route.useParams() + const { directoryId, orgSlug } = Route.useParams() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const { data: directoryInfo, isLoading: isLoadingDirectoryInfo } = useQuery( - useAtomValue(directoryInfoQueryAtom(directoryId)), + directoryInfoQuery({ org, drive, directoryId }), ) const directoryUrlById = useCallback( - (directoryId: string) => `/directories/${directoryId}`, - [], + (directoryId: string) => `/${orgSlug}/directories/${directoryId}`, + [orgSlug], ) if (isLoadingDirectoryInfo) { @@ -260,16 +263,18 @@ function _DirectoryContentTable() { const useMock = useAtomValue(mockTableAtom) const { directory } = useContext(DirectoryPageContext) const navigate = Route.useNavigate() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const search = Route.useSearch() - const query = useAtomValue( - directoryContentQueryAtom({ - directoryId: directory.id, - orderBy: search.orderBy, - direction: search.direction, - limit: 100, - }), - ) + const query = directoryContentQuery({ + org, + drive, + directoryId: directory.id, + orderBy: search.orderBy, + direction: search.direction, + limit: 100, + }) const setOpenedFile = useSetAtom(openedFileAtom) const setContextMenuTargetItems = useSetAtom(contextMenuTargetItemsAtom) @@ -281,9 +286,10 @@ function _DirectoryContentTable() { [setOpenedFile], ) + const { orgSlug } = Route.useParams() const directoryUrlFn = useCallback( - (directory: DirectoryInfo) => `/directories/${directory.id}`, - [], + (directory: DirectoryInfo) => `/${orgSlug}/directories/${directory.id}`, + [orgSlug], ) const handleContextMenuRequest = useCallback( @@ -379,11 +385,12 @@ function DirectoryContentContextMenu({ const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom) const setCutItems = useSetAtom(cutItemsAtom) const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const { directory } = useContext(DirectoryPageContext) const search = Route.useSearch() const setActiveDialogData = useSetAtom(activeDialogDataAtom) - const moveToTrashMutation = useAtomValue(moveToTrashMutationAtom) + const moveToTrashMutation = moveToTrashMutationOptions({ org, drive }) const { mutate: moveToTrash } = useMutation({ ...moveToTrashMutation, onMutate: (items, { client }) => { @@ -394,7 +401,7 @@ function DirectoryContentContextMenu({ return null } return optimisticallyRemoveDirectoryItems(client, { - queryKey: directoryContentQueryKey(drive.id, directory.id, { + queryKey: directoryContentQueryKey(org.slug, drive.id, directory.id, { orderBy: search.orderBy, direction: search.direction, }), diff --git a/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/drives.$driveId.tsx b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/drives.$driveId.tsx new file mode 100644 index 0000000..d1df98b --- /dev/null +++ b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/drives.$driveId.tsx @@ -0,0 +1,25 @@ +import { listOrganizationDrivesQuery } from "@/organization/api" +import { useQuery } from "@tanstack/react-query" +import { createFileRoute } from "@tanstack/react-router" + +export const Route = createFileRoute( + "/_authenticated/$orgSlug/_sidebar-layout/drives/$driveId", +)({ + component: RouteComponent, + loader: ({ context, params }) => { + context.queryClient.ensureQueryData( + listOrganizationDrivesQuery(params.orgSlug), + ) + }, +}) + +function RouteComponent() { + const { orgSlug } = Route.useParams() + const { data: drives } = useQuery(listOrganizationDrivesQuery(orgSlug)) + + return ( +
+ Hello "/_authenticated/$orgSlug/_sidebar-layout/drives/$driveSlug"! +
+ ) +} diff --git a/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/index.tsx b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/index.tsx new file mode 100644 index 0000000..c233a4b --- /dev/null +++ b/apps/drive-web/src/routes/_authenticated/$orgSlug/_sidebar-layout/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from "@tanstack/react-router" +import { useCurrentOrganization } from "@/organization/context" + +export const Route = createFileRoute( + "/_authenticated/$orgSlug/_sidebar-layout/", +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const org = useCurrentOrganization() + return
Home of organization "{org.name}"
+} diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout.tsx deleted file mode 100644 index a4f8e57..0000000 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createFileRoute, Outlet } from "@tanstack/react-router" -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" -import { DashboardSidebar } from "@/dashboard/dashboard-sidebar" - -export const Route = createFileRoute("/_authenticated/_sidebar-layout")({ - component: RouteComponent, -}) - -function RouteComponent() { - return ( - -
- - - - -
-
- ) -} diff --git a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/home.tsx b/apps/drive-web/src/routes/_authenticated/_sidebar-layout/home.tsx deleted file mode 100644 index 726856f..0000000 --- a/apps/drive-web/src/routes/_authenticated/_sidebar-layout/home.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router" - -export const Route = createFileRoute("/_authenticated/_sidebar-layout/home")({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/_authenticated/home"!
-} diff --git a/apps/drive-web/src/routes/_authenticated/index.tsx b/apps/drive-web/src/routes/_authenticated/index.tsx index 96d34c6..7957a5d 100644 --- a/apps/drive-web/src/routes/_authenticated/index.tsx +++ b/apps/drive-web/src/routes/_authenticated/index.tsx @@ -1,9 +1,40 @@ +import { useQuery } from "@tanstack/react-query" import { createFileRoute, Navigate } from "@tanstack/react-router" +import { LoadingSpinner } from "@/components/ui/loading-spinner" +import { listOrganizationsQuery } from "@/organization/api" +import { ORGANIZATION_KIND } from "@/organization/organization" export const Route = createFileRoute("/_authenticated/")({ component: RouteComponent, + loader: ({ context }) => { + void context.queryClient.ensureQueryData(listOrganizationsQuery) + }, }) function RouteComponent() { - return + const { data: orgs, isLoading } = useQuery(listOrganizationsQuery) + + if (isLoading) { + return ( +
+ +
+ ) + } + + const personalOrg = orgs?.find( + (org) => org.kind === ORGANIZATION_KIND.personal, + ) + + if (!personalOrg) { + return + } + + return ( + + ) } diff --git a/apps/drive-web/src/routes/login.tsx b/apps/drive-web/src/routes/login.tsx index 292b77b..01c6a13 100644 --- a/apps/drive-web/src/routes/login.tsx +++ b/apps/drive-web/src/routes/login.tsx @@ -19,6 +19,9 @@ import { } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" +import { useStore } from "jotai" +import { ORGANIZATION_KIND } from "@/organization/organization" +import { organizationQuery } from "@/organization/api" export const Route = createFileRoute("/login")({ component: RouteComponent, @@ -73,10 +76,19 @@ function LoginForm() { ...loginMutation, onSuccess: (data, vars, result, context) => { loginMutation.onSuccess?.(data, vars, result, context) - navigate({ - to: "/", - replace: true, - }) + const personalOrg = data.organizations.find( + (org) => org.kind === ORGANIZATION_KIND.personal, + ) + if (personalOrg) { + context.client.ensureQueryData( + organizationQuery(personalOrg.slug), + ) + navigate({ + to: "/$orgSlug", + params: { orgSlug: personalOrg.slug }, + replace: true, + }) + } }, }) diff --git a/apps/drive-web/src/sharing/api.ts b/apps/drive-web/src/sharing/api.ts index 78dc67b..1c086e9 100644 --- a/apps/drive-web/src/sharing/api.ts +++ b/apps/drive-web/src/sharing/api.ts @@ -1,53 +1,81 @@ import { mutationOptions, queryOptions, skipToken } from "@tanstack/react-query" -import { atom } from "jotai" -import { atomFamily } from "jotai/utils" import { fetchApi, Nothing } from "@/lib/api" -import { currentDriveAtom } from "@/drive/drive" +import { type Drive } from "@/drive/drive" +import type { Organization } from "@/organization/organization" import { Share } from "./share" -export const fileSharesQueryAtom = atomFamily((fileId: string) => - atom((get) => { - const drive = get(currentDriveAtom) - return queryOptions({ - queryKey: ["drives", drive?.id, "shares", { fileId }], - queryFn: drive +export const fileSharesQuery = ({ + org, + drive, + fileId, +}: { + org: Organization | null + drive: Drive | null + fileId: string +}) => + queryOptions({ + queryKey: [ + "organizations", + org?.slug, + "drives", + drive?.id, + "shares", + { fileId }, + ], + queryFn: + org && drive ? () => fetchApi( "GET", - `/drives/${drive.id}/files/${fileId}/shares?includesExpired=true`, + `/${org.slug}/drives/${drive.id}/files/${fileId}/shares?includesExpired=true`, { returns: Share.array() }, ).then(([_, result]) => result) : skipToken, - }) - }), -) + }) -export const directorySharesQueryAtom = atomFamily((directoryId: string) => - atom((get) => { - const drive = get(currentDriveAtom) - return queryOptions({ - queryKey: ["drives", drive?.id, "shares", { directoryId }], - queryFn: drive +export const directorySharesQuery = ({ + org, + drive, + directoryId, +}: { + org: Organization | null + drive: Drive | null + directoryId: string +}) => + queryOptions({ + queryKey: [ + "organizations", + org?.slug, + "drives", + drive?.id, + "shares", + { directoryId }, + ], + queryFn: + org && drive ? () => fetchApi( "GET", - `/drives/${drive.id}/directories/${directoryId}/shares?includesExpired=true`, + `/${org.slug}/drives/${drive.id}/directories/${directoryId}/shares?includesExpired=true`, { returns: Share.array() }, ).then(([_, result]) => result) - : skipToken, - }) - }), -) + : skipToken, + }) -export const createShareMutationAtom = atom((get) => +export const createShareMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async ({ items }: { items: string[] }) => { - const drive = get(currentDriveAtom) if (!drive) throw new Error("No drive selected") const [_, result] = await fetchApi( "POST", - `/drives/${drive.id}/shares`, + `/${org.slug}/drives/${drive.id}/shares`, { body: JSON.stringify({ items }), returns: Share, @@ -56,25 +84,34 @@ export const createShareMutationAtom = atom((get) => return result }, - }), -) + }) -export const deleteShareMutationAtom = atom((get) => +export const deleteShareMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async ({ shareId }: { shareId: string }) => { - const drive = get(currentDriveAtom) if (!drive) throw new Error("No drive selected") await fetchApi( "DELETE", - `/drives/${drive.id}/shares/${shareId}`, + `/${org.slug}/drives/${drive.id}/shares/${shareId}`, { returns: Nothing }, ) }, - }), -) + }) -export const updateShareMutationAtom = atom((get) => +export const updateShareMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async ({ shareId, @@ -83,14 +120,12 @@ export const updateShareMutationAtom = atom((get) => shareId: string expiresAt?: Date | null }) => { - const drive = get(currentDriveAtom) if (!drive) throw new Error("No drive selected") await fetchApi( "PATCH", - `/drives/${drive.id}/shares/${shareId}`, + `/${org.slug}/drives/${drive.id}/shares/${shareId}`, { body: JSON.stringify({ expiresAt }), returns: Share }, ) }, - }), -) + }) diff --git a/apps/drive-web/src/sharing/item-share-dialog.tsx b/apps/drive-web/src/sharing/item-share-dialog.tsx index cde205e..629ba50 100644 --- a/apps/drive-web/src/sharing/item-share-dialog.tsx +++ b/apps/drive-web/src/sharing/item-share-dialog.tsx @@ -1,11 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query" -import { - atom, - type PrimitiveAtom, - useAtomValue, - useSetAtom, - useStore, -} from "jotai" +import { atom, type PrimitiveAtom, useAtomValue, useSetAtom } from "jotai" import { CheckIcon, CopyIcon, @@ -59,14 +53,17 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" +import { currentDriveAtom, type Drive } from "@/drive/drive" import { copyToClipboardMutation } from "@/lib/clipboard" import { cn } from "@/lib/utils" +import type { Organization } from "@/organization/organization" +import { useCurrentOrganization } from "@/organization/context" import { - createShareMutationAtom, - deleteShareMutationAtom, - directorySharesQueryAtom, - fileSharesQueryAtom, - updateShareMutationAtom, + createShareMutationOptions, + deleteShareMutationOptions, + directorySharesQuery, + fileSharesQuery, + updateShareMutationOptions, } from "@/sharing/api" import type { DirectoryItem } from "@/vfs/vfs" import type { Share } from "./share" @@ -118,9 +115,40 @@ export function ItemShareDialog({ item, open, onClose }: ItemShareDialogProps) { ) } +function itemSharesQueryKey( + item: DirectoryItem, + org: Organization | null, + drive: Drive | null, +): readonly unknown[] | null { + if (!org || !drive) return null + switch (item.kind) { + case "file": + return fileSharesQuery({ org, drive, fileId: item.id }).queryKey + case "directory": + return directorySharesQuery({ + org, + drive, + directoryId: item.id, + }).queryKey + default: + return null + } +} + function PublicAccessSection({ item }: { item: DirectoryItem }) { - const fileSharesQuery = useAtomValue(fileSharesQueryAtom(item.id)) - const directorySharesQuery = useAtomValue(directorySharesQueryAtom(item.id)) + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() + + const fileSharesQueryOptions = fileSharesQuery({ + org, + drive, + fileId: item.id, + }) + const directorySharesQueryOptions = directorySharesQuery({ + org, + drive, + directoryId: item.id, + }) const sortShares = (shares: Share[]) => [...shares].sort((a, b) => { @@ -131,13 +159,13 @@ function PublicAccessSection({ item }: { item: DirectoryItem }) { }) const { data: fileShares, isLoading: isLoadingFileShares } = useQuery({ - ...fileSharesQuery, + ...fileSharesQueryOptions, enabled: item.kind === "file", select: sortShares, }) const { data: directoryShares, isLoading: isLoadingDirectoryShares } = useQuery({ - ...directorySharesQuery, + ...directorySharesQueryOptions, enabled: item.kind === "directory", select: sortShares, }) @@ -301,24 +329,12 @@ function ShareLinkListItem({ share }: { share: Share }) { function AddShareLinkListItem() { const { item } = useContext(ItemShareDialogContext) - const store = useStore() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const { mutate: createShare, isPending: isCreatingShare } = useMutation({ - ...useAtomValue(createShareMutationAtom), + ...createShareMutationOptions({ org, drive }), onSuccess: (_createdShare, _vars, _, { client }) => { - let queryKey: readonly unknown[] | null - switch (item.kind) { - case "file": - queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey - break - case "directory": - queryKey = store.get( - directorySharesQueryAtom(item.id), - ).queryKey - break - default: - queryKey = null - break - } + const queryKey = itemSharesQueryKey(item, org, drive) if (queryKey) { client.invalidateQueries({ queryKey, @@ -417,7 +433,7 @@ function RenameShareLinkPopover({ }: React.PropsWithChildren<{ share: Share activePopoverAtom: PrimitiveAtom -}>) { + }>) { const setActivePopover = useSetAtom(activePopoverAtom) const inputId = `rename-share-link-${share.id}` @@ -486,27 +502,15 @@ function ConfigureShareLinkExpirationPopover({ share.expiresAt === null ? EXPIRATION_TYPE.never : EXPIRATION_TYPE.date, ) const { item } = useContext(ItemShareDialogContext) - const store = useStore() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const setActivePopover = useSetAtom(activePopoverAtom) const dateInputRef = useRef(null) const { mutate: updateShare, isPending: isUpdatingShare } = useMutation({ - ...useAtomValue(updateShareMutationAtom), + ...updateShareMutationOptions({ org, drive }), onSuccess: (_updatedShare, _vars, _, { client }) => { - let queryKey: readonly unknown[] | null - switch (item.kind) { - case "file": - queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey - break - case "directory": - queryKey = store.get( - directorySharesQueryAtom(item.id), - ).queryKey - break - default: - queryKey = null - break - } + const queryKey = itemSharesQueryKey(item, org, drive) if (queryKey) { client.invalidateQueries({ queryKey, @@ -617,28 +621,16 @@ function ShareLinkOptionsMenu({ }: React.PropsWithChildren<{ share: Share activePopoverAtom: PrimitiveAtom -}>) { + }>) { const { item } = useContext(ItemShareDialogContext) const setActivePopover = useSetAtom(activePopoverAtom) - const store = useStore() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const { mutate: deleteShare } = useMutation({ - ...useAtomValue(deleteShareMutationAtom), + ...deleteShareMutationOptions({ org, drive }), onMutate: ({ shareId }, { client }) => { - let queryKey: readonly unknown[] | null - switch (item.kind) { - case "file": - queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey - break - case "directory": - queryKey = store.get( - directorySharesQueryAtom(item.id), - ).queryKey - break - default: - queryKey = null - break - } + const queryKey = itemSharesQueryKey(item, org, drive) if (queryKey) { const prevShares = client.getQueryData(queryKey) client.setQueryData( @@ -702,24 +694,12 @@ function ShareLinkOptionsMenu({ function CreateShareLinkButton() { const { item } = useContext(ItemShareDialogContext) - const store = useStore() + const drive = useAtomValue(currentDriveAtom) + const org = useCurrentOrganization() const { mutate: createShare, isPending: isCreatingShare } = useMutation({ - ...useAtomValue(createShareMutationAtom), + ...createShareMutationOptions({ org, drive }), onSuccess: (_createdShare, _vars, _, { client }) => { - let queryKey: readonly unknown[] | null - switch (item.kind) { - case "file": - queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey - break - case "directory": - queryKey = store.get( - directorySharesQueryAtom(item.id), - ).queryKey - break - default: - queryKey = null - break - } + const queryKey = itemSharesQueryKey(item, org, drive) if (queryKey) { client.invalidateQueries({ queryKey, diff --git a/apps/drive-web/src/vfs/api.ts b/apps/drive-web/src/vfs/api.ts index 190734d..09bd223 100644 --- a/apps/drive-web/src/vfs/api.ts +++ b/apps/drive-web/src/vfs/api.ts @@ -6,10 +6,9 @@ import { skipToken, } from "@tanstack/react-query" import { type } from "arktype" -import { atom } from "jotai" -import { atomFamily } from "jotai/utils" +import { type Drive } from "@/drive/drive" import { fetchApi } from "@/lib/api" -import { currentDriveAtom } from "@/drive/drive" +import type { Organization } from "@/organization/organization" import { DirectoryContent, DirectoryInfo, @@ -24,51 +23,54 @@ export const DirectoryContentResponse = type({ }) export type DirectoryContentResponseType = typeof DirectoryContentResponse.infer -/** - * This atom derives the file url for a given file. - * It is recommended to use {@link useFileUrl} instead of using this atom directly. - */ -export const fileUrlAtom = atomFamily((fileId: string) => - atom((get) => { - const drive = get(currentDriveAtom) - if (!drive) { - return "" - } - return `${import.meta.env.VITE_API_URL}/drives/${drive.id}/files/${fileId}/content` - }), -) - -export const rootDirectoryQueryAtom = atom((get) => { - const drive = get(currentDriveAtom) - return queryOptions({ - queryKey: ["drives", drive?.id, "directories", "root"], - queryFn: drive - ? () => - fetchApi( - "GET", - `/drives/${drive.id}/directories/root?include=path`, - { returns: DirectoryInfoWithPath }, - ).then(([_, result]) => result) - : skipToken, - }) -}) - -export const directoryInfoQueryAtom = atomFamily((directoryId: string) => - atom((get) => { - const drive = get(currentDriveAtom) - return queryOptions({ - queryKey: ["drives", drive?.id, "directories", directoryId], - queryFn: drive +export const rootDirectoryQuery = ({ + org, + drive, +}: { + org: Organization | null + drive: Drive | null +}) => + queryOptions({ + queryKey: ["organizations", org?.slug, "drives", drive?.id, "root"], + queryFn: + org && drive ? () => fetchApi( "GET", - `/drives/${drive.id}/directories/${directoryId}?include=path`, + `/${org.slug}/drives/${drive.id}/directories/root?include=path`, { returns: DirectoryInfoWithPath }, ).then(([_, result]) => result) : skipToken, - }) - }), -) + }) + +export const directoryInfoQuery = ({ + org, + drive, + directoryId, +}: { + org: Organization | null + drive: Drive | null + directoryId: string +}) => + queryOptions({ + queryKey: [ + "organizations", + org?.slug, + "drives", + drive?.id, + "directories", + directoryId, + ], + queryFn: + org && drive + ? () => + fetchApi( + "GET", + `/${org.slug}/drives/${drive.id}/directories/${directoryId}?include=path`, + { returns: DirectoryInfoWithPath }, + ).then(([_, result]) => result) + : skipToken, + }) export const DIRECTORY_CONTENT_ORDER_BY = { name: "name", @@ -100,6 +102,7 @@ type DirectoryContentPageParam = { } export const directoryContentQueryKey = ( + orgSlug: string | undefined, driveId: string | undefined, directoryId: string, params?: { @@ -107,6 +110,8 @@ export const directoryContentQueryKey = ( direction?: DirectoryContentOrderDirection }, ): readonly unknown[] => [ + "organizations", + orgSlug, "drives", driveId, "directories", @@ -123,53 +128,60 @@ export type DirectoryContentQuery = ReturnType< DirectoryContentPageParam > > -export const directoryContentQueryAtom = atomFamily( - ({ directoryId, orderBy, direction, limit }: DirectoryContentQueryParams) => - atom((get) => { - const drive = get(currentDriveAtom) - return infiniteQueryOptions({ - queryKey: directoryContentQueryKey(drive?.id, directoryId, { - orderBy, - direction, - }), - initialPageParam: { - orderBy, - direction, - limit, - cursor: "", - }, - queryFn: ({ pageParam }) => - drive - ? fetchApi( - "GET", - `/drives/${drive.id}/directories/${directoryId}/content?orderBy=${pageParam.orderBy}&dir=${pageParam.direction}&limit=${pageParam.limit}${pageParam.cursor ? `&cursor=${pageParam.cursor}` : ""}`, - { returns: DirectoryContentResponse }, - ).then(([_, result]) => result) - : Promise.reject(new Error("No drive selected")), - getNextPageParam: (lastPage, _pages, lastPageParam) => - lastPage.nextCursor - ? { - ...lastPageParam, - cursor: lastPage.nextCursor, - } - : null, - }) - }), - (paramsA, paramsB) => - paramsA.directoryId === paramsB.directoryId && - paramsA.orderBy === paramsB.orderBy && - paramsA.direction === paramsB.direction && - paramsA.limit === paramsB.limit, -) -export const createDirectoryMutationAtom = atom((get) => { - const drive = get(currentDriveAtom) - return mutationOptions({ +export const directoryContentQuery = ({ + org, + drive, + directoryId, + orderBy, + direction, + limit, +}: { + org: Organization | null + drive: Drive | null +} & DirectoryContentQueryParams) => + infiniteQueryOptions({ + queryKey: directoryContentQueryKey(org?.slug, drive?.id, directoryId, { + orderBy, + direction, + }), + initialPageParam: { + orderBy, + direction, + limit, + cursor: "", + } satisfies DirectoryContentPageParam, + queryFn: + org && drive + ? ({ pageParam }) => + fetchApi( + "GET", + `/${org.slug}/drives/${drive.id}/directories/${directoryId}/content?orderBy=${pageParam.orderBy}&dir=${pageParam.direction}&limit=${pageParam.limit}${pageParam.cursor ? `&cursor=${pageParam.cursor}` : ""}`, + { returns: DirectoryContentResponse }, + ).then(([_, result]) => result) + : skipToken, + getNextPageParam: (lastPage, _pages, lastPageParam) => + lastPage.nextCursor + ? { + ...lastPageParam, + cursor: lastPage.nextCursor, + } + : null, + }) + +export const createDirectoryMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => + mutationOptions({ mutationFn: async (data: { name: string; parentId: string }) => { if (!drive) throw new Error("No drive selected") return fetchApi( "POST", - `/drives/${drive.id}/directories?include=path`, + `/${org.slug}/drives/${drive.id}/directories?include=path`, { body: JSON.stringify({ name: data.name, @@ -180,14 +192,12 @@ export const createDirectoryMutationAtom = atom((get) => { ).then(([_, result]) => result) }, onSuccess: (_data, { parentId }, _context, { client }) => { - if (drive) { - client.invalidateQueries({ - queryKey: directoryContentQueryKey(drive.id, parentId), - }) - } + if (!drive) return + client.invalidateQueries({ + queryKey: directoryContentQueryKey(org.slug, drive.id, parentId), + }) }, }) -}) export const MoveDirectoryItemsResult = type({ items: DirectoryItem.array(), @@ -200,16 +210,21 @@ export const MoveDirectoryItemsResult = type({ }) export type MoveDirectoryItemsResult = typeof MoveDirectoryItemsResult.infer -export const moveDirectoryItemsMutationAtom = atom((get) => +export const moveDirectoryItemsMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async ({ targetDirectory, items, - }: { + }: { targetDirectory: DirectoryInfo | string items: DirectoryItem[] }) => { - const drive = get(currentDriveAtom) if (!drive) { throw new Error("Drive not found") } @@ -221,7 +236,7 @@ export const moveDirectoryItemsMutationAtom = atom((get) => const [, result] = await fetchApi( "POST", - `/drives/${drive.id}/directories/${dirId}/content`, + `/${org.slug}/drives/${drive.id}/directories/${dirId}/content`, { body: JSON.stringify({ items: items.map((item) => item.id), @@ -232,7 +247,6 @@ export const moveDirectoryItemsMutationAtom = atom((get) => return result }, onSuccess: (_data, { targetDirectory, items }, _result, { client }) => { - const drive = get(currentDriveAtom) if (!drive) return const dirId = @@ -241,12 +255,13 @@ export const moveDirectoryItemsMutationAtom = atom((get) => : targetDirectory.id // Invalidate using base key (without params) to invalidate all queries for these directories client.invalidateQueries({ - queryKey: directoryContentQueryKey(drive.id, dirId), + queryKey: directoryContentQueryKey(org.slug, drive.id, dirId), }) for (const item of items) { if (item.parentId) { client.invalidateQueries({ queryKey: directoryContentQueryKey( + org.slug, drive.id, item.parentId, ), @@ -254,13 +269,17 @@ export const moveDirectoryItemsMutationAtom = atom((get) => } } }, - }), -) + }) -export const moveToTrashMutationAtom = atom((get) => +export const moveToTrashMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async (items: DirectoryItem[]) => { - const drive = get(currentDriveAtom) if (!drive) { throw new Error("Drive not found") } @@ -285,7 +304,7 @@ export const moveToTrashMutationAtom = atom((get) => fileDeleteParams.set("trash", "true") deleteFilesPromise = fetchApi( "DELETE", - `/drives/${drive.id}/files?${fileDeleteParams.toString()}`, + `/${org.slug}/drives/${drive.id}/files?${fileDeleteParams.toString()}`, { returns: FileInfo.array(), }, @@ -301,7 +320,7 @@ export const moveToTrashMutationAtom = atom((get) => directoryDeleteParams.set("trash", "true") deleteDirectoriesPromise = fetchApi( "DELETE", - `/drives/${drive.id}/directories?${directoryDeleteParams.toString()}`, + `/${org.slug}/drives/${drive.id}/directories?${directoryDeleteParams.toString()}`, { returns: DirectoryInfo.array(), }, @@ -318,35 +337,38 @@ export const moveToTrashMutationAtom = atom((get) => return [...deletedFiles, ...deletedDirectories] }, onSuccess: (_data, items, _result, { client }) => { - const drive = get(currentDriveAtom) - if (drive) { - // Invalidate using base key (without params) to invalidate all queries for these directories - for (const item of items) { - if (item.parentId) { - client.invalidateQueries({ - queryKey: directoryContentQueryKey( - drive.id, - item.parentId, - ), - }) - } + if (!drive) return + // Invalidate using base key (without params) to invalidate all queries for these directories + for (const item of items) { + if (item.parentId) { + client.invalidateQueries({ + queryKey: directoryContentQueryKey( + org.slug, + drive.id, + item.parentId, + ), + }) } } }, - }), -) + }) -export const renameFileMutationAtom = atom((get) => +export const renameFileMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async (file: FileInfo) => { - const drive = get(currentDriveAtom) if (!drive) { throw new Error("Drive not found") } const [, result] = await fetchApi( "PATCH", - `/drives/${drive.id}/files/${file.id}`, + `/${org.slug}/drives/${drive.id}/files/${file.id}`, { body: JSON.stringify({ name: file.name }), returns: FileInfo, @@ -355,20 +377,24 @@ export const renameFileMutationAtom = atom((get) => return result }, - }), -) + }) -export const renameDirectoryMutationAtom = atom((get) => +export const renameDirectoryMutationOptions = ({ + org, + drive, +}: { + org: Organization + drive: Drive | null +}) => mutationOptions({ mutationFn: async (directory: DirectoryInfo) => { - const drive = get(currentDriveAtom) if (!drive) { throw new Error("Drive not found") } const [, result] = await fetchApi( "PATCH", - `/drives/${drive.id}/directories/${directory.id}`, + `/${org.slug}/drives/${drive.id}/directories/${directory.id}`, { body: JSON.stringify({ name: directory.name }), returns: DirectoryInfo, @@ -378,10 +404,11 @@ export const renameDirectoryMutationAtom = atom((get) => return result }, onSuccess: (data, _variables, _context, { client }) => { + if (!drive) return client.setQueryData( - get(directoryInfoQueryAtom(data.id)).queryKey, + directoryInfoQuery({ org, drive, directoryId: data.id }) + .queryKey, (prev) => (prev ? { ...prev, name: data.name } : undefined), ) }, - }), -) + }) diff --git a/apps/drive-web/src/vfs/hooks.ts b/apps/drive-web/src/vfs/hooks.ts index 8a65e0f..f0b769c 100644 --- a/apps/drive-web/src/vfs/hooks.ts +++ b/apps/drive-web/src/vfs/hooks.ts @@ -1,15 +1,14 @@ import { useAtomValue } from "jotai" -import { useEffect } from "react" -import { fileUrlAtom } from "./api" +import { currentDriveAtom } from "@/drive/drive" +import { useCurrentOrganization } from "@/organization/context" +import { buildApiUrl } from "@/lib/api" import type { FileInfo } from "./vfs" export function useFileUrl(file: FileInfo) { - const fileUrl = useAtomValue(fileUrlAtom(file.id)) - useEffect( - () => () => { - fileUrlAtom.remove(file.id) - }, - [file.id], - ) - return fileUrl + const org = useCurrentOrganization() + const drive = useAtomValue(currentDriveAtom) + if (!drive) return "" + return buildApiUrl( + `/${org.slug}/drives/${drive.id}/files/${file.id}/content`, + ).toString() }