create-tulip-app 0.6.1 → 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 (90) hide show
  1. package/dist/index.d.mts +1 -0
  2. package/dist/index.mjs +3095 -0
  3. package/dist/index.mjs.map +1 -0
  4. package/dist/templates/basic/README.md +1 -0
  5. package/dist/templates/basic/drizzle.config.ts +11 -0
  6. package/dist/templates/basic/next.config.ts +15 -0
  7. package/dist/templates/basic/package.json +80 -0
  8. package/dist/templates/basic/postcss.config.js +6 -0
  9. package/dist/templates/basic/public/fonts/Inter/Inter-Black.ttf +0 -0
  10. package/dist/templates/basic/public/fonts/Inter/Inter-Bold.ttf +0 -0
  11. package/dist/templates/basic/public/fonts/Inter/Inter-ExtraBold.ttf +0 -0
  12. package/dist/templates/basic/public/fonts/Inter/Inter-ExtraLight.ttf +0 -0
  13. package/dist/templates/basic/public/fonts/Inter/Inter-Italic.ttf +0 -0
  14. package/dist/templates/basic/public/fonts/Inter/Inter-Light.ttf +0 -0
  15. package/dist/templates/basic/public/fonts/Inter/Inter-Medium.ttf +0 -0
  16. package/dist/templates/basic/public/fonts/Inter/Inter-Regular.ttf +0 -0
  17. package/dist/templates/basic/public/fonts/Inter/Inter-SemiBold.ttf +0 -0
  18. package/dist/templates/basic/public/fonts/Inter/Inter-Thin.ttf +0 -0
  19. package/dist/templates/basic/public/icons/icon-x192.png +0 -0
  20. package/dist/templates/basic/public/icons/icon-x512.png +0 -0
  21. package/dist/templates/basic/public/images/auth-background.jpeg +0 -0
  22. package/dist/templates/basic/public/images/placeholder.jpeg +0 -0
  23. package/dist/templates/basic/src/app/admin/(dashboard)/_components/budget-bar-chart.client.tsx +135 -0
  24. package/dist/templates/basic/src/app/admin/(dashboard)/_components/date-year-picker.tsx +49 -0
  25. package/dist/templates/basic/src/app/admin/(dashboard)/layout.tsx +16 -0
  26. package/dist/templates/basic/src/app/admin/(dashboard)/loading.tsx +15 -0
  27. package/dist/templates/basic/src/app/admin/(dashboard)/page.tsx +12 -0
  28. package/dist/templates/basic/src/app/admin/drive/(root)/layout.tsx +16 -0
  29. package/dist/templates/basic/src/app/admin/drive/(root)/loading.tsx +5 -0
  30. package/dist/templates/basic/src/app/admin/drive/(root)/page.tsx +3 -0
  31. package/dist/templates/basic/src/app/admin/drive/[namespace]/layout.tsx +21 -0
  32. package/dist/templates/basic/src/app/admin/drive/[namespace]/loading.tsx +5 -0
  33. package/dist/templates/basic/src/app/admin/drive/[namespace]/page.tsx +5 -0
  34. package/dist/templates/basic/src/app/admin/drive/_components/command-create.tsx +104 -0
  35. package/dist/templates/basic/src/app/admin/drive/_components/command-upload.tsx +31 -0
  36. package/dist/templates/basic/src/app/admin/drive/_components/drive-context.client.tsx +175 -0
  37. package/dist/templates/basic/src/app/admin/drive/_components/drive-header.client.tsx +219 -0
  38. package/dist/templates/basic/src/app/admin/drive/_components/drive-sidebar.tsx +61 -0
  39. package/dist/templates/basic/src/app/admin/drive/_components/drive-view.client.tsx +49 -0
  40. package/dist/templates/basic/src/app/admin/drive/_components/grid.client.tsx +372 -0
  41. package/dist/templates/basic/src/app/admin/drive/_components/list.client.tsx +83 -0
  42. package/dist/templates/basic/src/app/admin/drive/_components/toolbars.tsx +74 -0
  43. package/dist/templates/basic/src/app/admin/drive/_config/columns-data.tsx +98 -0
  44. package/dist/templates/basic/src/app/admin/drive/_config/commands.tsx +197 -0
  45. package/dist/templates/basic/src/app/admin/drive/_config/types.tsx +90 -0
  46. package/dist/templates/basic/src/app/admin/drive/_lib/router.ts +72 -0
  47. package/dist/templates/basic/src/app/admin/drive/_lib/search-params.ts +21 -0
  48. package/dist/templates/basic/src/app/admin/drive/loading.tsx +10 -0
  49. package/dist/templates/basic/src/app/admin/error.tsx +5 -0
  50. package/dist/templates/basic/src/app/admin/layout.tsx +21 -0
  51. package/dist/templates/basic/src/app/admin/not-found.tsx +3 -0
  52. package/dist/templates/basic/src/app/api/auth/[...all]/route.ts +4 -0
  53. package/dist/templates/basic/src/app/api/rpc/[[...rest]]/route.ts +5 -0
  54. package/dist/templates/basic/src/app/api/storage/files/route.ts +4 -0
  55. package/dist/templates/basic/src/app/apple-icon.png +0 -0
  56. package/dist/templates/basic/src/app/auth/forget-password/loading.tsx +3 -0
  57. package/dist/templates/basic/src/app/auth/forget-password/page.tsx +5 -0
  58. package/dist/templates/basic/src/app/auth/layout.tsx +13 -0
  59. package/dist/templates/basic/src/app/auth/login/loading.tsx +3 -0
  60. package/dist/templates/basic/src/app/auth/login/page.tsx +5 -0
  61. package/dist/templates/basic/src/app/auth/reset-password/loading.tsx +3 -0
  62. package/dist/templates/basic/src/app/auth/reset-password/page.tsx +5 -0
  63. package/dist/templates/basic/src/app/favicon.ico +0 -0
  64. package/dist/templates/basic/src/app/globals.css +3 -0
  65. package/dist/templates/basic/src/app/layout.tsx +17 -0
  66. package/dist/templates/basic/src/app/loading.tsx +3 -0
  67. package/dist/templates/basic/src/app/manifest.ts +4 -0
  68. package/dist/templates/basic/src/app/not-found.tsx +3 -0
  69. package/dist/templates/basic/src/instrumentation.ts +5 -0
  70. package/dist/templates/basic/src/lib/config/base.ts +11 -0
  71. package/dist/templates/basic/src/lib/config/paths.tsx +33 -0
  72. package/dist/templates/basic/src/proxy.ts +8 -0
  73. package/dist/templates/basic/src/server/auth/client.ts +6 -0
  74. package/dist/templates/basic/src/server/auth/init.ts +7 -0
  75. package/dist/templates/basic/src/server/auth/permissions.ts +46 -0
  76. package/dist/templates/basic/src/server/context.ts +9 -0
  77. package/dist/templates/basic/src/server/db/init.ts +16 -0
  78. package/dist/templates/basic/src/server/db/schema.ts +22 -0
  79. package/dist/templates/basic/src/server/db/types.ts +3 -0
  80. package/dist/templates/basic/src/server/providers/email.ts +3 -0
  81. package/dist/templates/basic/src/server/router/caller.ts +10 -0
  82. package/dist/templates/basic/src/server/router/client.ts +9 -0
  83. package/dist/templates/basic/src/server/router/init.ts +7 -0
  84. package/dist/templates/basic/src/server/router/register.ts +4 -0
  85. package/dist/templates/basic/src/server/router/router.ts +11 -0
  86. package/dist/templates/basic/src/server/storage/client.ts +14 -0
  87. package/dist/templates/basic/src/server/storage/config.ts +9 -0
  88. package/dist/templates/basic/src/server/storage/init.ts +14 -0
  89. package/dist/templates/basic/tsconfig.json +13 -0
  90. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
4
+ import type { VisibilityState } from "@tanstack/react-table";
5
+ import { TableLayout, tableSearchParams } from "@tulip-systems/core/data-tables";
6
+ import {
7
+ createTableConfig,
8
+ DataTable,
9
+ TableConfigProvider,
10
+ useInfiniteStrategy,
11
+ } from "@tulip-systems/core/data-tables/client";
12
+ import { useQueryStates } from "nuqs";
13
+ import { useDeferredValue, useMemo } from "react";
14
+ import { orpc } from "@/server/router/client";
15
+ import { type DriveColumn, driveColumns } from "../_config/columns-data.js";
16
+ import { driveCommands } from "../_config/commands.js";
17
+ import { driveFilterSearchParams, driveSearchParams } from "../_lib/search-params.js";
18
+ import { useDriveContext } from "./drive-context.client.js";
19
+ import { DriveToolbar } from "./toolbars.js";
20
+
21
+ /**
22
+ * Drive List
23
+ */
24
+ type DriveListProps = {
25
+ columnVisibility?: VisibilityState;
26
+ };
27
+
28
+ export function DriveList({ columnVisibility }: DriveListProps) {
29
+ const [{ parentId }] = useQueryStates(driveSearchParams);
30
+ const { namespace, where } = useDriveContext();
31
+
32
+ const [query] = useQueryStates(tableSearchParams);
33
+ const [filters] = useQueryStates(driveFilterSearchParams);
34
+ const input = useDeferredValue({
35
+ ...query,
36
+ filters: { ...filters, ...where, parentId, namespace },
37
+ });
38
+
39
+ /**
40
+ * Data
41
+ */
42
+ const response = useSuspenseInfiniteQuery(
43
+ orpc.drive.list.infiniteOptions({
44
+ initialPageParam: 0,
45
+ input: (cursor: number | undefined) => ({ ...input, cursor }),
46
+ getNextPageParam: ({ pagination }) => pagination.nextCursor,
47
+ getPreviousPageParam: ({ pagination }) => pagination.previousCursor,
48
+ }),
49
+ );
50
+
51
+ const { cursor, total } = response.data.pages.at(-1)?.pagination ?? {};
52
+
53
+ const queryData = useMemo(
54
+ () => response.data.pages.flatMap(({ data }) => data) ?? [],
55
+ [response.data],
56
+ );
57
+
58
+ /**
59
+ * Strategy
60
+ */
61
+ const strategy = useInfiniteStrategy({ ...response, cursor, total });
62
+
63
+ /**
64
+ * Table
65
+ */
66
+ const config = createTableConfig<DriveColumn>({
67
+ queryData,
68
+ columns: driveColumns,
69
+ strategy,
70
+ commands: driveCommands,
71
+ where,
72
+ columnVisibility,
73
+ });
74
+
75
+ return (
76
+ <TableLayout>
77
+ <TableConfigProvider config={config}>
78
+ <DriveToolbar />
79
+ <DataTable />
80
+ </TableConfigProvider>
81
+ </TableLayout>
82
+ );
83
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { Input } from "@tulip-systems/core/components";
4
+ import {
5
+ parseFiltersVisibility,
6
+ resolveFilterDescriptors,
7
+ TableToolbar,
8
+ TableToolbarList,
9
+ tableSearchParams,
10
+ } from "@tulip-systems/core/data-tables";
11
+ import { TableFilterCombobox } from "@tulip-systems/core/data-tables/client";
12
+ import { nodesTableFilters } from "@tulip-systems/core/storage";
13
+ import { FileIcon, FolderIcon } from "lucide-react";
14
+ import { useQueryStates } from "nuqs";
15
+ import { type PropsWithChildren, Suspense, startTransition } from "react";
16
+
17
+ const nodesTableFiltersDescriptors = resolveFilterDescriptors(nodesTableFilters);
18
+
19
+ const filtersVisibilityConfig = ["types"] satisfies (keyof typeof nodesTableFiltersDescriptors)[];
20
+
21
+ export function DriveToolbar(
22
+ props: PropsWithChildren<{
23
+ filtersVisibility?: Partial<Record<(typeof filtersVisibilityConfig)[number], boolean>>;
24
+ }>,
25
+ ) {
26
+ const visibilityState = parseFiltersVisibility(filtersVisibilityConfig, props.filtersVisibility);
27
+
28
+ const [query, setQuery] = useQueryStates(tableSearchParams, { shallow: false, startTransition });
29
+ const defaultValue = query.search ?? "";
30
+
31
+ return (
32
+ <TableToolbar>
33
+ <TableToolbarList>
34
+ <Input
35
+ placeholder="Zoeken..."
36
+ defaultValue={defaultValue}
37
+ onChange={(e) => {
38
+ const value = e.target.value;
39
+ setQuery({ cursor: null, search: value ? value : null });
40
+ }}
41
+ className="h-8 w-[150px] min-w-fit lg:w-[250px]"
42
+ />
43
+
44
+ <Suspense>{visibilityState.types && <DriveTypeFilter />}</Suspense>
45
+ </TableToolbarList>
46
+
47
+ <TableToolbarList>{props.children}</TableToolbarList>
48
+ </TableToolbar>
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Project status filter
54
+ */
55
+ export function DriveTypeFilter() {
56
+ return (
57
+ <TableFilterCombobox
58
+ title="Type"
59
+ filter={nodesTableFiltersDescriptors.types}
60
+ options={[
61
+ {
62
+ label: "Folders",
63
+ value: "folder",
64
+ icon: FolderIcon,
65
+ },
66
+ {
67
+ label: "Bestanden",
68
+ value: "file",
69
+ icon: FileIcon,
70
+ },
71
+ ]}
72
+ />
73
+ );
74
+ }
@@ -0,0 +1,98 @@
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 { getFileUrl, isFile, isFolder, type Node } from "@tulip-systems/core/storage";
8
+ import { FolderIcon } from "lucide-react";
9
+ import Link from "next/link";
10
+ import { createSerializer, useQueryStates } from "nuqs";
11
+ import { driveSearchParams } from "../_lib/search-params.js";
12
+ import { nodeSubtypeConfig, nodeSubtypeVariants } from "./types.js";
13
+
14
+ export type DriveColumn = Node;
15
+
16
+ export const driveColumns: TableColumnDef<DriveColumn>[] = [
17
+ createTableSelectCell(),
18
+ {
19
+ id: "icon",
20
+ accessorKey: "icon",
21
+ header: () => null,
22
+ cell: ({ row }) => {
23
+ if (isFile(row.original)) {
24
+ const subtype = findStatus(nodeSubtypeConfig, row.original.subtype);
25
+ if (!subtype) return null;
26
+
27
+ return (
28
+ <TableTextCell className="w-4">
29
+ <Tooltip>
30
+ <TooltipTrigger>
31
+ <subtype.icon
32
+ className={nodeSubtypeVariants({
33
+ status: row.original.subtype,
34
+ className: "size-4",
35
+ })}
36
+ />
37
+ </TooltipTrigger>
38
+
39
+ <TooltipContent>{subtype.label}</TooltipContent>
40
+ </Tooltip>
41
+ </TableTextCell>
42
+ );
43
+ }
44
+
45
+ if (isFolder(row.original)) {
46
+ return (
47
+ <TableTextCell className="w-4">
48
+ <FolderIcon className="w-4 min-w-4" />
49
+ </TableTextCell>
50
+ );
51
+ }
52
+
53
+ return null;
54
+ },
55
+ enableSorting: false,
56
+ },
57
+ {
58
+ id: "name",
59
+ accessorKey: "name",
60
+ header: ({ column }) => <TableColumnHeader column={column} title="Naam" />,
61
+ cell: ({ row }) => {
62
+ const [query] = useQueryStates(driveSearchParams);
63
+ const serialize = createSerializer(driveSearchParams);
64
+
65
+ return (
66
+ <TableTextCell className="w-full min-w-64">
67
+ <Link
68
+ href={
69
+ row.original.type === "file"
70
+ ? getFileUrl(row.original.id)
71
+ : serialize({ ...query, parentId: row.original.id })
72
+ }
73
+ className="truncate hover:underline"
74
+ target={row.original.type === "file" ? "_blank" : "_self"}
75
+ >
76
+ {row.getValue("name")}
77
+ </Link>
78
+ </TableTextCell>
79
+ );
80
+ },
81
+ enableSorting: false,
82
+ },
83
+ // {
84
+ // id: "subtype",
85
+ // accessorKey: "subtype",
86
+ // header: () => null,
87
+ // cell: ({ row }) => (
88
+ // <TableTextCell className="min-w-40">
89
+ // {isFile(row.original) ? <NodeSubtypeField subtype={row.original.subtype} /> : null}
90
+ // </TableTextCell>
91
+ // ),
92
+ // },
93
+ // {
94
+ // accessorKey: "size",
95
+ // header: ({ column }) => <TableColumnHeader column={column} title="Grootte" />,
96
+ // cell: ({ row }) => <TableTextCell>{row.getValue("size")}</TableTextCell>,
97
+ // },
98
+ ];
@@ -0,0 +1,197 @@
1
+ "use client";
2
+
3
+ import type { CommandDef } from "@tulip-systems/core/commands";
4
+ import {
5
+ ArchiveCommand,
6
+ CommandLabel,
7
+ DeleteCommand,
8
+ RestoreCommand,
9
+ } from "@tulip-systems/core/commands/client";
10
+ import { toast } from "@tulip-systems/core/components/client";
11
+ import type { Node } from "@tulip-systems/core/storage";
12
+ import { FilePen, FolderPen, FolderPlus } from "lucide-react";
13
+ import { orpc } from "@/server/router/client";
14
+ import {
15
+ NodeDialogCommand,
16
+ NodeDialogCommandCancel,
17
+ NodeDialogCommandContent,
18
+ NodeDialogCommandFields,
19
+ NodeDialogCommandFooter,
20
+ NodeDialogCommandHeader,
21
+ NodeDialogCommandSubmit,
22
+ NodeDialogCommandTitle,
23
+ NodeDialogCommandTrigger,
24
+ } from "../_components/command-create.js";
25
+ import { UploadCommand } from "../_components/command-upload.js";
26
+
27
+ /**
28
+ * Node commands
29
+ */
30
+ export const driveCommands: CommandDef<Node>[] = [
31
+ {
32
+ name: "folders-update",
33
+ mode: "single",
34
+ permission: { drive: ["update"] },
35
+ conditions: ({ data }) => [data.type === "folder", !data.readonly],
36
+ render: ({ id, data }) => (
37
+ <NodeDialogCommand>
38
+ <NodeDialogCommandTrigger label="Wijzigen">
39
+ <FolderPen className="size-4" />
40
+ <CommandLabel />
41
+ </NodeDialogCommandTrigger>
42
+
43
+ <NodeDialogCommandContent
44
+ defaultValues={data}
45
+ variables={(data) => ({ id, data })}
46
+ mutation={orpc.drive.updateNode.mutationOptions({
47
+ onSuccess: () => {
48
+ toast.success("Folder succesvol gewijzigd");
49
+ },
50
+ })}
51
+ >
52
+ <NodeDialogCommandHeader>
53
+ <NodeDialogCommandTitle>Folder Wijzigen</NodeDialogCommandTitle>
54
+ </NodeDialogCommandHeader>
55
+
56
+ <NodeDialogCommandFields />
57
+
58
+ <NodeDialogCommandFooter>
59
+ <NodeDialogCommandCancel>Annuleren</NodeDialogCommandCancel>
60
+ <NodeDialogCommandSubmit>Opslaan</NodeDialogCommandSubmit>
61
+ </NodeDialogCommandFooter>
62
+ </NodeDialogCommandContent>
63
+ </NodeDialogCommand>
64
+ ),
65
+ },
66
+ {
67
+ name: "file-update",
68
+ mode: "single",
69
+ permission: { drive: ["update"] },
70
+ conditions: ({ data }) => [data.type === "file", !data.readonly],
71
+ render: ({ id, data }) => {
72
+ const [name, extension] = data.name.split(".");
73
+
74
+ return (
75
+ <NodeDialogCommand>
76
+ <NodeDialogCommandTrigger label="Wijzigen">
77
+ <FilePen className="size-4" />
78
+ <CommandLabel />
79
+ </NodeDialogCommandTrigger>
80
+
81
+ <NodeDialogCommandContent
82
+ defaultValues={{ ...data, name }}
83
+ variables={(data) => ({
84
+ id,
85
+ data: { ...data, name: `${data.name}.${extension}` },
86
+ })}
87
+ mutation={orpc.drive.updateNode.mutationOptions({
88
+ onSuccess: () => {
89
+ toast.success("Bestand succesvol gewijzigd");
90
+ },
91
+ })}
92
+ >
93
+ <NodeDialogCommandHeader>
94
+ <NodeDialogCommandTitle>File Wijzigen</NodeDialogCommandTitle>
95
+ </NodeDialogCommandHeader>
96
+
97
+ <NodeDialogCommandFields />
98
+
99
+ <NodeDialogCommandFooter>
100
+ <NodeDialogCommandCancel>Annuleren</NodeDialogCommandCancel>
101
+ <NodeDialogCommandSubmit>Opslaan</NodeDialogCommandSubmit>
102
+ </NodeDialogCommandFooter>
103
+ </NodeDialogCommandContent>
104
+ </NodeDialogCommand>
105
+ );
106
+ },
107
+ },
108
+ {
109
+ name: "node-archive",
110
+ mode: "bulk",
111
+ permission: { drive: ["archive"] },
112
+ conditions: ({ data }) => data.every((item) => !item.isDeleted),
113
+ render: ({ ids }) => (
114
+ <ArchiveCommand
115
+ variables={{ ids }}
116
+ mutation={orpc.drive.archive.mutationOptions({
117
+ onSuccess: () => toast.success("Succesvol gearchiveerd"),
118
+ })}
119
+ />
120
+ ),
121
+ },
122
+ {
123
+ name: "node-restore",
124
+ mode: "bulk",
125
+ permission: { drive: ["archive"] },
126
+ conditions: ({ data }) => data.every((item) => item.isDeleted),
127
+ render: ({ ids }) => (
128
+ <RestoreCommand
129
+ variables={{ ids }}
130
+ mutation={orpc.drive.restore.mutationOptions({
131
+ onSuccess: () => toast.success("Succesvol hersteld"),
132
+ })}
133
+ />
134
+ ),
135
+ },
136
+ {
137
+ name: "node-delete",
138
+ mode: "bulk",
139
+ permission: { drive: ["delete"] },
140
+ render: ({ ids }) => (
141
+ <DeleteCommand
142
+ variables={{ ids }}
143
+ mutation={orpc.drive.deleteNodes.mutationOptions({
144
+ onSuccess: () => {
145
+ toast.success("Succesvol verwijderd");
146
+ },
147
+ })}
148
+ />
149
+ ),
150
+ },
151
+ ];
152
+
153
+ /**
154
+ * Project commands
155
+ */
156
+ export const driveGlobalCommands: CommandDef<{ namespace: string }>[] = [
157
+ {
158
+ name: "folders-create",
159
+ mode: "single",
160
+ permission: { drive: ["create"] },
161
+ render: ({ data }) => (
162
+ <NodeDialogCommand>
163
+ <NodeDialogCommandTrigger label="Folder toevoegen" hotkey="mod+i">
164
+ <FolderPlus className="size-4" />
165
+ <CommandLabel />
166
+ </NodeDialogCommandTrigger>
167
+
168
+ <NodeDialogCommandContent
169
+ defaultValues={{ ...data, type: "folder" }}
170
+ variables={(values) => values}
171
+ mutation={orpc.drive.createFolder.mutationOptions({
172
+ onSuccess: () => {
173
+ toast.success("Folder succesvol toegevoegd");
174
+ },
175
+ })}
176
+ >
177
+ <NodeDialogCommandHeader>
178
+ <NodeDialogCommandTitle>Folder toevoegen</NodeDialogCommandTitle>
179
+ </NodeDialogCommandHeader>
180
+
181
+ <NodeDialogCommandFields />
182
+
183
+ <NodeDialogCommandFooter>
184
+ <NodeDialogCommandCancel>Annuleren</NodeDialogCommandCancel>
185
+ <NodeDialogCommandSubmit>Toevoegen</NodeDialogCommandSubmit>
186
+ </NodeDialogCommandFooter>
187
+ </NodeDialogCommandContent>
188
+ </NodeDialogCommand>
189
+ ),
190
+ },
191
+ {
192
+ name: "files-upload",
193
+ mode: "single",
194
+ permission: { drive: ["create"] },
195
+ render: () => <UploadCommand />,
196
+ },
197
+ ];
@@ -0,0 +1,90 @@
1
+ import {
2
+ createStatusConfig,
3
+ createStatusVariants,
4
+ StatusBadge,
5
+ StatusField,
6
+ } from "@tulip-systems/core/components";
7
+ import type { NodeSubtype } from "@tulip-systems/core/storage";
8
+ import { cva } from "class-variance-authority";
9
+ import {
10
+ FileArchiveIcon,
11
+ FileAudioIcon,
12
+ FileIcon,
13
+ FileImageIcon,
14
+ FileSpreadsheetIcon,
15
+ FileTextIcon,
16
+ FileVideoIcon,
17
+ } from "lucide-react";
18
+
19
+ /**
20
+ * Node subtype config
21
+ */
22
+ export const nodeSubtypeConfig = createStatusConfig<NodeSubtype>([
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: NodeSubtype }) {
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: NodeSubtype }) {
87
+ return (
88
+ <StatusField config={nodeSubtypeConfig} variants={nodeSubtypeVariants} status={props.subtype} />
89
+ );
90
+ }
@@ -0,0 +1,72 @@
1
+ import "server-cli-only";
2
+
3
+ import { tableQuerySchema } from "@tulip-systems/core/data-tables";
4
+ import { createTableQueryResponse, getTableData } from "@tulip-systems/core/data-tables/server";
5
+ import { bulkActionSchema } from "@tulip-systems/core/router";
6
+ import { nodes, nodesTableFiltersSchema } from "@tulip-systems/core/storage";
7
+ import { createDriveBaseProcedures } from "@tulip-systems/core/storage/server";
8
+ import { and, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm";
9
+ import { revalidatePath } from "next/cache";
10
+ import { permissionMiddleware, protectedProcedure } from "@/server/router/init";
11
+ import type { DriveColumn } from "../_config/columns-data.js";
12
+
13
+ export const driveRouter = {
14
+ /**
15
+ * Base procedures
16
+ */
17
+ ...createDriveBaseProcedures(),
18
+ /**
19
+ * Extended procedures
20
+ */
21
+
22
+ list: protectedProcedure
23
+ .use(permissionMiddleware({ drive: ["view"] }))
24
+ .input(tableQuerySchema.extend({ filters: nodesTableFiltersSchema }))
25
+ .handler(async ({ context, input: { filters, ...input } }) => {
26
+ const { data, total } = await getTableData(context, input, {
27
+ table: nodes,
28
+ where: and(
29
+ filters.nodeIds != null ? inArray(nodes.id, filters.nodeIds) : undefined,
30
+ filters.types != null ? inArray(nodes.type, filters.types) : undefined,
31
+ filters.isDeleted != null ? eq(nodes.isDeleted, filters.isDeleted) : undefined,
32
+ filters.isOrphaned === true
33
+ ? isNotNull(nodes.orphanedAt)
34
+ : filters.isOrphaned === false
35
+ ? isNull(nodes.orphanedAt)
36
+ : undefined,
37
+ filters.hidden != null ? eq(nodes.hidden, filters.hidden) : undefined,
38
+ filters.parentId ? eq(nodes.parentId, filters.parentId) : isNull(nodes.parentId),
39
+ eq(nodes.namespace, filters.namespace),
40
+ ),
41
+ searchFields: [nodes.name],
42
+ defaultOrder: [desc(nodes.createdAt)],
43
+ });
44
+
45
+ return createTableQueryResponse<DriveColumn>({ data, input, total });
46
+ }),
47
+ archive: protectedProcedure
48
+ .use(permissionMiddleware({ drive: ["archive"] }))
49
+ .input(bulkActionSchema)
50
+ .handler(async ({ context, input }) => {
51
+ await context.db
52
+ .update(nodes)
53
+ .set({ isDeleted: true })
54
+ .where(inArray(nodes.id, input.ids))
55
+ .returning();
56
+
57
+ revalidatePath("/drive", "page");
58
+ }),
59
+
60
+ restore: protectedProcedure
61
+ .use(permissionMiddleware({ drive: ["archive"] }))
62
+ .input(bulkActionSchema)
63
+ .handler(async ({ context, input }) => {
64
+ await context.db
65
+ .update(nodes)
66
+ .set({ isDeleted: false })
67
+ .where(inArray(nodes.id, input.ids))
68
+ .returning();
69
+
70
+ revalidatePath("/drive", "page");
71
+ }),
72
+ };
@@ -0,0 +1,21 @@
1
+ import { resolveFiltersSearchParams } from "@tulip-systems/core/data-tables";
2
+ import { nodesTableFilters } from "@tulip-systems/core/storage";
3
+ import { parseAsStringEnum } from "nuqs";
4
+ import { createLoader, parseAsString } from "nuqs/server";
5
+
6
+ /**
7
+ * Params
8
+ */
9
+ export const driveSearchParams = {
10
+ parentId: parseAsString,
11
+ view: parseAsStringEnum(["list", "grid"]).withDefault("grid"),
12
+ };
13
+
14
+ export const loadDriveGridSearchParams = createLoader(driveSearchParams);
15
+
16
+ /**
17
+ * Filters
18
+ */
19
+ export const driveFilterSearchParams = resolveFiltersSearchParams(nodesTableFilters);
20
+
21
+ export const loadDriveGridFilterSearchParams = createLoader(driveFilterSearchParams);
@@ -0,0 +1,10 @@
1
+ import { AdminLoadingLayout } from "@tulip-systems/core/components";
2
+ import { DriveGridLoading } from "./_components/grid.client.js";
3
+
4
+ export default function Loading() {
5
+ return (
6
+ <AdminLoadingLayout>
7
+ <DriveGridLoading className="p-content" />
8
+ </AdminLoadingLayout>
9
+ );
10
+ }
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ import { AdminErrorPage } from "@tulip-systems/core/components";
4
+
5
+ export default AdminErrorPage;
@@ -0,0 +1,21 @@
1
+ import { AuthProvider } from "@tulip-systems/core/auth/client";
2
+ import { AuthGuard } from "@tulip-systems/core/auth/server";
3
+ import { AdminContent } from "@tulip-systems/core/components/client";
4
+ import { AdminLayout } from "@tulip-systems/core/components/server";
5
+ import type { PropsWithChildren } from "react";
6
+ import { AdminSidebar } from "@/lib/config/paths";
7
+ import { authClient } from "@/server/auth/client";
8
+ import { auth } from "@/server/auth/init";
9
+
10
+ export default async function Layout(props: PropsWithChildren) {
11
+ return (
12
+ <AuthGuard auth={auth}>
13
+ <AuthProvider authClient={authClient}>
14
+ <AdminLayout>
15
+ <AdminSidebar />
16
+ <AdminContent>{props.children}</AdminContent>
17
+ </AdminLayout>
18
+ </AuthProvider>
19
+ </AuthGuard>
20
+ );
21
+ }
@@ -0,0 +1,3 @@
1
+ import { AdminNotFoundPage } from "@tulip-systems/core/components";
2
+
3
+ export default AdminNotFoundPage;
@@ -0,0 +1,4 @@
1
+ import { handleAuthRoute } from "@tulip-systems/core/auth/server";
2
+ import { auth } from "@/server/auth/init";
3
+
4
+ export const { GET, POST } = handleAuthRoute({ auth });