@buildpad/cli 0.1.4

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 (44) hide show
  1. package/README.md +557 -0
  2. package/dist/chunk-J4KKVECI.js +365 -0
  3. package/dist/chunk-J4KKVECI.js.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +2582 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/outdated-JMAYAZ7W.js +110 -0
  8. package/dist/outdated-JMAYAZ7W.js.map +1 -0
  9. package/dist/templates/api/auth-callback-route.ts +36 -0
  10. package/dist/templates/api/auth-headers.ts +72 -0
  11. package/dist/templates/api/auth-login-route.ts +63 -0
  12. package/dist/templates/api/auth-logout-route.ts +41 -0
  13. package/dist/templates/api/auth-user-route.ts +71 -0
  14. package/dist/templates/api/collections-route.ts +54 -0
  15. package/dist/templates/api/fields-route.ts +44 -0
  16. package/dist/templates/api/files-id-route.ts +116 -0
  17. package/dist/templates/api/files-route.ts +83 -0
  18. package/dist/templates/api/items-id-route.ts +120 -0
  19. package/dist/templates/api/items-route.ts +88 -0
  20. package/dist/templates/api/login-page.tsx +142 -0
  21. package/dist/templates/api/permissions-me-route.ts +72 -0
  22. package/dist/templates/api/relations-route.ts +46 -0
  23. package/dist/templates/app/content/[collection]/[id]/page.tsx +35 -0
  24. package/dist/templates/app/content/[collection]/page.tsx +65 -0
  25. package/dist/templates/app/content/layout.tsx +64 -0
  26. package/dist/templates/app/content/page.tsx +66 -0
  27. package/dist/templates/app/design-tokens.css +183 -0
  28. package/dist/templates/app/globals.css +58 -0
  29. package/dist/templates/app/layout.tsx +49 -0
  30. package/dist/templates/app/page.tsx +23 -0
  31. package/dist/templates/components/ColorSchemeToggle.tsx +35 -0
  32. package/dist/templates/lib/common-utils.ts +156 -0
  33. package/dist/templates/lib/hooks/index.ts +98 -0
  34. package/dist/templates/lib/services/index.ts +31 -0
  35. package/dist/templates/lib/theme.ts +241 -0
  36. package/dist/templates/lib/types/index.ts +10 -0
  37. package/dist/templates/lib/utils-index.ts +32 -0
  38. package/dist/templates/lib/utils.ts +14 -0
  39. package/dist/templates/lib/vform/index.ts +24 -0
  40. package/dist/templates/middleware/middleware.ts +29 -0
  41. package/dist/templates/supabase/client.ts +25 -0
  42. package/dist/templates/supabase/middleware.ts +66 -0
  43. package/dist/templates/supabase/server.ts +45 -0
  44. package/package.json +61 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Files API Route
3
+ *
4
+ * Proxies file operations to the DaaS backend.
5
+ * Required for file upload components.
6
+ *
7
+ * @buildpad/origin: api-routes/files
8
+ * @buildpad/version: 1.0.0
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server';
12
+ import { getAuthHeaders, getDaasUrl } from '@/lib/api/auth-headers';
13
+
14
+ /**
15
+ * GET /api/files
16
+ * List files with optional filtering
17
+ */
18
+ export async function GET(request: NextRequest) {
19
+ try {
20
+ const headers = await getAuthHeaders();
21
+ const daasUrl = getDaasUrl();
22
+
23
+ // Forward query parameters
24
+ const searchParams = request.nextUrl.searchParams.toString();
25
+ const url = `${daasUrl}/api/files${searchParams ? `?${searchParams}` : ''}`;
26
+
27
+ const response = await fetch(url, {
28
+ headers,
29
+ cache: 'no-store',
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
34
+ return NextResponse.json(error, { status: response.status });
35
+ }
36
+
37
+ const data = await response.json();
38
+ return NextResponse.json(data);
39
+ } catch (error) {
40
+ console.error('Files GET error:', error);
41
+ return NextResponse.json(
42
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
43
+ { status: 500 }
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * POST /api/files
50
+ * Upload a file
51
+ */
52
+ export async function POST(request: NextRequest) {
53
+ try {
54
+ const headers = await getAuthHeaders();
55
+ const daasUrl = getDaasUrl();
56
+
57
+ // Get the form data from the request
58
+ const formData = await request.formData();
59
+
60
+ // Remove Content-Type header to let fetch set it with boundary for multipart
61
+ const { 'Content-Type': _, ...restHeaders } = headers as Record<string, string>;
62
+
63
+ const response = await fetch(`${daasUrl}/api/files`, {
64
+ method: 'POST',
65
+ headers: restHeaders,
66
+ body: formData,
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Upload failed' }] }));
71
+ return NextResponse.json(error, { status: response.status });
72
+ }
73
+
74
+ const data = await response.json();
75
+ return NextResponse.json(data);
76
+ } catch (error) {
77
+ console.error('Files POST error:', error);
78
+ return NextResponse.json(
79
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
80
+ { status: 500 }
81
+ );
82
+ }
83
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Items Single Item API Route
3
+ *
4
+ * Proxies single item CRUD operations to the DaaS backend.
5
+ *
6
+ * @buildpad/origin: api-routes/items-id
7
+ * @buildpad/version: 1.0.0
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import { getAuthHeaders, getDaasUrl } from '@/lib/api/auth-headers';
12
+
13
+ type RouteParams = { params: Promise<{ collection: string; id: string }> };
14
+
15
+ /**
16
+ * GET /api/items/[collection]/[id]
17
+ * Get a single item by ID
18
+ */
19
+ export async function GET(
20
+ request: NextRequest,
21
+ { params }: RouteParams
22
+ ) {
23
+ try {
24
+ const { collection, id } = await params;
25
+ const headers = await getAuthHeaders();
26
+ const daasUrl = getDaasUrl();
27
+
28
+ // Forward query parameters (e.g., fields)
29
+ const searchParams = request.nextUrl.searchParams.toString();
30
+ const url = `${daasUrl}/api/items/${collection}/${id}${searchParams ? `?${searchParams}` : ''}`;
31
+
32
+ const response = await fetch(url, {
33
+ headers,
34
+ cache: 'no-store',
35
+ });
36
+
37
+ if (!response.ok) {
38
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
39
+ return NextResponse.json(error, { status: response.status });
40
+ }
41
+
42
+ const data = await response.json();
43
+ return NextResponse.json(data);
44
+ } catch (error) {
45
+ console.error('Items GET by ID error:', error);
46
+ return NextResponse.json(
47
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
48
+ { status: 500 }
49
+ );
50
+ }
51
+ }
52
+
53
+ /**
54
+ * PATCH /api/items/[collection]/[id]
55
+ * Update an existing item
56
+ */
57
+ export async function PATCH(
58
+ request: NextRequest,
59
+ { params }: RouteParams
60
+ ) {
61
+ try {
62
+ const { collection, id } = await params;
63
+ const headers = await getAuthHeaders();
64
+ const daasUrl = getDaasUrl();
65
+ const body = await request.json();
66
+
67
+ const response = await fetch(`${daasUrl}/api/items/${collection}/${id}`, {
68
+ method: 'PATCH',
69
+ headers,
70
+ body: JSON.stringify(body),
71
+ });
72
+
73
+ if (!response.ok) {
74
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
75
+ return NextResponse.json(error, { status: response.status });
76
+ }
77
+
78
+ const data = await response.json();
79
+ return NextResponse.json(data);
80
+ } catch (error) {
81
+ console.error('Items PATCH error:', error);
82
+ return NextResponse.json(
83
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
84
+ { status: 500 }
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * DELETE /api/items/[collection]/[id]
91
+ * Delete an item
92
+ */
93
+ export async function DELETE(
94
+ request: NextRequest,
95
+ { params }: RouteParams
96
+ ) {
97
+ try {
98
+ const { collection, id } = await params;
99
+ const headers = await getAuthHeaders();
100
+ const daasUrl = getDaasUrl();
101
+
102
+ const response = await fetch(`${daasUrl}/api/items/${collection}/${id}`, {
103
+ method: 'DELETE',
104
+ headers,
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
109
+ return NextResponse.json(error, { status: response.status });
110
+ }
111
+
112
+ return NextResponse.json({ data: null });
113
+ } catch (error) {
114
+ console.error('Items DELETE error:', error);
115
+ return NextResponse.json(
116
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
117
+ { status: 500 }
118
+ );
119
+ }
120
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Items Collection API Route
3
+ *
4
+ * Proxies CRUD operations for collection items to the DaaS backend.
5
+ * Supports DaaS-compatible query parameters.
6
+ *
7
+ * @buildpad/origin: api-routes/items
8
+ * @buildpad/version: 1.0.0
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server';
12
+ import { getAuthHeaders, getDaasUrl } from '@/lib/api/auth-headers';
13
+
14
+ type RouteParams = { params: Promise<{ collection: string }> };
15
+
16
+ /**
17
+ * GET /api/items/[collection]
18
+ * List items with optional filtering, sorting, and pagination
19
+ */
20
+ export async function GET(
21
+ request: NextRequest,
22
+ { params }: RouteParams
23
+ ) {
24
+ try {
25
+ const { collection } = await params;
26
+ const headers = await getAuthHeaders();
27
+ const daasUrl = getDaasUrl();
28
+
29
+ // Forward all query parameters
30
+ const searchParams = request.nextUrl.searchParams.toString();
31
+ const url = `${daasUrl}/api/items/${collection}${searchParams ? `?${searchParams}` : ''}`;
32
+
33
+ const response = await fetch(url, {
34
+ headers,
35
+ cache: 'no-store',
36
+ });
37
+
38
+ if (!response.ok) {
39
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
40
+ return NextResponse.json(error, { status: response.status });
41
+ }
42
+
43
+ const data = await response.json();
44
+ return NextResponse.json(data);
45
+ } catch (error) {
46
+ console.error('Items GET error:', error);
47
+ return NextResponse.json(
48
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
53
+
54
+ /**
55
+ * POST /api/items/[collection]
56
+ * Create a new item
57
+ */
58
+ export async function POST(
59
+ request: NextRequest,
60
+ { params }: RouteParams
61
+ ) {
62
+ try {
63
+ const { collection } = await params;
64
+ const headers = await getAuthHeaders();
65
+ const daasUrl = getDaasUrl();
66
+ const body = await request.json();
67
+
68
+ const response = await fetch(`${daasUrl}/api/items/${collection}`, {
69
+ method: 'POST',
70
+ headers,
71
+ body: JSON.stringify(body),
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
76
+ return NextResponse.json(error, { status: response.status });
77
+ }
78
+
79
+ const data = await response.json();
80
+ return NextResponse.json(data);
81
+ } catch (error) {
82
+ console.error('Items POST error:', error);
83
+ return NextResponse.json(
84
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
85
+ { status: 500 }
86
+ );
87
+ }
88
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Login Page Template
3
+ *
4
+ * Server-side proxy login page that uses the /api/auth/login proxy route
5
+ * instead of calling Supabase directly from the browser.
6
+ * This avoids CORS issues in the two-tier architecture.
7
+ *
8
+ * Pattern: Browser → /api/auth/login (same origin) → Supabase Auth (server-side)
9
+ *
10
+ * @buildpad/origin: pages/login
11
+ * @buildpad/version: 1.0.0
12
+ */
13
+
14
+ 'use client';
15
+
16
+ import { useState } from 'react';
17
+ import {
18
+ Paper,
19
+ TextInput,
20
+ PasswordInput,
21
+ Button,
22
+ Title,
23
+ Text,
24
+ Container,
25
+ Stack,
26
+ Box,
27
+ Group,
28
+ Anchor,
29
+ } from '@mantine/core';
30
+ import { useForm } from '@mantine/form';
31
+ import { notifications } from '@mantine/notifications';
32
+ import { useRouter } from 'next/navigation';
33
+
34
+ export default function LoginPage() {
35
+ const router = useRouter();
36
+ const [loading, setLoading] = useState(false);
37
+
38
+ const form = useForm({
39
+ initialValues: {
40
+ email: '',
41
+ password: '',
42
+ },
43
+ validate: {
44
+ email: (value) => (!value ? 'Email is required' : /^\S+@\S+$/.test(value) ? null : 'Invalid email'),
45
+ password: (value) => (!value ? 'Password is required' : null),
46
+ },
47
+ });
48
+
49
+ const handleLogin = async (values: { email: string; password: string }) => {
50
+ setLoading(true);
51
+
52
+ try {
53
+ // Use the proxy route — NOT the Supabase client directly
54
+ // This avoids CORS issues because the request stays same-origin
55
+ const response = await fetch('/api/auth/login', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify(values),
59
+ credentials: 'include', // Include cookies
60
+ });
61
+
62
+ const data = await response.json();
63
+
64
+ if (!response.ok) {
65
+ throw new Error(data.errors?.[0]?.message || 'Login failed');
66
+ }
67
+
68
+ notifications.show({
69
+ title: 'Success',
70
+ message: 'Logged in successfully',
71
+ color: 'green',
72
+ });
73
+
74
+ router.push('/');
75
+ router.refresh();
76
+ } catch (error) {
77
+ console.error('Login error:', error);
78
+ notifications.show({
79
+ title: 'Error',
80
+ message: error instanceof Error ? error.message : 'Failed to login',
81
+ color: 'red',
82
+ });
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ return (
89
+ <Box
90
+ style={{
91
+ position: 'fixed',
92
+ top: 0,
93
+ left: 0,
94
+ right: 0,
95
+ bottom: 0,
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ background: 'linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%)',
100
+ }}
101
+ >
102
+ <Container size={420}>
103
+ <Title ta="center" mb="md">
104
+ Welcome back
105
+ </Title>
106
+ <Text c="dimmed" size="sm" ta="center" mb="xl">
107
+ Sign in to your account
108
+ </Text>
109
+
110
+ <Paper withBorder shadow="md" p={30} radius="md">
111
+ <form onSubmit={form.onSubmit(handleLogin)}>
112
+ <Stack>
113
+ <TextInput
114
+ label="Email"
115
+ placeholder="you@example.com"
116
+ required
117
+ {...form.getInputProps('email')}
118
+ />
119
+
120
+ <PasswordInput
121
+ label="Password"
122
+ placeholder="Your password"
123
+ required
124
+ {...form.getInputProps('password')}
125
+ />
126
+
127
+ <Group justify="flex-end">
128
+ <Anchor component="button" type="button" c="dimmed" size="xs">
129
+ Forgot password?
130
+ </Anchor>
131
+ </Group>
132
+
133
+ <Button type="submit" fullWidth loading={loading}>
134
+ Sign in
135
+ </Button>
136
+ </Stack>
137
+ </form>
138
+ </Paper>
139
+ </Container>
140
+ </Box>
141
+ );
142
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Permissions (Me) API Route
3
+ *
4
+ * Proxies the current user's permission requests to the DaaS backend.
5
+ * Required for CollectionList permission-aware column filtering via
6
+ * PermissionsService.getReadableFields().
7
+ *
8
+ * @buildpad/origin: api-routes/permissions-me
9
+ * @buildpad/version: 1.0.0
10
+ */
11
+
12
+ import { getAuthHeaders, getDaasUrl } from "@/lib/api/auth-headers";
13
+ import { NextRequest, NextResponse } from "next/server";
14
+
15
+ /**
16
+ * GET /api/permissions/me
17
+ * Returns the current user's field-level permissions for all collections.
18
+ *
19
+ * Query Parameters:
20
+ * - collection=name Filter to specific collection(s), can be repeated
21
+ *
22
+ * Response format (DaaS / DaaS):
23
+ * {
24
+ * "data": {
25
+ * "<collection>": {
26
+ * "read": { "fields": ["id","title",...], "permissions": null, "validation": null, "presets": null },
27
+ * "create": { ... },
28
+ * "update": { ... },
29
+ * "delete": { ... }
30
+ * }
31
+ * }
32
+ * }
33
+ */
34
+ export async function GET(request: NextRequest) {
35
+ try {
36
+ const headers = await getAuthHeaders();
37
+ const daasUrl = getDaasUrl();
38
+
39
+ const searchParams = request.nextUrl.searchParams.toString();
40
+ const url = `${daasUrl}/api/permissions/me${
41
+ searchParams ? `?${searchParams}` : ""
42
+ }`;
43
+
44
+ const response = await fetch(url, {
45
+ headers,
46
+ cache: "no-store",
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const error = await response.json().catch(() => ({
51
+ errors: [{ message: "Request failed" }],
52
+ }));
53
+ return NextResponse.json(error, { status: response.status });
54
+ }
55
+
56
+ const data = await response.json();
57
+ return NextResponse.json(data);
58
+ } catch (error) {
59
+ console.error("Permissions API error:", error);
60
+ return NextResponse.json(
61
+ {
62
+ errors: [
63
+ {
64
+ message:
65
+ error instanceof Error ? error.message : "Internal server error",
66
+ },
67
+ ],
68
+ },
69
+ { status: 500 },
70
+ );
71
+ }
72
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Relations API Route
3
+ *
4
+ * Proxies relation schema requests to the DaaS backend.
5
+ * Required for M2O, M2M, O2M, and M2A relation components.
6
+ *
7
+ * @buildpad/origin: api-routes/relations
8
+ * @buildpad/version: 1.0.0
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server';
12
+ import { getAuthHeaders, getDaasUrl } from '@/lib/api/auth-headers';
13
+
14
+ /**
15
+ * GET /api/relations
16
+ * Get all relation definitions
17
+ */
18
+ export async function GET(request: NextRequest) {
19
+ try {
20
+ const headers = await getAuthHeaders();
21
+ const daasUrl = getDaasUrl();
22
+
23
+ // Forward query parameters (e.g., filter by collection)
24
+ const searchParams = request.nextUrl.searchParams.toString();
25
+ const url = `${daasUrl}/api/relations${searchParams ? `?${searchParams}` : ''}`;
26
+
27
+ const response = await fetch(url, {
28
+ headers,
29
+ cache: 'no-store',
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
34
+ return NextResponse.json(error, { status: response.status });
35
+ }
36
+
37
+ const data = await response.json();
38
+ return NextResponse.json(data);
39
+ } catch (error) {
40
+ console.error('Relations API error:', error);
41
+ return NextResponse.json(
42
+ { errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
43
+ { status: 500 }
44
+ );
45
+ }
46
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Item Edit/Create Page
3
+ *
4
+ * Renders a dynamic form for creating or editing a single item in a collection.
5
+ * Uses CollectionForm (which wraps VForm with all 40+ field interfaces).
6
+ *
7
+ * Generated by @buildpad/cli
8
+ */
9
+
10
+ "use client";
11
+
12
+ import React, { use } from 'react';
13
+ import { useRouter } from 'next/navigation';
14
+ import { CollectionForm } from '@/components/ui/collection-form';
15
+
16
+ export default function ItemPage({
17
+ params,
18
+ }: {
19
+ params: Promise<{ collection: string; id: string }>;
20
+ }) {
21
+ const { collection, id } = use(params);
22
+ const router = useRouter();
23
+
24
+ const isNew = id === '+' || id === 'new';
25
+
26
+ return (
27
+ <CollectionForm
28
+ collection={collection}
29
+ id={isNew ? undefined : id}
30
+ mode={isNew ? 'create' : 'edit'}
31
+ onSuccess={() => router.push(`/content/${collection}`)}
32
+ onCancel={() => router.back()}
33
+ />
34
+ );
35
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Collection List Page
3
+ *
4
+ * Displays a table of items for the given collection with search,
5
+ * filtering, pagination, selection, and bulk actions.
6
+ *
7
+ * Generated by @buildpad/cli
8
+ */
9
+
10
+ "use client";
11
+
12
+ import React, { use, useEffect } from 'react';
13
+ import { useRouter } from 'next/navigation';
14
+ import { Button, Group } from '@mantine/core';
15
+ import { IconPlus, IconTrash } from '@tabler/icons-react';
16
+ import { CollectionList } from '@/components/ui/collection-list';
17
+ import { useLocalStorage } from '@/lib/buildpad/hooks';
18
+ import type { BulkAction } from '@/components/ui/collection-list';
19
+
20
+ export default function CollectionPage({
21
+ params,
22
+ }: {
23
+ params: Promise<{ collection: string }>;
24
+ }) {
25
+ const { collection } = use(params);
26
+ const router = useRouter();
27
+ const { setValue: setLastAccessed } = useLocalStorage<string>('last-accessed-collection');
28
+
29
+ // Track last accessed collection
30
+ useEffect(() => {
31
+ setLastAccessed(collection);
32
+ }, [collection, setLastAccessed]);
33
+
34
+ const bulkActions: BulkAction[] = [
35
+ {
36
+ label: 'Delete',
37
+ icon: <IconTrash size={16} />,
38
+ color: 'red',
39
+ action: async (selectedIds) => {
40
+ if (!confirm(`Delete ${selectedIds.length} item(s)?`)) return;
41
+ try {
42
+ await fetch(`/api/items/${collection}`, {
43
+ method: 'DELETE',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(selectedIds),
46
+ });
47
+ // CollectionList will auto-refresh
48
+ window.location.reload();
49
+ } catch (error) {
50
+ console.error('Batch delete failed:', error);
51
+ }
52
+ },
53
+ },
54
+ ];
55
+
56
+ return (
57
+ <CollectionList
58
+ collection={collection}
59
+ enableSearch
60
+ enableSelection
61
+ bulkActions={bulkActions}
62
+ onItemClick={(item) => router.push(`/content/${collection}/${item.id}`)}
63
+ />
64
+ );
65
+ }