@tulip-systems/drive 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +662 -0
  2. package/package.json +113 -0
  3. package/src/components/content.tsx +13 -0
  4. package/src/components/context.client.tsx +12 -0
  5. package/src/components/dnd.client.tsx +47 -0
  6. package/src/components/grid-card.client.tsx +252 -0
  7. package/src/components/grid.client.tsx +96 -0
  8. package/src/components/navigation/breadcrumbs.client.tsx +125 -0
  9. package/src/components/navigation/header.client.tsx +45 -0
  10. package/src/components/navigation/toolbar.client.tsx +35 -0
  11. package/src/components/navigation/view-switcher.client.tsx +32 -0
  12. package/src/components/selection.client.tsx +48 -0
  13. package/src/components/view.client.tsx +67 -0
  14. package/src/config/filters.ts +14 -0
  15. package/src/config/types.tsx +90 -0
  16. package/src/entry.client.ts +7 -0
  17. package/src/entry.server.ts +4 -0
  18. package/src/entry.ts +10 -0
  19. package/src/lib/constants.ts +19 -0
  20. package/src/lib/contracts.ts +121 -0
  21. package/src/lib/dto.ts +83 -0
  22. package/src/lib/helpers.server.ts +14 -0
  23. package/src/lib/helpers.ts +32 -0
  24. package/src/lib/search-params.ts +5 -0
  25. package/src/lib/validators.ts +89 -0
  26. package/src/providers/google/components/command-file-update.tsx +100 -0
  27. package/src/providers/google/components/command-folder-create.tsx +104 -0
  28. package/src/providers/google/components/command-folder-update.tsx +100 -0
  29. package/src/providers/google/components/content.client.tsx +6 -0
  30. package/src/providers/google/components/navigation.client.tsx +21 -0
  31. package/src/providers/google/components/provider.client.tsx +60 -0
  32. package/src/providers/google/components/view.client.tsx +158 -0
  33. package/src/providers/google/config/columns-data.tsx +81 -0
  34. package/src/providers/google/config/filters.ts +3 -0
  35. package/src/providers/google/entry.client.ts +10 -0
  36. package/src/providers/google/entry.server.ts +5 -0
  37. package/src/providers/google/entry.ts +12 -0
  38. package/src/providers/google/lib/constants.ts +10 -0
  39. package/src/providers/google/lib/dto.ts +104 -0
  40. package/src/providers/google/lib/helpers.ts +37 -0
  41. package/src/providers/google/lib/router.server.ts +62 -0
  42. package/src/providers/google/lib/schema.ts +9 -0
  43. package/src/providers/google/lib/search-params.ts +7 -0
  44. package/src/providers/google/lib/service.server.ts +792 -0
  45. package/src/providers/google/lib/validators.ts +148 -0
  46. package/src/providers/local/components/command-file-update.tsx +93 -0
  47. package/src/providers/local/components/command-file-upload.tsx +29 -0
  48. package/src/providers/local/components/command-folder-create.tsx +100 -0
  49. package/src/providers/local/components/command-folder-update.tsx +93 -0
  50. package/src/providers/local/components/content.client.tsx +3 -0
  51. package/src/providers/local/components/navigation.client.tsx +23 -0
  52. package/src/providers/local/components/provider.client.tsx +90 -0
  53. package/src/providers/local/components/upload-zone-context.client.tsx +43 -0
  54. package/src/providers/local/components/upload-zone.client.tsx +182 -0
  55. package/src/providers/local/components/view.client.tsx +145 -0
  56. package/src/providers/local/config/columns-data.tsx +81 -0
  57. package/src/providers/local/config/filters.ts +14 -0
  58. package/src/providers/local/entry.client.ts +18 -0
  59. package/src/providers/local/entry.server.ts +7 -0
  60. package/src/providers/local/entry.ts +14 -0
  61. package/src/providers/local/lib/constants.ts +23 -0
  62. package/src/providers/local/lib/helpers.ts +105 -0
  63. package/src/providers/local/lib/route-handler.server.ts +153 -0
  64. package/src/providers/local/lib/router.server.ts +137 -0
  65. package/src/providers/local/lib/schema.ts +104 -0
  66. package/src/providers/local/lib/search-params.ts +4 -0
  67. package/src/providers/local/lib/service.server.ts +1116 -0
  68. package/src/providers/local/lib/upload.client.ts +177 -0
  69. package/src/providers/local/lib/validators.ts +154 -0
  70. package/src/styles.css +1 -0
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import {
4
+ Header,
5
+ HeaderTopbarBackButton,
6
+ HeaderTopbarMobileNavSwitcher,
7
+ Topbar,
8
+ TopbarTools,
9
+ } from "@tulip-systems/core/components/client";
10
+ import type { ComponentProps } from "react";
11
+ import { type DriveBreadCrumbsProps, DriveBreadcrumbs } from "./breadcrumbs.client";
12
+
13
+ /**
14
+ * Drive Header
15
+ */
16
+ export function DriveHeader({ children, ...props }: ComponentProps<"div">) {
17
+ return (
18
+ <Header {...props}>
19
+ <Topbar>{children}</Topbar>
20
+ </Header>
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Drive Header Breadcrumbs
26
+ */
27
+ export function DriveHeaderBreadcrumbs({ breadcrumbs }: DriveBreadCrumbsProps) {
28
+ return (
29
+ <div className="flex h-full items-center gap-4">
30
+ <div className="h-full">
31
+ <HeaderTopbarBackButton />
32
+ <HeaderTopbarMobileNavSwitcher />
33
+ </div>
34
+
35
+ <DriveBreadcrumbs breadcrumbs={breadcrumbs} />
36
+ </div>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Drive Header Tools
42
+ */
43
+ export function DriveHeaderTools({ children, ...props }: ComponentProps<"div">) {
44
+ return <TopbarTools {...props}>{children}</TopbarTools>;
45
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { cn } from "@tulip-systems/core/lib";
4
+ import type { ComponentProps } from "react";
5
+ import { DriveBreadcrumbs } from "./breadcrumbs.client";
6
+
7
+ /**
8
+ * Drive Page Topbar
9
+ */
10
+ export function DriveToolbar({ className, children, ...props }: ComponentProps<"div">) {
11
+ return (
12
+ <div
13
+ {...props}
14
+ className={cn("flex items-center justify-between overflow-x-auto pt-3", className)}
15
+ >
16
+ {children}
17
+ </div>
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Drive Toolbar Breadcrumbs
23
+ */
24
+ export const DriveToolbarBreadcrumbs = DriveBreadcrumbs;
25
+
26
+ /**
27
+ * Drive Header Tools
28
+ */
29
+ export function DriveToolbarTools({ children, className, ...props }: ComponentProps<"div">) {
30
+ return (
31
+ <div {...props} className={cn("flex h-full items-center justify-end gap-4", className)}>
32
+ {children}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { ToggleGroup, ToggleGroupItem } from "@tulip-systems/core/components/client";
4
+ import { LayoutGridIcon, StretchVerticalIcon } from "lucide-react";
5
+ import { type DriveViewMode, useDriveViewContext } from "../view.client";
6
+
7
+ /**
8
+ * Drive view switcher
9
+ */
10
+ export function DriveViewSwitcher() {
11
+ const { view, onViewChange } = useDriveViewContext();
12
+
13
+ return (
14
+ <ToggleGroup
15
+ type="single"
16
+ variant="outline"
17
+ value={view}
18
+ onValueChange={(value) => {
19
+ console.log("DriveViewSwitcher: Changing view to", value);
20
+ onViewChange(value ? (value as DriveViewMode) : "grid");
21
+ }}
22
+ >
23
+ <ToggleGroupItem value="grid" aria-label="Toggle grid view">
24
+ <LayoutGridIcon />
25
+ </ToggleGroupItem>
26
+
27
+ <ToggleGroupItem value="list" aria-label="Toggle list view">
28
+ <StretchVerticalIcon />
29
+ </ToggleGroupItem>
30
+ </ToggleGroup>
31
+ );
32
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import type { Selection } from "@tulip-systems/core/data-tables";
4
+ import { createContext, type PropsWithChildren, use, useMemo, useState } from "react";
5
+ import type { DriveNode } from "@/lib/dto";
6
+
7
+ /**
8
+ * DriveSelectionContextValue
9
+ */
10
+ export type DriveSelectionContextValue = {
11
+ selection?: Selection;
12
+ selectionConditions?: (node: DriveNode) => boolean | boolean[];
13
+ };
14
+
15
+ const DriveSelectionContext = createContext<DriveSelectionContextValue | null>(null);
16
+
17
+ /**
18
+ * useDriveSelectionContext
19
+ */
20
+ export function useDriveSelectionContext() {
21
+ const context = use(DriveSelectionContext);
22
+ if (!context) throw new Error("DriveSelectionContext not found!");
23
+ return context;
24
+ }
25
+
26
+ /**
27
+ * DriveSelectionProvider
28
+ */
29
+ export function DriveSelectionProvider({
30
+ selection,
31
+ selectionConditions,
32
+ children,
33
+ }: PropsWithChildren<DriveSelectionContextValue>) {
34
+ const [_rowSelection, _setRowSelection] = useState({});
35
+
36
+ const value = useMemo(
37
+ () => ({
38
+ selection: {
39
+ rowSelection: selection?.rowSelection ?? _rowSelection,
40
+ setRowSelection: selection?.setRowSelection ?? _setRowSelection,
41
+ },
42
+ selectionConditions: selectionConditions,
43
+ }),
44
+ [selection, selectionConditions, _rowSelection],
45
+ );
46
+
47
+ return <DriveSelectionContext value={value}>{children}</DriveSelectionContext>;
48
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { createContext, type PropsWithChildren, use, useCallback, useState } from "react";
4
+ import { DRIVE_VIEW_COOKIE, DRIVE_VIEW_COOKIE_MAX_AGE } from "@/lib/constants";
5
+
6
+ export type DriveViewMode = "grid" | "list";
7
+
8
+ /**
9
+ * DriveViewContextValue
10
+ */
11
+ export type DriveViewContextValue = {
12
+ view: DriveViewMode;
13
+ onViewChange: (view: DriveViewMode) => void;
14
+ };
15
+
16
+ const DriveViewContext = createContext<DriveViewContextValue | null>(null);
17
+
18
+ /**
19
+ * useDriveViewContext
20
+ */
21
+ export function useDriveViewContext() {
22
+ const context = use(DriveViewContext);
23
+
24
+ if (!context) {
25
+ console.warn("DriveViewContext not found!");
26
+ return {
27
+ view: "grid" as DriveViewMode,
28
+ onViewChange: () => {
29
+ throw new Error("DriveViewContext not found!");
30
+ },
31
+ };
32
+ }
33
+
34
+ return context;
35
+ }
36
+
37
+ /**
38
+ * DriveViewProvider
39
+ */
40
+
41
+ export type DriveViewProviderProps = PropsWithChildren<Partial<DriveViewContextValue>> & {
42
+ initialView?: DriveViewMode;
43
+ };
44
+
45
+ export function DriveViewProvider(props: DriveViewProviderProps) {
46
+ const [_view, _setView] = useState<DriveViewMode>(props.view ?? props.initialView ?? "grid");
47
+ const view = props.view ?? _view;
48
+
49
+ const onViewChange = useCallback(
50
+ (value: DriveViewMode | ((value: DriveViewMode) => DriveViewMode)) => {
51
+ const viewState = typeof value === "function" ? value(view) : value;
52
+
53
+ if (props.onViewChange) {
54
+ props.onViewChange(viewState);
55
+ } else {
56
+ _setView(viewState);
57
+ }
58
+
59
+ // This sets the cookie to keep the view state.
60
+ // biome-ignore lint/suspicious/noDocumentCookie: This is needed to persist the sidebar state across page reloads.
61
+ document.cookie = `${DRIVE_VIEW_COOKIE}=${viewState}; path=/; max-age=${DRIVE_VIEW_COOKIE_MAX_AGE}`;
62
+ },
63
+ [props.onViewChange, view],
64
+ );
65
+
66
+ return <DriveViewContext value={{ view, onViewChange }}>{props.children}</DriveViewContext>;
67
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ createTableFilters,
3
+ parseFilterArray,
4
+ parseFilterBoolean,
5
+ } from "@tulip-systems/core/data-tables";
6
+ import z from "zod";
7
+ import { DRIVE_NODE_TYPES } from "@/lib/constants";
8
+
9
+ export const driveTreeFilters = createTableFilters({
10
+ types: parseFilterArray(z.array(z.enum(DRIVE_NODE_TYPES))),
11
+ contentTypes: parseFilterArray(z.array(z.string())),
12
+ nodeIds: parseFilterArray(z.array(z.string())),
13
+ isArchived: parseFilterBoolean(z.boolean()),
14
+ });
@@ -0,0 +1,90 @@
1
+ import {
2
+ createStatusConfig,
3
+ createStatusVariants,
4
+ StatusBadge,
5
+ StatusField,
6
+ } from "@tulip-systems/core/components";
7
+ import { cva } from "class-variance-authority";
8
+ import {
9
+ FileArchiveIcon,
10
+ FileAudioIcon,
11
+ FileIcon,
12
+ FileImageIcon,
13
+ FileSpreadsheetIcon,
14
+ FileTextIcon,
15
+ FileVideoIcon,
16
+ } from "lucide-react";
17
+ import type { DriveNodeSubtype } from "@/lib/validators";
18
+
19
+ /**
20
+ * Node subtype config
21
+ */
22
+ export const nodeSubtypeConfig = createStatusConfig<DriveNodeSubtype>([
23
+ {
24
+ value: "image",
25
+ label: "Afbeelding",
26
+ icon: FileImageIcon,
27
+ className: "stroke-blue-500",
28
+ },
29
+ {
30
+ value: "document",
31
+ label: "Document",
32
+ icon: FileTextIcon,
33
+ className: "stroke-red-600",
34
+ },
35
+ {
36
+ value: "archive",
37
+ label: "Archief",
38
+ icon: FileArchiveIcon,
39
+ className: "stroke-yellow-500",
40
+ },
41
+ {
42
+ value: "audio",
43
+ label: "Audio",
44
+ icon: FileAudioIcon,
45
+ className: "stroke-pink-500",
46
+ },
47
+ {
48
+ value: "video",
49
+ label: "Video",
50
+ icon: FileVideoIcon,
51
+ className: "stroke-purple-500",
52
+ },
53
+ {
54
+ value: "spreadsheet",
55
+ label: "Tabel",
56
+ icon: FileSpreadsheetIcon,
57
+ className: "stroke-green-600",
58
+ },
59
+ {
60
+ value: "other",
61
+ label: "Bestand",
62
+ icon: FileIcon,
63
+ className: "stroke-gray-400",
64
+ },
65
+ ]);
66
+
67
+ /**
68
+ * Node subtype variants
69
+ */
70
+ export const nodeSubtypeVariants = cva("", {
71
+ variants: { status: createStatusVariants(nodeSubtypeConfig) },
72
+ });
73
+
74
+ /**
75
+ * Node subtype badge
76
+ */
77
+ export function NodeSubtypeBadge(props: { subtype: DriveNodeSubtype }) {
78
+ return (
79
+ <StatusBadge config={nodeSubtypeConfig} variants={nodeSubtypeVariants} status={props.subtype} />
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Node subtype field
85
+ */
86
+ export function NodeSubtypeField(props: { subtype: DriveNodeSubtype }) {
87
+ return (
88
+ <StatusField config={nodeSubtypeConfig} variants={nodeSubtypeVariants} status={props.subtype} />
89
+ );
90
+ }
@@ -0,0 +1,7 @@
1
+ export * from "@/components/grid.client";
2
+ export * from "@/components/grid-card.client";
3
+ /**
4
+ * Components
5
+ */
6
+ export * from "@/components/selection.client";
7
+ export * from "@/components/view.client";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Lib
3
+ */
4
+ export * from "./lib/helpers.server";
package/src/entry.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Lib
3
+ */
4
+
5
+ export * from "./lib/constants";
6
+ export * from "./lib/contracts";
7
+ export * from "./lib/dto";
8
+ export * from "./lib/helpers";
9
+ export * from "./lib/search-params";
10
+ export * from "./lib/validators";
@@ -0,0 +1,19 @@
1
+ export const DRIVE_PROVIDERS = ["local", "google"] as const;
2
+
3
+ export const DRIVE_NODE_TYPES = ["folder", "file"] as const;
4
+
5
+ export const DRIVE_NODE_SUBTYPES = [
6
+ "image",
7
+ "document",
8
+ "spreadsheet",
9
+ "video",
10
+ "audio",
11
+ "archive",
12
+ "other",
13
+ ] as const;
14
+
15
+ /**
16
+ * The cookie name for storing the drive view mode (grid or list).
17
+ */
18
+ export const DRIVE_VIEW_COOKIE = "tulip.drive_view_mode" as const;
19
+ export const DRIVE_VIEW_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
@@ -0,0 +1,121 @@
1
+ import type { TableQueryResponse } from "@tulip-systems/core/data-tables/server";
2
+ import type {
3
+ DriveFolderParent,
4
+ DriveNode,
5
+ DriveNodeBase,
6
+ DriveNodeChild,
7
+ DriveNodeWithChildren,
8
+ } from "./dto";
9
+ import type {
10
+ CreateDriveFolderInput,
11
+ GetDriveNodesByParentIdInput,
12
+ ListDriveTreeInput,
13
+ PresignDriveFileInput,
14
+ UpdateDriveNodeInput,
15
+ UploadDriveFileInput,
16
+ } from "./validators";
17
+
18
+ export type DrivePresignedUploadResult<TNode extends DriveNodeBase = DriveNode> = {
19
+ uploadId: string;
20
+ presignedUrl: string;
21
+ node: TNode;
22
+ };
23
+
24
+ /**
25
+ * Read-only drive operations every provider should be able to support.
26
+ * This is the base contract for provider-agnostic consumers and intentionally
27
+ * excludes storage, upload, preview generation, readonly, and soft-delete
28
+ * behavior because those are provider capabilities, not universal drive reads.
29
+ */
30
+ export interface DriveReader<
31
+ TNode extends DriveNodeBase = DriveNodeBase,
32
+ TNodeWithChildren extends DriveNodeWithChildren<TNode> = DriveNodeWithChildren<TNode>,
33
+ TNodeChild extends DriveNodeChild<TNode> = DriveNodeChild<TNode>,
34
+ > {
35
+ getNodeById(id: string): Promise<TNode | null>;
36
+
37
+ getNodeWithChildren(input: { id: string; namespace: string }): Promise<TNodeWithChildren | null>;
38
+
39
+ getNodesByParentId(input: GetDriveNodesByParentIdInput): Promise<TNode[]>;
40
+
41
+ listTree(input: ListDriveTreeInput): Promise<TableQueryResponse<TNode>>;
42
+
43
+ getFolderParents(input: {
44
+ id: string | null;
45
+ namespace: string;
46
+ }): Promise<Array<DriveFolderParent<TNode>>>;
47
+
48
+ getNodeChildren(ids: string[]): Promise<TNodeChild[]>;
49
+ }
50
+
51
+ /**
52
+ * Optional capability for providers that can mutate the node tree.
53
+ * Providers that are read-only, remote-index-only, or permission-limited should
54
+ * not implement this contract.
55
+ */
56
+ export interface DriveNodeMutations<TNode extends DriveNodeBase = DriveNodeBase> {
57
+ createFolder(input: CreateDriveFolderInput): Promise<TNode>;
58
+ updateNode(id: string, data: UpdateDriveNodeInput): Promise<TNode>;
59
+ moveNode(input: { id: string; parentId: string | null }): Promise<TNode>;
60
+ deleteNode(id: string): Promise<void>;
61
+ deleteNodes(ids: string[]): Promise<void>;
62
+ }
63
+
64
+ /**
65
+ * Optional capability for providers that accept a file body directly.
66
+ * The body type is provider-specific so the base contracts do not depend on any
67
+ * storage package or runtime upload representation.
68
+ */
69
+ export interface DriveDirectUpload<TNode extends DriveNodeBase = DriveNodeBase, TBody = unknown> {
70
+ uploadFile(input: UploadDriveFileInput & { body: TBody }): Promise<TNode>;
71
+ }
72
+
73
+ /**
74
+ * Optional capability for providers that create upload sessions/URLs.
75
+ * The returned metadata is provider-specific; local/object-storage providers can
76
+ * expose asset metadata here without leaking storage concepts into the base Drive
77
+ * contract.
78
+ */
79
+ export interface DrivePresignedUpload<
80
+ TNode extends DriveNodeBase = DriveNodeBase,
81
+ TResult extends DrivePresignedUploadResult<TNode> = DrivePresignedUploadResult<TNode>,
82
+ > {
83
+ presignUpload(input: PresignDriveFileInput): Promise<TResult>;
84
+
85
+ confirmUpload(uploadId: string): Promise<TNode>;
86
+ }
87
+
88
+ /**
89
+ * Optional capability for providers that generate and persist preview variants.
90
+ * This is useful for local/object-storage providers. Providers with native
91
+ * previews, such as Google Drive, should not implement this just as a no-op.
92
+ */
93
+ export interface DrivePreviewGenerator {
94
+ generatePreviews(input: { id: string }): Promise<void>;
95
+ }
96
+
97
+ /**
98
+ * Optional capability for providers that can lock/unlock nodes.
99
+ * Google Drive maps this to `contentRestrictions.readOnly`; local drive maps it
100
+ * to its readonly flag. This is intentionally not part of the base reader
101
+ * contract because not every provider can honestly enforce it.
102
+ */
103
+ export interface DriveReadonly<TNode extends DriveNodeBase = DriveNodeBase> {
104
+ setReadonly(ids: string[], readonly: boolean): Promise<TNode[]>;
105
+ }
106
+
107
+ /**
108
+ * Optional capability for providers that support soft removal.
109
+ * Local drive maps this to archive state; Google Drive maps this to trash.
110
+ */
111
+ export interface DriveArchive<TNode extends DriveNodeBase = DriveNodeBase> {
112
+ archiveNodes(ids: string[]): Promise<TNode[]>;
113
+ restoreNodes(ids: string[]): Promise<TNode[]>;
114
+ }
115
+
116
+ /**
117
+ * Base provider-agnostic Drive contract.
118
+ * Additional behavior should be composed by intersecting optional capability
119
+ * contracts, for example `Drive<TNode> & DriveNodeMutations<TNode>`.
120
+ */
121
+ export type Drive<TNode extends DriveNodeBase = DriveNodeBase> = DriveReader<TNode>;
package/src/lib/dto.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { DRIVE_NODE_SUBTYPES, DRIVE_NODE_TYPES, DRIVE_PROVIDERS } from "./constants";
3
+
4
+ /**
5
+ * Drive node provider
6
+ */
7
+ export const driveNodeProviderSchema = z.enum(DRIVE_PROVIDERS);
8
+ export type DriveNodeProvider = z.infer<typeof driveNodeProviderSchema>;
9
+
10
+ /**
11
+ * Drive node type
12
+ */
13
+ export const driveNodeTypeSchema = z.enum(DRIVE_NODE_TYPES);
14
+ export type DriveNodeType = z.infer<typeof driveNodeTypeSchema>;
15
+
16
+ /**
17
+ * Drive node subtype
18
+ */
19
+ export const driveNodeSubtypeSchema = z.enum(DRIVE_NODE_SUBTYPES);
20
+ export type DriveNodeSubtype = z.infer<typeof driveNodeSubtypeSchema>;
21
+
22
+ /**
23
+ * Node availability
24
+ */
25
+ export const driveNodeAvailabilitySchema = z.enum(["ready", "pending", "failed"]);
26
+ export type DriveNodeAvailability = z.infer<typeof driveNodeAvailabilitySchema>;
27
+
28
+ /**
29
+ * Node links
30
+ */
31
+ export const driveNodeLinksSchema = z.object({
32
+ view: z.string().nullable(),
33
+ download: z.string().nullable(),
34
+ preview: z.string().nullable(),
35
+ thumbnail: z.string().nullable(),
36
+ });
37
+ export type DriveNodeLinks = z.infer<typeof driveNodeLinksSchema>;
38
+
39
+ /**
40
+ * Node
41
+ */
42
+ export const driveNodeSchema = z.object({
43
+ id: z.string(),
44
+ provider: driveNodeProviderSchema,
45
+ createdAt: z.date(),
46
+ updatedAt: z.date(),
47
+ name: z.string(),
48
+ namespace: z.string(),
49
+ type: driveNodeTypeSchema,
50
+ subtype: driveNodeSubtypeSchema,
51
+ size: z.number().nullable(),
52
+ contentType: z.string().nullable(),
53
+ readonly: z.boolean(),
54
+ hidden: z.boolean(),
55
+ archivedAt: z.date().nullable(),
56
+ parentId: z.string().nullable(),
57
+ links: driveNodeLinksSchema,
58
+ availability: driveNodeAvailabilitySchema,
59
+ });
60
+ export type DriveNode = z.infer<typeof driveNodeSchema>;
61
+ export type DriveNodeBase = Omit<DriveNode, "provider">;
62
+
63
+ /**
64
+ * Node with children
65
+ */
66
+ export type DriveNodeWithChildren<TNode extends DriveNodeBase = DriveNode> = TNode & {
67
+ children: TNode[];
68
+ };
69
+
70
+ /**
71
+ * Node parent and child types
72
+ */
73
+ export type DriveFolderParent<TNode extends DriveNodeBase = DriveNode> = Pick<
74
+ TNode,
75
+ "id" | "name" | "parentId"
76
+ > & {
77
+ depth: number;
78
+ };
79
+
80
+ /**
81
+ * Node child type
82
+ */
83
+ export type DriveNodeChild<TNode extends DriveNodeBase = DriveNode> = TNode & { depth: number };
@@ -0,0 +1,14 @@
1
+ import "server-cli-only";
2
+
3
+ import type { cookies } from "next/headers";
4
+ import type { DriveViewMode } from "@/components/view.client";
5
+ import { DRIVE_VIEW_COOKIE } from "./constants";
6
+
7
+ /**
8
+ * Helper function to get the drive view mode from cookies
9
+ * @param cookiesStore - The cookies store from Next.js headers
10
+ * @returns The drive view mode ("grid" or "list") or undefined if not set
11
+ */
12
+ export function getDriveViewModeFromCookies(cookiesStore: Awaited<ReturnType<typeof cookies>>) {
13
+ return cookiesStore.get(DRIVE_VIEW_COOKIE)?.value as DriveViewMode | undefined;
14
+ }
@@ -0,0 +1,32 @@
1
+ import type { DriveNode } from "./dto";
2
+
3
+ /**
4
+ * Check if the node is a file
5
+ */
6
+ export function isFile(node: Pick<DriveNode, "type">): node is DriveNode {
7
+ return node.type === "file";
8
+ }
9
+
10
+ /**
11
+ * Check if the node is a folder
12
+ */
13
+ export function isFolder(node: Pick<DriveNode, "type">): node is DriveNode {
14
+ return node.type === "folder";
15
+ }
16
+
17
+ /**
18
+ * Render bytes
19
+ */
20
+ export function renderBytes(bytes: number) {
21
+ const units = ["B", "KB", "MB", "GB", "TB", "PB"];
22
+
23
+ let size = bytes;
24
+ let unitIndex = 0;
25
+
26
+ while (size >= 1024 && unitIndex < units.length - 1) {
27
+ size /= 1024;
28
+ unitIndex++;
29
+ }
30
+
31
+ return `${size.toFixed(2)}${units[unitIndex]}`;
32
+ }
@@ -0,0 +1,5 @@
1
+ import { parseAsString } from "nuqs/server";
2
+
3
+ export const driveTreeSearchParams = {
4
+ parentId: parseAsString,
5
+ };