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.
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +3095 -0
- package/dist/index.mjs.map +1 -0
- package/dist/templates/basic/README.md +1 -0
- package/dist/templates/basic/drizzle.config.ts +11 -0
- package/dist/templates/basic/next.config.ts +15 -0
- package/dist/templates/basic/package.json +80 -0
- package/dist/templates/basic/postcss.config.js +6 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Black.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Bold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-ExtraBold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-ExtraLight.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Italic.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Light.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Medium.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Regular.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-SemiBold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Thin.ttf +0 -0
- package/dist/templates/basic/public/icons/icon-x192.png +0 -0
- package/dist/templates/basic/public/icons/icon-x512.png +0 -0
- package/dist/templates/basic/public/images/auth-background.jpeg +0 -0
- package/dist/templates/basic/public/images/placeholder.jpeg +0 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/_components/budget-bar-chart.client.tsx +135 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/_components/date-year-picker.tsx +49 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/layout.tsx +16 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/loading.tsx +15 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/page.tsx +12 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/layout.tsx +16 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/loading.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/page.tsx +3 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/layout.tsx +21 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/loading.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/page.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/_components/command-create.tsx +104 -0
- package/dist/templates/basic/src/app/admin/drive/_components/command-upload.tsx +31 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-context.client.tsx +175 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-header.client.tsx +219 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-sidebar.tsx +61 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-view.client.tsx +49 -0
- package/dist/templates/basic/src/app/admin/drive/_components/grid.client.tsx +372 -0
- package/dist/templates/basic/src/app/admin/drive/_components/list.client.tsx +83 -0
- package/dist/templates/basic/src/app/admin/drive/_components/toolbars.tsx +74 -0
- package/dist/templates/basic/src/app/admin/drive/_config/columns-data.tsx +98 -0
- package/dist/templates/basic/src/app/admin/drive/_config/commands.tsx +197 -0
- package/dist/templates/basic/src/app/admin/drive/_config/types.tsx +90 -0
- package/dist/templates/basic/src/app/admin/drive/_lib/router.ts +72 -0
- package/dist/templates/basic/src/app/admin/drive/_lib/search-params.ts +21 -0
- package/dist/templates/basic/src/app/admin/drive/loading.tsx +10 -0
- package/dist/templates/basic/src/app/admin/error.tsx +5 -0
- package/dist/templates/basic/src/app/admin/layout.tsx +21 -0
- package/dist/templates/basic/src/app/admin/not-found.tsx +3 -0
- package/dist/templates/basic/src/app/api/auth/[...all]/route.ts +4 -0
- package/dist/templates/basic/src/app/api/rpc/[[...rest]]/route.ts +5 -0
- package/dist/templates/basic/src/app/api/storage/files/route.ts +4 -0
- package/dist/templates/basic/src/app/apple-icon.png +0 -0
- package/dist/templates/basic/src/app/auth/forget-password/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/forget-password/page.tsx +5 -0
- package/dist/templates/basic/src/app/auth/layout.tsx +13 -0
- package/dist/templates/basic/src/app/auth/login/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/login/page.tsx +5 -0
- package/dist/templates/basic/src/app/auth/reset-password/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/reset-password/page.tsx +5 -0
- package/dist/templates/basic/src/app/favicon.ico +0 -0
- package/dist/templates/basic/src/app/globals.css +3 -0
- package/dist/templates/basic/src/app/layout.tsx +17 -0
- package/dist/templates/basic/src/app/loading.tsx +3 -0
- package/dist/templates/basic/src/app/manifest.ts +4 -0
- package/dist/templates/basic/src/app/not-found.tsx +3 -0
- package/dist/templates/basic/src/instrumentation.ts +5 -0
- package/dist/templates/basic/src/lib/config/base.ts +11 -0
- package/dist/templates/basic/src/lib/config/paths.tsx +33 -0
- package/dist/templates/basic/src/proxy.ts +8 -0
- package/dist/templates/basic/src/server/auth/client.ts +6 -0
- package/dist/templates/basic/src/server/auth/init.ts +7 -0
- package/dist/templates/basic/src/server/auth/permissions.ts +46 -0
- package/dist/templates/basic/src/server/context.ts +9 -0
- package/dist/templates/basic/src/server/db/init.ts +16 -0
- package/dist/templates/basic/src/server/db/schema.ts +22 -0
- package/dist/templates/basic/src/server/db/types.ts +3 -0
- package/dist/templates/basic/src/server/providers/email.ts +3 -0
- package/dist/templates/basic/src/server/router/caller.ts +10 -0
- package/dist/templates/basic/src/server/router/client.ts +9 -0
- package/dist/templates/basic/src/server/router/init.ts +7 -0
- package/dist/templates/basic/src/server/router/register.ts +4 -0
- package/dist/templates/basic/src/server/router/router.ts +11 -0
- package/dist/templates/basic/src/server/storage/client.ts +14 -0
- package/dist/templates/basic/src/server/storage/config.ts +9 -0
- package/dist/templates/basic/src/server/storage/init.ts +14 -0
- package/dist/templates/basic/tsconfig.json +13 -0
- package/package.json +1 -1
package/dist/templates/basic/src/app/admin/(dashboard)/_components/budget-bar-chart.client.tsx
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Button, Card, CardContent, CardHeader, CardTitle } from "@tulip-systems/core/components";
|
|
5
|
+
import {
|
|
6
|
+
type ChartConfig,
|
|
7
|
+
ChartContainer,
|
|
8
|
+
ChartTooltip,
|
|
9
|
+
ChartTooltipContent,
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverContent,
|
|
12
|
+
PopoverTrigger,
|
|
13
|
+
} from "@tulip-systems/core/components/client";
|
|
14
|
+
import { formatCurrency } from "@tulip-systems/core/lib";
|
|
15
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
16
|
+
import { parseAsInteger, useQueryState } from "nuqs";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
19
|
+
import { orpc } from "@/server/router/client";
|
|
20
|
+
import { DateYearPicker } from "./date-year-picker.js";
|
|
21
|
+
|
|
22
|
+
const chartConfig = {
|
|
23
|
+
budget: {
|
|
24
|
+
label: "Budget",
|
|
25
|
+
color: "var(--chart-1)",
|
|
26
|
+
},
|
|
27
|
+
} satisfies ChartConfig;
|
|
28
|
+
|
|
29
|
+
const MONTHS = [
|
|
30
|
+
"January",
|
|
31
|
+
"February",
|
|
32
|
+
"March",
|
|
33
|
+
"April",
|
|
34
|
+
"May",
|
|
35
|
+
"June",
|
|
36
|
+
"July",
|
|
37
|
+
"August",
|
|
38
|
+
"September",
|
|
39
|
+
"October",
|
|
40
|
+
"November",
|
|
41
|
+
"December",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function BudgetBarChart() {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [year, setYear] = useQueryState(
|
|
47
|
+
"year",
|
|
48
|
+
parseAsInteger.withDefault(new Date().getFullYear()),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { data } = useSuspenseQuery(
|
|
52
|
+
orpc.projects.getBudgetByMonth.queryOptions({
|
|
53
|
+
input: { year },
|
|
54
|
+
select: (data) => {
|
|
55
|
+
if (data.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
const budgetMap = new Map(data.map((item) => [item.month.trim(), item.budget]));
|
|
58
|
+
|
|
59
|
+
return MONTHS.map((month) => ({
|
|
60
|
+
month,
|
|
61
|
+
budget: budgetMap.get(month) ?? 0,
|
|
62
|
+
}));
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader className="flex w-full flex-row flex-wrap items-center justify-between">
|
|
70
|
+
<CardTitle>Budget</CardTitle>
|
|
71
|
+
|
|
72
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
73
|
+
<PopoverTrigger asChild>
|
|
74
|
+
<Button variant="outline" className="w-36 justify-between font-normal">
|
|
75
|
+
{year ? year : "Selecteer jaar"}
|
|
76
|
+
<ChevronDownIcon />
|
|
77
|
+
</Button>
|
|
78
|
+
</PopoverTrigger>
|
|
79
|
+
<PopoverContent className="mx-10 w-auto overflow-hidden p-0" align="start">
|
|
80
|
+
<DateYearPicker
|
|
81
|
+
year={new Date(year, 0, 1)}
|
|
82
|
+
onYearChange={(date) => {
|
|
83
|
+
if (!date) return;
|
|
84
|
+
|
|
85
|
+
setYear(date.getFullYear());
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</PopoverContent>
|
|
89
|
+
</Popover>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
|
|
92
|
+
<CardContent className="px-2">
|
|
93
|
+
<div className="h-80 w-full">
|
|
94
|
+
{data ? (
|
|
95
|
+
<ChartContainer config={chartConfig} className="h-80 w-full">
|
|
96
|
+
<BarChart accessibilityLayer data={data}>
|
|
97
|
+
<CartesianGrid vertical={false} />
|
|
98
|
+
<XAxis
|
|
99
|
+
dataKey="month"
|
|
100
|
+
tickLine={false}
|
|
101
|
+
tickMargin={10}
|
|
102
|
+
axisLine={false}
|
|
103
|
+
tickFormatter={(value) => value.slice(0, 3)}
|
|
104
|
+
interval={0}
|
|
105
|
+
angle={-45}
|
|
106
|
+
textAnchor="end"
|
|
107
|
+
height={60}
|
|
108
|
+
className="md:angle-0 text-xs md:text-sm"
|
|
109
|
+
/>
|
|
110
|
+
<YAxis
|
|
111
|
+
tickLine={false}
|
|
112
|
+
axisLine={false}
|
|
113
|
+
tickFormatter={(value) => `€${(value / 1000).toFixed(0)}k`}
|
|
114
|
+
/>
|
|
115
|
+
<ChartTooltip
|
|
116
|
+
content={
|
|
117
|
+
<ChartTooltipContent
|
|
118
|
+
indicator="dashed"
|
|
119
|
+
formatter={(value) => formatCurrency(Number(value))}
|
|
120
|
+
/>
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
<Bar dataKey="budget" fill="var(--color-budget)" radius={4} />
|
|
124
|
+
</BarChart>
|
|
125
|
+
</ChartContainer>
|
|
126
|
+
) : (
|
|
127
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
128
|
+
<p className="text-muted-foreground">{year} heeft nog geen budget</p>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</CardContent>
|
|
133
|
+
</Card>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@tulip-systems/core/components";
|
|
4
|
+
|
|
5
|
+
interface DateYearPickerProps {
|
|
6
|
+
year?: Date;
|
|
7
|
+
onYearChange: (year: Date | undefined) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DateYearPicker({ year, onYearChange }: DateYearPickerProps) {
|
|
11
|
+
const currentYear = year?.getFullYear() || new Date().getFullYear();
|
|
12
|
+
|
|
13
|
+
const years = Array.from({ length: 21 }, (_, i) => currentYear - 10 + i);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="w-auto p-4">
|
|
17
|
+
<div className="grid grid-cols-3 gap-2 pb-5">
|
|
18
|
+
{years.map((y) => (
|
|
19
|
+
<Button
|
|
20
|
+
key={y}
|
|
21
|
+
variant={y === currentYear ? "default" : "ghost"}
|
|
22
|
+
size="sm"
|
|
23
|
+
onClick={() => {
|
|
24
|
+
const newDate = new Date(year || new Date());
|
|
25
|
+
newDate.setFullYear(y);
|
|
26
|
+
newDate.setHours(12, 0, 0, 0);
|
|
27
|
+
onYearChange(newDate);
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{y}
|
|
31
|
+
</Button>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex w-full justify-center border-t p-3">
|
|
35
|
+
<Button
|
|
36
|
+
size="sm"
|
|
37
|
+
variant="outline"
|
|
38
|
+
onClick={() => {
|
|
39
|
+
const today = new Date();
|
|
40
|
+
today.setHours(12, 0, 0, 0);
|
|
41
|
+
onYearChange(today);
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
Dit jaar
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Header, Topbar, TopbarTitle } from "@tulip-systems/core/components/client";
|
|
2
|
+
import type { PropsWithChildren } from "react";
|
|
3
|
+
|
|
4
|
+
export default function Layout(props: PropsWithChildren) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Header>
|
|
8
|
+
<Topbar>
|
|
9
|
+
<TopbarTitle breadcrumbs={[{ label: "Dashboard" }]} />
|
|
10
|
+
</Topbar>
|
|
11
|
+
</Header>
|
|
12
|
+
|
|
13
|
+
{props.children}
|
|
14
|
+
</>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Skeleton } from "@tulip-systems/core/components";
|
|
2
|
+
|
|
3
|
+
export default function Loading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="p-content space-y-5">
|
|
6
|
+
<div className="grid grid-rows-1 gap-5 lg:grid-cols-2 xl:grid-cols-3">
|
|
7
|
+
<Skeleton className="h-80" />
|
|
8
|
+
<Skeleton className="h-80" />
|
|
9
|
+
<Skeleton className="h-80" />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<Skeleton className="h-80" />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Allowed } from "@tulip-systems/core/auth/client";
|
|
2
|
+
import { BudgetBarChart } from "./_components/budget-bar-chart.client.js";
|
|
3
|
+
|
|
4
|
+
export default function Page() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="space-y-5 p-content">
|
|
7
|
+
<Allowed permission={{ dashboard: ["view.budget"] }}>
|
|
8
|
+
<BudgetBarChart />
|
|
9
|
+
</Allowed>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DRIVE_NAMESPACES } from "@/server/storage/config";
|
|
2
|
+
import { DriveContextProvider } from "../_components/drive-context.client.js";
|
|
3
|
+
import { DriveHeader } from "../_components/drive-header.client.js";
|
|
4
|
+
import { DriveContent } from "../_components/drive-view.client.js";
|
|
5
|
+
|
|
6
|
+
const { namespace, permission } = DRIVE_NAMESPACES.global;
|
|
7
|
+
|
|
8
|
+
export default function Layout(props: LayoutProps<"/admin/drive">) {
|
|
9
|
+
return (
|
|
10
|
+
<DriveContextProvider namespace={namespace} permission={permission}>
|
|
11
|
+
<DriveHeader namespace={namespace} />
|
|
12
|
+
|
|
13
|
+
<DriveContent>{props.children}</DriveContent>
|
|
14
|
+
</DriveContextProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import { DRIVE_NAMESPACES } from "@/server/storage/config";
|
|
3
|
+
import { DriveContextProvider } from "../_components/drive-context.client.js";
|
|
4
|
+
import { DriveHeader } from "../_components/drive-header.client.js";
|
|
5
|
+
import { DriveContent } from "../_components/drive-view.client.js";
|
|
6
|
+
|
|
7
|
+
export default async function Layout(
|
|
8
|
+
props: PropsWithChildren<{ params: Promise<{ namespace: string }> }>,
|
|
9
|
+
) {
|
|
10
|
+
const params = await props.params;
|
|
11
|
+
const { namespace, permission } =
|
|
12
|
+
DRIVE_NAMESPACES[params.namespace as keyof typeof DRIVE_NAMESPACES];
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<DriveContextProvider namespace={namespace} permission={permission}>
|
|
16
|
+
<DriveHeader namespace={namespace} />
|
|
17
|
+
|
|
18
|
+
<DriveContent>{props.children}</DriveContent>
|
|
19
|
+
</DriveContextProvider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import type { DefaultError, MutationOptions } from "@tanstack/react-query";
|
|
5
|
+
import {
|
|
6
|
+
CommandFormDialog,
|
|
7
|
+
CommandFormDialogCancel,
|
|
8
|
+
CommandFormDialogContent,
|
|
9
|
+
CommandFormDialogFields,
|
|
10
|
+
CommandFormDialogFooter,
|
|
11
|
+
CommandFormDialogHeader,
|
|
12
|
+
CommandFormDialogSubmit,
|
|
13
|
+
CommandFormDialogTitle,
|
|
14
|
+
CommandFormDialogTrigger,
|
|
15
|
+
} from "@tulip-systems/core/commands/client";
|
|
16
|
+
import { Input } from "@tulip-systems/core/components";
|
|
17
|
+
import {
|
|
18
|
+
Form,
|
|
19
|
+
FormControl,
|
|
20
|
+
FormField,
|
|
21
|
+
FormItem,
|
|
22
|
+
FormLabel,
|
|
23
|
+
FormMessage,
|
|
24
|
+
} from "@tulip-systems/core/components/client";
|
|
25
|
+
import {
|
|
26
|
+
type CreateNodeInput,
|
|
27
|
+
type CreateNodeSchema,
|
|
28
|
+
createNodeSchema,
|
|
29
|
+
} from "@tulip-systems/core/storage";
|
|
30
|
+
import { useQueryStates } from "nuqs";
|
|
31
|
+
import type { PropsWithChildren } from "react";
|
|
32
|
+
import { useForm, useFormContext } from "react-hook-form";
|
|
33
|
+
import { driveSearchParams } from "../_lib/search-params.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create folder command content
|
|
37
|
+
*/
|
|
38
|
+
export type NodeDialogCommandContentProps<
|
|
39
|
+
TData = unknown,
|
|
40
|
+
TError = DefaultError,
|
|
41
|
+
TVariables = undefined,
|
|
42
|
+
TOnMutateResult = unknown,
|
|
43
|
+
> = PropsWithChildren<{
|
|
44
|
+
variables: (values: CreateNodeSchema) => TVariables;
|
|
45
|
+
mutation: MutationOptions<TData, TError, TVariables, TOnMutateResult>;
|
|
46
|
+
defaultValues?: Partial<CreateNodeSchema>;
|
|
47
|
+
message?: string;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
export function NodeDialogCommandContent<
|
|
51
|
+
TData = unknown,
|
|
52
|
+
TError = DefaultError,
|
|
53
|
+
TVariables = undefined,
|
|
54
|
+
TOnMutateResult = unknown,
|
|
55
|
+
>(props: NodeDialogCommandContentProps<TData, TError, TVariables, TOnMutateResult>) {
|
|
56
|
+
const [{ parentId }] = useQueryStates(driveSearchParams);
|
|
57
|
+
|
|
58
|
+
const form = useForm<CreateNodeInput>({
|
|
59
|
+
resolver: zodResolver(createNodeSchema),
|
|
60
|
+
values: { ...props.defaultValues, parentId } as CreateNodeSchema,
|
|
61
|
+
resetOptions: { keepDirtyValues: true },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Form {...form}>
|
|
66
|
+
<CommandFormDialogContent variables={props.variables} mutation={props.mutation}>
|
|
67
|
+
{props.children}
|
|
68
|
+
</CommandFormDialogContent>
|
|
69
|
+
</Form>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create folder command fields
|
|
75
|
+
*/
|
|
76
|
+
export function NodeDialogCommandFields() {
|
|
77
|
+
const form = useFormContext<CreateNodeSchema>();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<CommandFormDialogFields>
|
|
81
|
+
<FormField
|
|
82
|
+
control={form.control}
|
|
83
|
+
name="name"
|
|
84
|
+
render={({ field }) => (
|
|
85
|
+
<FormItem>
|
|
86
|
+
<FormLabel>Naam</FormLabel>
|
|
87
|
+
<FormControl>
|
|
88
|
+
<Input {...field} />
|
|
89
|
+
</FormControl>
|
|
90
|
+
<FormMessage />
|
|
91
|
+
</FormItem>
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
</CommandFormDialogFields>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const NodeDialogCommand = CommandFormDialog;
|
|
99
|
+
export const NodeDialogCommandTrigger = CommandFormDialogTrigger;
|
|
100
|
+
export const NodeDialogCommandHeader = CommandFormDialogHeader;
|
|
101
|
+
export const NodeDialogCommandTitle = CommandFormDialogTitle;
|
|
102
|
+
export const NodeDialogCommandFooter = CommandFormDialogFooter;
|
|
103
|
+
export const NodeDialogCommandCancel = CommandFormDialogCancel;
|
|
104
|
+
export const NodeDialogCommandSubmit = CommandFormDialogSubmit;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CommandEmpty, CommandLabel } from "@tulip-systems/core/commands/client";
|
|
4
|
+
import { Input } from "@tulip-systems/core/components";
|
|
5
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
6
|
+
import { useUploadZone } from "@tulip-systems/core/storage/client";
|
|
7
|
+
import { UploadIcon } from "lucide-react";
|
|
8
|
+
import type { ComponentProps } from "react";
|
|
9
|
+
|
|
10
|
+
export function UploadCommand({ className, ...props }: ComponentProps<"div">) {
|
|
11
|
+
const { onUpload } = useUploadZone();
|
|
12
|
+
|
|
13
|
+
console.log("UploadCommand");
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<CommandEmpty {...props} className={cn("relative gap-2", className)} label="Upload">
|
|
17
|
+
<UploadIcon className="h-3 w-3" />
|
|
18
|
+
<CommandLabel />
|
|
19
|
+
|
|
20
|
+
<Input
|
|
21
|
+
type="file"
|
|
22
|
+
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
|
|
23
|
+
multiple
|
|
24
|
+
onChange={(e) => {
|
|
25
|
+
if (!e.target.files) return;
|
|
26
|
+
Array.from(e.target.files).forEach(onUpload);
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
</CommandEmpty>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DragDropProvider } from "@dnd-kit/react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import type { Permission } from "@tulip-systems/core/auth";
|
|
6
|
+
import { Allowed } from "@tulip-systems/core/auth/client";
|
|
7
|
+
import { tableSearchParams } from "@tulip-systems/core/data-tables";
|
|
8
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
9
|
+
import { useAction } from "@tulip-systems/core/lib/client";
|
|
10
|
+
import type { Node, NodesTableFilters } from "@tulip-systems/core/storage";
|
|
11
|
+
import { UploadZone, type UploadZoneProps } from "@tulip-systems/core/storage/client";
|
|
12
|
+
import { useQueryStates } from "nuqs";
|
|
13
|
+
import { type ComponentProps, createContext, use, useDeferredValue } from "react";
|
|
14
|
+
import { orpc } from "@/server/router/client";
|
|
15
|
+
import { uploadClient } from "@/server/storage/client";
|
|
16
|
+
import { driveFilterSearchParams, driveSearchParams } from "../_lib/search-params.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DriveContext
|
|
20
|
+
*/
|
|
21
|
+
export type DriveContextValue = {
|
|
22
|
+
namespace: string;
|
|
23
|
+
where?: Omit<NodesTableFilters, "namespace" | "parentId">;
|
|
24
|
+
onUploadCompleted?: (node: Node) => Promise<unknown> | unknown;
|
|
25
|
+
permission?: Permission;
|
|
26
|
+
};
|
|
27
|
+
export const DriveContext = createContext({} as DriveContextValue);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* useDriveContext
|
|
31
|
+
*/
|
|
32
|
+
export function useDriveContext() {
|
|
33
|
+
const context = use(DriveContext);
|
|
34
|
+
if (!context) throw new Error("DriveContext not found!");
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* DriveContextProvider
|
|
40
|
+
*/
|
|
41
|
+
export function DriveContextProvider({
|
|
42
|
+
namespace,
|
|
43
|
+
where,
|
|
44
|
+
uploadHooks,
|
|
45
|
+
onUploadCompleted,
|
|
46
|
+
onUploadFailed,
|
|
47
|
+
permission = { drive: ["view"] },
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: ComponentProps<"div"> &
|
|
52
|
+
DriveContextValue &
|
|
53
|
+
Pick<UploadZoneProps, "uploadHooks" | "onUploadCompleted" | "onUploadFailed">) {
|
|
54
|
+
const queryClient = useQueryClient();
|
|
55
|
+
|
|
56
|
+
const [{ parentId }] = useQueryStates(driveSearchParams);
|
|
57
|
+
const [filters] = useQueryStates(driveFilterSearchParams);
|
|
58
|
+
|
|
59
|
+
const [query] = useQueryStates(tableSearchParams);
|
|
60
|
+
const input = useDeferredValue({
|
|
61
|
+
...query,
|
|
62
|
+
filters: { ...filters, ...where, parentId, namespace },
|
|
63
|
+
});
|
|
64
|
+
const queryKey = orpc.drive.list.infiniteKey({
|
|
65
|
+
initialPageParam: 0,
|
|
66
|
+
input: (cursor: number | undefined) => ({ ...input, cursor }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const changeParent = useAction(
|
|
70
|
+
orpc.drive.changeParent.mutationOptions({
|
|
71
|
+
onMutate: async (data) => {
|
|
72
|
+
// Cancel any outgoing refetches
|
|
73
|
+
await queryClient.cancelQueries({ queryKey });
|
|
74
|
+
|
|
75
|
+
// Snapshot the previous value
|
|
76
|
+
const previousNodes = queryClient.getQueryData(queryKey);
|
|
77
|
+
|
|
78
|
+
// Remove the deleted node
|
|
79
|
+
queryClient.setQueryData(queryKey, (old) => {
|
|
80
|
+
if (!old) return old;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...old,
|
|
84
|
+
pages: old.pages.map((page) => ({
|
|
85
|
+
...page,
|
|
86
|
+
data: page.data.filter(({ id }) => id !== data.id),
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { previousNodes };
|
|
92
|
+
},
|
|
93
|
+
onError: (err, _, context) => {
|
|
94
|
+
console.error("changeParent error: ", err);
|
|
95
|
+
queryClient.setQueryData(queryKey, context?.previousNodes);
|
|
96
|
+
},
|
|
97
|
+
onSettled: () => {
|
|
98
|
+
queryClient.invalidateQueries({ queryKey });
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Allowed permission={permission}>
|
|
105
|
+
<DriveContext.Provider value={{ namespace, where, onUploadCompleted }}>
|
|
106
|
+
<DragDropProvider
|
|
107
|
+
onDragEnd={(event) => {
|
|
108
|
+
const { operation, canceled } = event;
|
|
109
|
+
const { source, target } = operation;
|
|
110
|
+
|
|
111
|
+
if (!source || !target || canceled) return;
|
|
112
|
+
|
|
113
|
+
const id = source.id.toString();
|
|
114
|
+
const parentId = target.id.toString() === "drive" ? null : target.id.toString();
|
|
115
|
+
|
|
116
|
+
if (id === parentId) return;
|
|
117
|
+
|
|
118
|
+
changeParent.mutate({ id, parentId });
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<UploadZone
|
|
122
|
+
{...props}
|
|
123
|
+
className={cn("min-h-dvh", className)}
|
|
124
|
+
variables={{ namespace, parentId }}
|
|
125
|
+
uploadClient={uploadClient}
|
|
126
|
+
uploadHooks={uploadHooks}
|
|
127
|
+
onUploadCompleted={onUploadCompleted}
|
|
128
|
+
onUploadFailed={onUploadFailed}
|
|
129
|
+
optimistic={{
|
|
130
|
+
add: (node) => {
|
|
131
|
+
queryClient.setQueryData(queryKey, (old) => {
|
|
132
|
+
if (!old) return old;
|
|
133
|
+
|
|
134
|
+
const firstPage = old.pages[0];
|
|
135
|
+
if (!firstPage) return old;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add the new node to the first page as the first item
|
|
139
|
+
*/
|
|
140
|
+
return {
|
|
141
|
+
...old,
|
|
142
|
+
pages: [
|
|
143
|
+
{
|
|
144
|
+
...firstPage,
|
|
145
|
+
data: [node, ...(firstPage.data ?? [])],
|
|
146
|
+
},
|
|
147
|
+
...old.pages.slice(1),
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
remove: (ids) => {
|
|
153
|
+
queryClient.setQueryData(queryKey, (old) => {
|
|
154
|
+
if (!old) return old;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...old,
|
|
158
|
+
pages: old.pages.map((page) => ({
|
|
159
|
+
...page,
|
|
160
|
+
data: page.data.filter(({ id }) => !ids.includes(id)),
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
cancel: () => queryClient.cancelQueries({ queryKey }),
|
|
166
|
+
invalidate: () => queryClient.invalidateQueries({ queryKey }),
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{children}
|
|
170
|
+
</UploadZone>
|
|
171
|
+
</DragDropProvider>
|
|
172
|
+
</DriveContext.Provider>
|
|
173
|
+
</Allowed>
|
|
174
|
+
);
|
|
175
|
+
}
|