@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.
- package/LICENSE +662 -0
- package/package.json +113 -0
- package/src/components/content.tsx +13 -0
- package/src/components/context.client.tsx +12 -0
- package/src/components/dnd.client.tsx +47 -0
- package/src/components/grid-card.client.tsx +252 -0
- package/src/components/grid.client.tsx +96 -0
- package/src/components/navigation/breadcrumbs.client.tsx +125 -0
- package/src/components/navigation/header.client.tsx +45 -0
- package/src/components/navigation/toolbar.client.tsx +35 -0
- package/src/components/navigation/view-switcher.client.tsx +32 -0
- package/src/components/selection.client.tsx +48 -0
- package/src/components/view.client.tsx +67 -0
- package/src/config/filters.ts +14 -0
- package/src/config/types.tsx +90 -0
- package/src/entry.client.ts +7 -0
- package/src/entry.server.ts +4 -0
- package/src/entry.ts +10 -0
- package/src/lib/constants.ts +19 -0
- package/src/lib/contracts.ts +121 -0
- package/src/lib/dto.ts +83 -0
- package/src/lib/helpers.server.ts +14 -0
- package/src/lib/helpers.ts +32 -0
- package/src/lib/search-params.ts +5 -0
- package/src/lib/validators.ts +89 -0
- package/src/providers/google/components/command-file-update.tsx +100 -0
- package/src/providers/google/components/command-folder-create.tsx +104 -0
- package/src/providers/google/components/command-folder-update.tsx +100 -0
- package/src/providers/google/components/content.client.tsx +6 -0
- package/src/providers/google/components/navigation.client.tsx +21 -0
- package/src/providers/google/components/provider.client.tsx +60 -0
- package/src/providers/google/components/view.client.tsx +158 -0
- package/src/providers/google/config/columns-data.tsx +81 -0
- package/src/providers/google/config/filters.ts +3 -0
- package/src/providers/google/entry.client.ts +10 -0
- package/src/providers/google/entry.server.ts +5 -0
- package/src/providers/google/entry.ts +12 -0
- package/src/providers/google/lib/constants.ts +10 -0
- package/src/providers/google/lib/dto.ts +104 -0
- package/src/providers/google/lib/helpers.ts +37 -0
- package/src/providers/google/lib/router.server.ts +62 -0
- package/src/providers/google/lib/schema.ts +9 -0
- package/src/providers/google/lib/search-params.ts +7 -0
- package/src/providers/google/lib/service.server.ts +792 -0
- package/src/providers/google/lib/validators.ts +148 -0
- package/src/providers/local/components/command-file-update.tsx +93 -0
- package/src/providers/local/components/command-file-upload.tsx +29 -0
- package/src/providers/local/components/command-folder-create.tsx +100 -0
- package/src/providers/local/components/command-folder-update.tsx +93 -0
- package/src/providers/local/components/content.client.tsx +3 -0
- package/src/providers/local/components/navigation.client.tsx +23 -0
- package/src/providers/local/components/provider.client.tsx +90 -0
- package/src/providers/local/components/upload-zone-context.client.tsx +43 -0
- package/src/providers/local/components/upload-zone.client.tsx +182 -0
- package/src/providers/local/components/view.client.tsx +145 -0
- package/src/providers/local/config/columns-data.tsx +81 -0
- package/src/providers/local/config/filters.ts +14 -0
- package/src/providers/local/entry.client.ts +18 -0
- package/src/providers/local/entry.server.ts +7 -0
- package/src/providers/local/entry.ts +14 -0
- package/src/providers/local/lib/constants.ts +23 -0
- package/src/providers/local/lib/helpers.ts +105 -0
- package/src/providers/local/lib/route-handler.server.ts +153 -0
- package/src/providers/local/lib/router.server.ts +137 -0
- package/src/providers/local/lib/schema.ts +104 -0
- package/src/providers/local/lib/search-params.ts +4 -0
- package/src/providers/local/lib/service.server.ts +1116 -0
- package/src/providers/local/lib/upload.client.ts +177 -0
- package/src/providers/local/lib/validators.ts +154 -0
- 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
|
+
}
|
package/src/entry.ts
ADDED
|
@@ -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
|
+
}
|