@@ -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()
}