@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,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { toast } from "@tulip-systems/core/components/client";
|
|
4
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
5
|
+
import { useAction } from "@tulip-systems/core/lib/client";
|
|
6
|
+
import type { StorageAsset } from "@tulip-systems/core/storage";
|
|
7
|
+
import { type ComponentProps, useCallback } from "react";
|
|
8
|
+
import { useDropzone } from "react-dropzone";
|
|
9
|
+
import { inferLocalDriveNodeSubtype } from "@/providers/local/lib/helpers";
|
|
10
|
+
import type {
|
|
11
|
+
LocalDriveUploadFileRequest,
|
|
12
|
+
LocalDriveUploadHooks,
|
|
13
|
+
PrepareLocalDriveUploadInput,
|
|
14
|
+
} from "@/providers/local/lib/upload.client";
|
|
15
|
+
import type { LocalDriveFileNode, LocalDriveNodeWithAsset } from "@/providers/local/lib/validators";
|
|
16
|
+
import {
|
|
17
|
+
LocalDriveUploadZoneContext,
|
|
18
|
+
type LocalDriveUploadZoneContextValue,
|
|
19
|
+
} from "./upload-zone-context.client";
|
|
20
|
+
|
|
21
|
+
export type LocalDriveUploadZoneProps = ComponentProps<"div"> &
|
|
22
|
+
Pick<LocalDriveUploadZoneContextValue, "optimistic" | "driveUploadClient" | "disabled"> & {
|
|
23
|
+
variables: Pick<
|
|
24
|
+
PrepareLocalDriveUploadInput,
|
|
25
|
+
"namespace" | "parentId" | "readonly" | "hidden" | "visibility"
|
|
26
|
+
>;
|
|
27
|
+
uploadHooks?: LocalDriveUploadHooks;
|
|
28
|
+
|
|
29
|
+
onUploadCompleted?: (node: LocalDriveFileNode) => Promise<void> | void;
|
|
30
|
+
onUploadFailed?: (error: unknown) => Promise<void> | void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type DriveUploadZoneProps = LocalDriveUploadZoneProps;
|
|
34
|
+
|
|
35
|
+
export function LocalDriveUploadZone({
|
|
36
|
+
variables,
|
|
37
|
+
optimistic,
|
|
38
|
+
driveUploadClient,
|
|
39
|
+
uploadHooks,
|
|
40
|
+
onUploadCompleted,
|
|
41
|
+
onUploadFailed,
|
|
42
|
+
disabled = false,
|
|
43
|
+
children,
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: LocalDriveUploadZoneProps) {
|
|
47
|
+
/**
|
|
48
|
+
* Delete mutation
|
|
49
|
+
*/
|
|
50
|
+
const deleteMutation = useAction({
|
|
51
|
+
mutationFn: async (ids: string[]) => driveUploadClient.deleteFiles(ids),
|
|
52
|
+
onMutate: async (ids) => {
|
|
53
|
+
await optimistic?.cancel?.();
|
|
54
|
+
await optimistic?.remove?.(ids);
|
|
55
|
+
},
|
|
56
|
+
onError: async (error) => {
|
|
57
|
+
console.error("Delete failed upload error: ", error);
|
|
58
|
+
await onUploadFailed?.(error);
|
|
59
|
+
},
|
|
60
|
+
onSettled: () => {
|
|
61
|
+
optimistic?.invalidate?.();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Upload mutation
|
|
67
|
+
*/
|
|
68
|
+
const uploadMutation = useAction({
|
|
69
|
+
mutationFn: async (params: LocalDriveUploadFileRequest) =>
|
|
70
|
+
driveUploadClient.upload(params, uploadHooks),
|
|
71
|
+
onMutate: async (variables) => {
|
|
72
|
+
await optimistic?.cancel?.();
|
|
73
|
+
|
|
74
|
+
// Generate a new node
|
|
75
|
+
const newNode = {
|
|
76
|
+
id: variables.uploadId,
|
|
77
|
+
provider: "local",
|
|
78
|
+
createdAt: new Date(),
|
|
79
|
+
updatedAt: new Date(),
|
|
80
|
+
type: "file",
|
|
81
|
+
name: variables.file.name,
|
|
82
|
+
namespace: variables.namespace,
|
|
83
|
+
parentId: variables.parentId ?? null,
|
|
84
|
+
size: variables.file.size,
|
|
85
|
+
contentType: variables.file.type || "application/octet-stream",
|
|
86
|
+
readonly: variables.readonly ?? false,
|
|
87
|
+
hidden: variables.hidden ?? false,
|
|
88
|
+
archivedAt: null,
|
|
89
|
+
assetId: null,
|
|
90
|
+
subtype: inferLocalDriveNodeSubtype({
|
|
91
|
+
name: variables.file.name,
|
|
92
|
+
contentType: variables.file.type || "application/octet-stream",
|
|
93
|
+
}),
|
|
94
|
+
links: {
|
|
95
|
+
view: null,
|
|
96
|
+
download: null,
|
|
97
|
+
preview: null,
|
|
98
|
+
thumbnail: null,
|
|
99
|
+
},
|
|
100
|
+
availability: "pending",
|
|
101
|
+
asset: {
|
|
102
|
+
status: "pending",
|
|
103
|
+
name: variables.file.name,
|
|
104
|
+
contentType: variables.file.type || "application/octet-stream",
|
|
105
|
+
size: variables.file.size,
|
|
106
|
+
visibility: variables.visibility || "private",
|
|
107
|
+
} as StorageAsset,
|
|
108
|
+
} as LocalDriveNodeWithAsset;
|
|
109
|
+
|
|
110
|
+
await optimistic?.add?.(newNode);
|
|
111
|
+
},
|
|
112
|
+
onSuccess: async (data) => {
|
|
113
|
+
await onUploadCompleted?.(data);
|
|
114
|
+
toast.success(`Succesvol geupload: ${data.name}`);
|
|
115
|
+
},
|
|
116
|
+
onError: async (error) => {
|
|
117
|
+
await onUploadFailed?.(error);
|
|
118
|
+
|
|
119
|
+
console.error("Upload error: ", error);
|
|
120
|
+
toast.error("Bestand uploaden mislukt", {
|
|
121
|
+
description: error instanceof Error ? error.message : undefined,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
onSettled: () => {
|
|
125
|
+
optimistic?.invalidate?.();
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Upload file handler
|
|
131
|
+
*/
|
|
132
|
+
const onUpload = useCallback(
|
|
133
|
+
async (file: File) => {
|
|
134
|
+
console.info("Uploading file", file);
|
|
135
|
+
const req = driveUploadClient.prepareUpload({ ...variables, file });
|
|
136
|
+
return await uploadMutation.mutateAsync(req);
|
|
137
|
+
},
|
|
138
|
+
[driveUploadClient, variables, uploadMutation],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Drop handler
|
|
143
|
+
*/
|
|
144
|
+
const onDrop = useCallback(
|
|
145
|
+
async (acceptedFiles: File[]) => {
|
|
146
|
+
if (acceptedFiles.length === 0) return;
|
|
147
|
+
await Promise.allSettled(acceptedFiles.map(onUpload));
|
|
148
|
+
},
|
|
149
|
+
[onUpload],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
153
|
+
onDrop,
|
|
154
|
+
noClick: true,
|
|
155
|
+
disabled,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div {...props} {...getRootProps()} className={cn("relative z-0", className)}>
|
|
160
|
+
<input {...getInputProps()} />
|
|
161
|
+
|
|
162
|
+
{/* Show drag overlay when drag is active */}
|
|
163
|
+
{isDragActive && (
|
|
164
|
+
<div className="absolute inset-0 z-10 rounded-md bg-primary/20 opacity-70 backdrop-blur-3xl" />
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<LocalDriveUploadZoneContext
|
|
168
|
+
value={{
|
|
169
|
+
driveUploadClient,
|
|
170
|
+
onUpload,
|
|
171
|
+
onRemove: deleteMutation.mutateAsync,
|
|
172
|
+
optimistic,
|
|
173
|
+
disabled,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</LocalDriveUploadZoneContext>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const DriveUploadZone = LocalDriveUploadZone;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { VisibilityState } from "@tanstack/react-table";
|
|
4
|
+
import type { CommandDef } from "@tulip-systems/core/commands";
|
|
5
|
+
import { FloatingCommandMenu } from "@tulip-systems/core/commands/client";
|
|
6
|
+
import { type TableColumnDef, TableLayout, TableSkeleton } from "@tulip-systems/core/data-tables";
|
|
7
|
+
import {
|
|
8
|
+
createTableConfig,
|
|
9
|
+
DataTable,
|
|
10
|
+
type InfiniteStrategyMeta,
|
|
11
|
+
TableConfigProvider,
|
|
12
|
+
type useInfiniteStrategy,
|
|
13
|
+
useTableConfigContext,
|
|
14
|
+
} from "@tulip-systems/core/data-tables/client";
|
|
15
|
+
import { type ComponentProps, useMemo } from "react";
|
|
16
|
+
import {
|
|
17
|
+
DriveGrid,
|
|
18
|
+
DriveGridBottombar,
|
|
19
|
+
DriveGridEmpty,
|
|
20
|
+
DriveGridLoading,
|
|
21
|
+
} from "@/components/grid.client";
|
|
22
|
+
import { DriveGridCard, DriveGridCardSkeleton } from "@/components/grid-card.client";
|
|
23
|
+
import { useDriveSelectionContext } from "@/components/selection.client";
|
|
24
|
+
import { useDriveViewContext } from "@/components/view.client";
|
|
25
|
+
import { localDriveColumns } from "../config/columns-data";
|
|
26
|
+
import type { LocalDriveNodeWithAsset } from "../lib/validators";
|
|
27
|
+
import { useLocalDriveContext } from "./provider.client";
|
|
28
|
+
import { useLocalDriveUploadZone } from "./upload-zone-context.client";
|
|
29
|
+
|
|
30
|
+
type LocalDriveViewProviderProps<TData extends LocalDriveNodeWithAsset = LocalDriveNodeWithAsset> =
|
|
31
|
+
ComponentProps<"div"> & {
|
|
32
|
+
queryData: TData[];
|
|
33
|
+
columns?: TableColumnDef<TData>[];
|
|
34
|
+
strategy: ReturnType<typeof useInfiniteStrategy>;
|
|
35
|
+
commands?: CommandDef<TData>[];
|
|
36
|
+
columnVisibility?: VisibilityState;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function LocalDriveViewProvider<
|
|
40
|
+
TData extends LocalDriveNodeWithAsset = LocalDriveNodeWithAsset,
|
|
41
|
+
>({
|
|
42
|
+
queryData,
|
|
43
|
+
columns = localDriveColumns as TableColumnDef<TData>[],
|
|
44
|
+
strategy,
|
|
45
|
+
commands,
|
|
46
|
+
columnVisibility,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: LocalDriveViewProviderProps<TData>) {
|
|
50
|
+
const { meta } = useLocalDriveContext();
|
|
51
|
+
const { selection } = useDriveSelectionContext();
|
|
52
|
+
|
|
53
|
+
const config = createTableConfig<TData>({
|
|
54
|
+
queryData,
|
|
55
|
+
columns,
|
|
56
|
+
strategy,
|
|
57
|
+
commands,
|
|
58
|
+
meta,
|
|
59
|
+
columnVisibility,
|
|
60
|
+
selection,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<TableConfigProvider config={config}>
|
|
65
|
+
<TableLayout {...props}>{children}</TableLayout>
|
|
66
|
+
</TableConfigProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function LocalDriveView() {
|
|
71
|
+
const { view } = useDriveViewContext();
|
|
72
|
+
|
|
73
|
+
if (view === "grid") return <LocalDriveGrid />;
|
|
74
|
+
if (view === "list") return <LocalDriveList />;
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function LocalDriveGrid(props: ComponentProps<"div">) {
|
|
80
|
+
const { queryData, strategy, commands, selection, meta } =
|
|
81
|
+
useTableConfigContext<LocalDriveNodeWithAsset>();
|
|
82
|
+
const { optimistic } = useLocalDriveUploadZone();
|
|
83
|
+
|
|
84
|
+
const { rowCount, paginationState } = strategy;
|
|
85
|
+
const { isFetching, isFetchingNextPage, fetchNextPage } = strategy.meta as InfiniteStrategyMeta;
|
|
86
|
+
|
|
87
|
+
const hasNextPage = rowCount == null ? false : queryData.length < rowCount;
|
|
88
|
+
|
|
89
|
+
const selectedData = useMemo(
|
|
90
|
+
() => queryData.filter((node) => selection?.rowSelection?.[node.id] === true),
|
|
91
|
+
[queryData, selection?.rowSelection],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<DriveGrid {...props}>
|
|
96
|
+
{queryData.length > 0 ? (
|
|
97
|
+
queryData
|
|
98
|
+
.filter((node) => !node.hidden)
|
|
99
|
+
.map((node) => <DriveGridCard key={node.id} node={node} commands={commands} />)
|
|
100
|
+
) : (
|
|
101
|
+
<DriveGridEmpty title="Geen resultaten gevonden" />
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{isFetchingNextPage &&
|
|
105
|
+
Array.from({ length: paginationState?.pageSize ?? 0 }).map((_, index) => (
|
|
106
|
+
<DriveGridCardSkeleton key={index} />
|
|
107
|
+
))}
|
|
108
|
+
|
|
109
|
+
<DriveGridBottombar
|
|
110
|
+
hasNextPage={hasNextPage}
|
|
111
|
+
fetchNextPage={fetchNextPage}
|
|
112
|
+
isFetching={isFetching}
|
|
113
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
{commands && commands.length > 0 && (
|
|
117
|
+
<FloatingCommandMenu
|
|
118
|
+
data={selectedData as never}
|
|
119
|
+
commands={commands as never}
|
|
120
|
+
meta={meta}
|
|
121
|
+
state={selectedData.length > 0 ? "open" : "closed"}
|
|
122
|
+
onSuccess={() => {
|
|
123
|
+
selection?.setRowSelection?.({});
|
|
124
|
+
void optimistic?.invalidate?.();
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
</DriveGrid>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function LocalDriveList(props: ComponentProps<typeof DataTable>) {
|
|
133
|
+
return <DataTable {...props} />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function LocalDriveViewLoading({ ...props }: ComponentProps<"div">) {
|
|
137
|
+
const { view } = useDriveViewContext();
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div {...props}>
|
|
141
|
+
{view === "grid" && <DriveGridLoading {...props} />}
|
|
142
|
+
{view === "list" && <TableSkeleton {...props} />}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { findStatus } from "@tulip-systems/core/components";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@tulip-systems/core/components/client";
|
|
5
|
+
import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/core/data-tables";
|
|
6
|
+
import { createTableSelectCell, TableTextCell } from "@tulip-systems/core/data-tables/client";
|
|
7
|
+
import { FolderIcon } from "lucide-react";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
import { createSerializer, useQueryStates } from "nuqs";
|
|
10
|
+
import { nodeSubtypeConfig, nodeSubtypeVariants } from "@/config/types";
|
|
11
|
+
import { driveTreeSearchParams } from "@/lib/search-params";
|
|
12
|
+
import type { LocalDriveNodeWithAsset } from "../lib/validators";
|
|
13
|
+
|
|
14
|
+
export const localDriveColumns: TableColumnDef<LocalDriveNodeWithAsset>[] = [
|
|
15
|
+
createTableSelectCell(),
|
|
16
|
+
{
|
|
17
|
+
id: "icon",
|
|
18
|
+
accessorKey: "icon",
|
|
19
|
+
header: () => null,
|
|
20
|
+
cell: ({ row }) => {
|
|
21
|
+
if (row.original.type === "file") {
|
|
22
|
+
const subtype = findStatus(nodeSubtypeConfig, row.original.subtype);
|
|
23
|
+
if (!subtype) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<TableTextCell className="w-4">
|
|
27
|
+
<Tooltip>
|
|
28
|
+
<TooltipTrigger>
|
|
29
|
+
<subtype.icon
|
|
30
|
+
className={nodeSubtypeVariants({
|
|
31
|
+
status: row.original.subtype,
|
|
32
|
+
className: "size-4",
|
|
33
|
+
})}
|
|
34
|
+
/>
|
|
35
|
+
</TooltipTrigger>
|
|
36
|
+
|
|
37
|
+
<TooltipContent>{subtype.label}</TooltipContent>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
</TableTextCell>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (row.original.type === "folder") {
|
|
44
|
+
return (
|
|
45
|
+
<TableTextCell className="w-4">
|
|
46
|
+
<FolderIcon className="w-4 min-w-4" />
|
|
47
|
+
</TableTextCell>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
enableSorting: false,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "name",
|
|
57
|
+
accessorKey: "name",
|
|
58
|
+
header: ({ column }) => <TableColumnHeader column={column} title="Naam" />,
|
|
59
|
+
cell: ({ row }) => {
|
|
60
|
+
const [query] = useQueryStates(driveTreeSearchParams);
|
|
61
|
+
const serialize = createSerializer(driveTreeSearchParams);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<TableTextCell className="w-full min-w-64">
|
|
65
|
+
<Link
|
|
66
|
+
href={
|
|
67
|
+
row.original.type === "file"
|
|
68
|
+
? ((row.original.links.view as string) ?? "#")
|
|
69
|
+
: serialize({ ...query, parentId: row.original.id })
|
|
70
|
+
}
|
|
71
|
+
className="truncate hover:underline"
|
|
72
|
+
target={row.original.type === "file" ? "_blank" : "_self"}
|
|
73
|
+
>
|
|
74
|
+
{row.getValue("name")}
|
|
75
|
+
</Link>
|
|
76
|
+
</TableTextCell>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
enableSorting: false,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
@@ -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 { driveTreeFilters } from "@/config/filters";
|
|
8
|
+
import { nodeSubtypes } from "../lib/constants";
|
|
9
|
+
|
|
10
|
+
export const localDriveFilters = createTableFilters({
|
|
11
|
+
...driveTreeFilters,
|
|
12
|
+
subtypes: parseFilterArray(z.array(z.enum(nodeSubtypes))),
|
|
13
|
+
hidden: parseFilterBoolean(z.boolean()),
|
|
14
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from "./components/command-file-update";
|
|
6
|
+
export * from "./components/command-file-upload";
|
|
7
|
+
export * from "./components/command-folder-create";
|
|
8
|
+
export * from "./components/command-folder-update";
|
|
9
|
+
export * from "./components/content.client";
|
|
10
|
+
export * from "./components/navigation.client";
|
|
11
|
+
export * from "./components/provider.client";
|
|
12
|
+
export * from "./components/upload-zone.client";
|
|
13
|
+
export * from "./components/upload-zone-context.client";
|
|
14
|
+
export * from "./components/view.client";
|
|
15
|
+
/**
|
|
16
|
+
* Lib
|
|
17
|
+
*/
|
|
18
|
+
export * from "./lib/upload.client";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config
|
|
3
|
+
*/
|
|
4
|
+
export * from "./config/columns-data";
|
|
5
|
+
export * from "./config/filters";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Lib
|
|
9
|
+
*/
|
|
10
|
+
export * from "./lib/constants";
|
|
11
|
+
export * from "./lib/helpers";
|
|
12
|
+
export * from "./lib/schema";
|
|
13
|
+
export * from "./lib/search-params";
|
|
14
|
+
export * from "./lib/validators";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Device sizes
|
|
2
|
+
export const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
3
|
+
export const imageSizes = deviceSizes;
|
|
4
|
+
|
|
5
|
+
// Image variants
|
|
6
|
+
export const imageVariants = [
|
|
7
|
+
"main",
|
|
8
|
+
...deviceSizes.map((size) => `preview-${size}` as const),
|
|
9
|
+
] as const;
|
|
10
|
+
export type ImageVariant = (typeof imageVariants)[number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Node subtype
|
|
14
|
+
*/
|
|
15
|
+
export const nodeSubtypes = [
|
|
16
|
+
"image",
|
|
17
|
+
"document",
|
|
18
|
+
"spreadsheet",
|
|
19
|
+
"video",
|
|
20
|
+
"audio",
|
|
21
|
+
"archive",
|
|
22
|
+
"other",
|
|
23
|
+
] as const;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ImageLoaderProps } from "next/image";
|
|
2
|
+
import type { nodeSubtypes } from "./constants";
|
|
3
|
+
import type { GetLocalFileURLSchema, LocalDriveNode, LocalNode } from "./validators";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a LocalNode to LocalDriveNode by adding provider and links
|
|
7
|
+
* @param node The LocalNode to convert
|
|
8
|
+
* @returns The converted LocalDriveNode
|
|
9
|
+
*/
|
|
10
|
+
export function toLocalDriveNode<TNode extends LocalNode>(node: TNode): LocalDriveNode {
|
|
11
|
+
return {
|
|
12
|
+
...node,
|
|
13
|
+
type: node.type as LocalDriveNode["type"],
|
|
14
|
+
readonly: node.readonly ?? false,
|
|
15
|
+
hidden: node.hidden ?? false,
|
|
16
|
+
provider: "local" as const,
|
|
17
|
+
links: {
|
|
18
|
+
view: node.type === "file" ? getLocalDriveFileUrl(node.id, { disposition: "inline" }) : null,
|
|
19
|
+
download:
|
|
20
|
+
node.type === "file" ? getLocalDriveFileUrl(node.id, { disposition: "attachment" }) : null,
|
|
21
|
+
preview: node.type === "file" ? getLocalDriveFileUrl(node.id) : null,
|
|
22
|
+
thumbnail:
|
|
23
|
+
node.type === "file" ? getLocalDriveFileUrl(node.id, { variant: "preview-256" }) : null,
|
|
24
|
+
},
|
|
25
|
+
availability: "ready" as const,
|
|
26
|
+
} satisfies LocalDriveNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if the node is a file
|
|
31
|
+
*/
|
|
32
|
+
export function isLocalDriveFile(node: Pick<LocalNode, "type">): node is LocalNode {
|
|
33
|
+
return node.type === "file";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if the node is a folder
|
|
38
|
+
*/
|
|
39
|
+
export function isLocalDriveFolder(node: Pick<LocalNode, "type">): node is LocalNode {
|
|
40
|
+
return node.type === "folder";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get file url
|
|
45
|
+
*/
|
|
46
|
+
export function getLocalDriveFileUrl(id: string, options: Partial<GetLocalFileURLSchema> = {}) {
|
|
47
|
+
const searchParams = new URLSearchParams();
|
|
48
|
+
if (options?.variant) searchParams.set("variant", options.variant);
|
|
49
|
+
if (options?.disposition) searchParams.set("disposition", options.disposition);
|
|
50
|
+
|
|
51
|
+
const queryString = searchParams.toString();
|
|
52
|
+
return `/api/drive/files/${id}${queryString ? `?${queryString}` : ""}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Image loader
|
|
57
|
+
*/
|
|
58
|
+
export function localDriveImageLoader({ src, width }: ImageLoaderProps) {
|
|
59
|
+
const url = new URL(src, window.location.origin);
|
|
60
|
+
url.searchParams.set("variant", `preview-${width}`);
|
|
61
|
+
return url.toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Node subtype inference
|
|
66
|
+
*/
|
|
67
|
+
export type LocalDriveNodeSubtype = (typeof nodeSubtypes)[number];
|
|
68
|
+
|
|
69
|
+
export function inferLocalDriveNodeSubtype({
|
|
70
|
+
name,
|
|
71
|
+
contentType,
|
|
72
|
+
}: Partial<Pick<LocalDriveNode, "name" | "contentType">>): LocalDriveNodeSubtype {
|
|
73
|
+
if (!contentType) return "other";
|
|
74
|
+
if (contentType?.startsWith("image/")) return "image";
|
|
75
|
+
if (contentType === "application/pdf") return "document";
|
|
76
|
+
if (contentType?.includes("spreadsheet")) return "spreadsheet";
|
|
77
|
+
if (contentType?.startsWith("video/")) return "video";
|
|
78
|
+
if (contentType?.startsWith("audio/")) return "audio";
|
|
79
|
+
if (contentType?.includes("zip") || contentType?.includes("archive")) return "archive";
|
|
80
|
+
|
|
81
|
+
const ext = name?.split(".").pop()?.toLowerCase();
|
|
82
|
+
if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext ?? "")) return "image";
|
|
83
|
+
if (["pdf", "doc", "docx", "txt"].includes(ext ?? "")) return "document";
|
|
84
|
+
if (["xls", "xlsx", "ods"].includes(ext ?? "")) return "spreadsheet";
|
|
85
|
+
if (["zip", "rar", "7z"].includes(ext ?? "")) return "archive";
|
|
86
|
+
|
|
87
|
+
return "other";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render bytes
|
|
92
|
+
*/
|
|
93
|
+
export function renderBytes(bytes: number) {
|
|
94
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
95
|
+
|
|
96
|
+
let size = bytes;
|
|
97
|
+
let unitIndex = 0;
|
|
98
|
+
|
|
99
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
100
|
+
size /= 1024;
|
|
101
|
+
unitIndex++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `${size.toFixed(2)}${units[unitIndex]}`;
|
|
105
|
+
}
|