mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-02 07:31:18 +00:00
feat(fronend): wip org prefixed routing
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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])
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={isActive("/recent")}>
|
||||
<Link to="/recent">
|
||||
<ClockIcon />
|
||||
<span>Recent</span>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link to="/$orgSlug" params={{ orgSlug: org.slug }}>
|
||||
<HouseIcon />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<AllFilesItem />
|
||||
<TrashItem />
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location.pathname.startsWith("/directories")}
|
||||
>
|
||||
<Link to={`/directories/${rootDirectory.id}`}>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link
|
||||
to="/$orgSlug/drives/$driveId"
|
||||
params={{ orgSlug: org.slug, driveId: drive.id }}
|
||||
>
|
||||
<FilesIcon />
|
||||
<span>All Files</span>
|
||||
</Link>
|
||||
@@ -114,29 +124,6 @@ function AllFilesItem() {
|
||||
)
|
||||
}
|
||||
|
||||
function TrashItem() {
|
||||
const location = useLocation()
|
||||
const { data: rootDirectory } = useQuery(
|
||||
useAtomValue(rootDirectoryQueryAtom),
|
||||
)
|
||||
|
||||
if (!rootDirectory) return null
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location.pathname.startsWith("/trash/directories")}
|
||||
>
|
||||
<Link to={`/trash/directories/${rootDirectory.id}`}>
|
||||
<TrashIcon />
|
||||
<span>Trash</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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 = (
|
||||
<StrictMode>
|
||||
<ThemeProvider defaultTheme="system" storageKey="fileone-ui-theme">
|
||||
<RouterProvider router={router} />
|
||||
<TanStackRouterDevtools router={router} />
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Schema extends type.Any>(
|
||||
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<Schema extends type.Any>(
|
||||
} else {
|
||||
path = route
|
||||
}
|
||||
const url = new URL(path, baseApiUrl)
|
||||
return new URL(path, baseApiUrl)
|
||||
}
|
||||
|
||||
export async function fetchApi<Schema extends type.Any>(
|
||||
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,
|
||||
|
||||
32
apps/drive-web/src/organization/api.ts
Normal file
32
apps/drive-web/src/organization/api.ts
Normal file
@@ -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),
|
||||
})
|
||||
16
apps/drive-web/src/organization/context.ts
Normal file
16
apps/drive-web/src/organization/context.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createContext, useContext } from "react"
|
||||
import type { Organization } from "./organization"
|
||||
|
||||
export const OrganizationContext = createContext<Organization>(
|
||||
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
|
||||
}
|
||||
18
apps/drive-web/src/organization/organization.ts
Normal file
18
apps/drive-web/src/organization/organization.ts
Normal file
@@ -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
|
||||
14
apps/drive-web/src/query-client.ts
Normal file
14
apps/drive-web/src/query-client.ts
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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(
|
||||
|
||||
14
apps/drive-web/src/router.ts
Normal file
14
apps/drive-web/src/router.ts
Normal file
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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<RouterContext>()({
|
||||
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
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<LoadingSpinner className="size-10" />
|
||||
@@ -33,7 +24,11 @@ function AuthenticatedLayout() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!drives) {
|
||||
const personalOrg = orgs?.find(
|
||||
(org) => org.kind === ORGANIZATION_KIND.personal,
|
||||
)
|
||||
|
||||
if (!personalOrg) {
|
||||
return <Navigate replace to="/login" />
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<LoadingSpinner className="size-10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!org) return null
|
||||
|
||||
return (
|
||||
<OrganizationContext value={org}>
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen w-full">
|
||||
<DashboardSidebar />
|
||||
<SidebarInset className="overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</OrganizationContext>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
@@ -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 (
|
||||
<div>
|
||||
Hello "/_authenticated/$orgSlug/_sidebar-layout/drives/$driveSlug"!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 <div>Home of organization "{org.name}"</div>
|
||||
}
|
||||
@@ -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 (
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen w-full">
|
||||
<DashboardSidebar />
|
||||
<SidebarInset className="overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/_sidebar-layout/home")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_authenticated/home"!</div>
|
||||
}
|
||||
@@ -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 <Navigate replace to="/home" />
|
||||
const { data: orgs, isLoading } = useQuery(listOrganizationsQuery)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<LoadingSpinner className="size-10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const personalOrg = orgs?.find(
|
||||
(org) => org.kind === ORGANIZATION_KIND.personal,
|
||||
)
|
||||
|
||||
if (!personalOrg) {
|
||||
return <Navigate replace to="/login" />
|
||||
}
|
||||
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to="/$orgSlug"
|
||||
params={{ orgSlug: personalOrg.slug }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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<ActivePopoverKind | null>
|
||||
}>) {
|
||||
}>) {
|
||||
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<DateInputHandle>(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<ActivePopoverKind | null>
|
||||
}>) {
|
||||
}>) {
|
||||
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<Share[]>(queryKey)
|
||||
client.setQueryData<Share[]>(
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user