@dyrected/admin 2.4.0 → 2.4.2

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 (170) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/admin.css +2 -0
  3. package/dist/components/auth/auth-gate.d.ts +13 -0
  4. package/dist/components/error-boundary.d.ts +16 -0
  5. package/dist/components/forms/field-renderer.d.ts +22 -0
  6. package/dist/components/forms/fields/block-builder.d.ts +9 -0
  7. package/dist/components/forms/fields/date-picker.d.ts +8 -0
  8. package/dist/components/forms/fields/json-editor.d.ts +8 -0
  9. package/dist/components/forms/fields/media-picker.d.ts +12 -0
  10. package/dist/components/forms/fields/multi-select.d.ts +19 -0
  11. package/dist/components/forms/fields/radio-field.d.ts +8 -0
  12. package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
  13. package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
  14. package/dist/components/forms/fields/select-field.d.ts +8 -0
  15. package/dist/components/forms/fields/switch-field.d.ts +6 -0
  16. package/dist/components/forms/fields/text-area-field.d.ts +8 -0
  17. package/dist/components/forms/fields/text-field.d.ts +8 -0
  18. package/dist/components/forms/form-engine.d.ts +14 -0
  19. package/dist/components/forms/form-field-renderer.d.ts +20 -0
  20. package/dist/components/forms/utils.d.ts +11 -0
  21. package/dist/components/layout/admin-shell.d.ts +5 -0
  22. package/dist/components/layout/branding-provider.d.ts +4 -0
  23. package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
  24. package/dist/components/media/focal-point-picker.d.ts +12 -0
  25. package/dist/components/media/media-card.d.ts +8 -0
  26. package/dist/components/media/media-grid.d.ts +8 -0
  27. package/dist/components/media/media-library-dialog.d.ts +11 -0
  28. package/dist/components/ui/aspect-ratio.d.ts +3 -0
  29. package/dist/components/ui/badge.d.ts +9 -0
  30. package/dist/components/ui/button.d.ts +11 -0
  31. package/dist/components/ui/calendar.d.ts +8 -0
  32. package/dist/components/ui/card.d.ts +8 -0
  33. package/dist/components/ui/checkbox.d.ts +4 -0
  34. package/dist/components/ui/command.d.ts +80 -0
  35. package/dist/components/ui/data-table.d.ts +14 -0
  36. package/dist/components/ui/dialog.d.ts +19 -0
  37. package/dist/components/ui/dropdown-menu.d.ts +27 -0
  38. package/dist/components/ui/form.d.ts +23 -0
  39. package/dist/components/ui/input.d.ts +3 -0
  40. package/dist/components/ui/label.d.ts +5 -0
  41. package/dist/components/ui/page-header.d.ts +10 -0
  42. package/dist/components/ui/pagination.d.ts +11 -0
  43. package/dist/components/ui/popover.d.ts +6 -0
  44. package/dist/components/ui/progress.d.ts +4 -0
  45. package/dist/components/ui/radio-group.d.ts +5 -0
  46. package/dist/components/ui/render-cell.d.ts +8 -0
  47. package/dist/components/ui/scroll-area.d.ts +5 -0
  48. package/dist/components/ui/select.d.ts +13 -0
  49. package/dist/components/ui/separator.d.ts +4 -0
  50. package/dist/components/ui/sheet.d.ts +25 -0
  51. package/dist/components/ui/sidebar.d.ts +65 -0
  52. package/dist/components/ui/skeleton.d.ts +2 -0
  53. package/dist/components/ui/sonner.d.ts +4 -0
  54. package/dist/components/ui/switch.d.ts +4 -0
  55. package/dist/components/ui/table.d.ts +10 -0
  56. package/dist/components/ui/tabs.d.ts +7 -0
  57. package/dist/components/ui/textarea.d.ts +3 -0
  58. package/dist/components/ui/toggle.d.ts +12 -0
  59. package/dist/components/ui/tooltip.d.ts +7 -0
  60. package/dist/hooks/use-mobile.d.ts +1 -0
  61. package/dist/hooks/use-preferences.d.ts +6 -0
  62. package/dist/index.d.ts +38 -0
  63. package/dist/index.mjs +69091 -0
  64. package/dist/lib/utils.d.ts +3 -0
  65. package/dist/main.d.ts +0 -0
  66. package/dist/pages/auth/first-user-page.d.ts +4 -0
  67. package/dist/pages/auth/login-page.d.ts +4 -0
  68. package/dist/pages/collections/edit-page.d.ts +1 -0
  69. package/dist/pages/collections/list-page.d.ts +5 -0
  70. package/dist/pages/dashboard/dashboard.d.ts +1 -0
  71. package/dist/pages/globals/editor-page.d.ts +1 -0
  72. package/dist/pages/media/media-page.d.ts +4 -0
  73. package/dist/pages/setup/setup-prompt.d.ts +6 -0
  74. package/dist/providers/dyrected-provider.d.ts +29 -0
  75. package/dist/providers/query-provider.d.ts +3 -0
  76. package/package.json +6 -3
  77. package/CHANGELOG.md +0 -153
  78. package/components.json +0 -17
  79. package/eslint.config.js +0 -22
  80. package/index.html +0 -13
  81. package/postcss.config.js +0 -6
  82. package/scripts/prefix-tailwind-precision.py +0 -98
  83. package/scripts/prefix-tailwind.py +0 -67
  84. package/src/App.css +0 -184
  85. package/src/App.tsx +0 -25
  86. package/src/assets/dyrected.svg +0 -155
  87. package/src/assets/hero.png +0 -0
  88. package/src/assets/react.svg +0 -1
  89. package/src/assets/vite.svg +0 -1
  90. package/src/components/auth/auth-gate.tsx +0 -64
  91. package/src/components/error-boundary.tsx +0 -45
  92. package/src/components/forms/field-renderer.tsx +0 -111
  93. package/src/components/forms/fields/block-builder.tsx +0 -213
  94. package/src/components/forms/fields/date-picker.tsx +0 -60
  95. package/src/components/forms/fields/json-editor.tsx +0 -62
  96. package/src/components/forms/fields/media-picker.tsx +0 -286
  97. package/src/components/forms/fields/multi-select.tsx +0 -145
  98. package/src/components/forms/fields/radio-field.tsx +0 -51
  99. package/src/components/forms/fields/relationship-picker.tsx +0 -143
  100. package/src/components/forms/fields/rich-text-editor.tsx +0 -224
  101. package/src/components/forms/fields/select-field.tsx +0 -35
  102. package/src/components/forms/fields/switch-field.tsx +0 -16
  103. package/src/components/forms/fields/text-area-field.tsx +0 -15
  104. package/src/components/forms/fields/text-field.tsx +0 -24
  105. package/src/components/forms/form-engine.tsx +0 -87
  106. package/src/components/forms/form-field-renderer.tsx +0 -269
  107. package/src/components/forms/utils.ts +0 -97
  108. package/src/components/layout/admin-shell.tsx +0 -479
  109. package/src/components/layout/branding-provider.tsx +0 -112
  110. package/src/components/live-preview/LivePreviewPane.tsx +0 -128
  111. package/src/components/media/focal-point-picker.tsx +0 -66
  112. package/src/components/media/media-card.tsx +0 -44
  113. package/src/components/media/media-grid.tsx +0 -32
  114. package/src/components/media/media-library-dialog.tsx +0 -465
  115. package/src/components/ui/aspect-ratio.tsx +0 -7
  116. package/src/components/ui/badge.tsx +0 -36
  117. package/src/components/ui/button.tsx +0 -56
  118. package/src/components/ui/calendar.tsx +0 -214
  119. package/src/components/ui/card.tsx +0 -79
  120. package/src/components/ui/checkbox.tsx +0 -28
  121. package/src/components/ui/command.tsx +0 -151
  122. package/src/components/ui/data-table.tsx +0 -219
  123. package/src/components/ui/dialog.tsx +0 -122
  124. package/src/components/ui/dropdown-menu.tsx +0 -200
  125. package/src/components/ui/form.tsx +0 -178
  126. package/src/components/ui/input.tsx +0 -24
  127. package/src/components/ui/label.tsx +0 -24
  128. package/src/components/ui/page-header.tsx +0 -30
  129. package/src/components/ui/pagination.tsx +0 -57
  130. package/src/components/ui/popover.tsx +0 -29
  131. package/src/components/ui/progress.tsx +0 -26
  132. package/src/components/ui/radio-group.tsx +0 -42
  133. package/src/components/ui/render-cell.tsx +0 -110
  134. package/src/components/ui/scroll-area.tsx +0 -46
  135. package/src/components/ui/select.tsx +0 -160
  136. package/src/components/ui/separator.tsx +0 -29
  137. package/src/components/ui/sheet.tsx +0 -140
  138. package/src/components/ui/sidebar.tsx +0 -771
  139. package/src/components/ui/skeleton.tsx +0 -15
  140. package/src/components/ui/sonner.tsx +0 -27
  141. package/src/components/ui/switch.tsx +0 -27
  142. package/src/components/ui/table.tsx +0 -117
  143. package/src/components/ui/tabs.tsx +0 -53
  144. package/src/components/ui/textarea.tsx +0 -22
  145. package/src/components/ui/toggle.tsx +0 -43
  146. package/src/components/ui/tooltip.tsx +0 -28
  147. package/src/hooks/use-mobile.tsx +0 -19
  148. package/src/hooks/use-preferences.ts +0 -56
  149. package/src/index.css +0 -111
  150. package/src/index.tsx +0 -198
  151. package/src/lib/utils.ts +0 -36
  152. package/src/main.tsx +0 -10
  153. package/src/pages/auth/first-user-page.tsx +0 -115
  154. package/src/pages/auth/login-page.tsx +0 -91
  155. package/src/pages/collections/edit-page.tsx +0 -280
  156. package/src/pages/collections/list-page.tsx +0 -343
  157. package/src/pages/dashboard/dashboard.tsx +0 -150
  158. package/src/pages/globals/editor-page.tsx +0 -122
  159. package/src/pages/media/media-page.tsx +0 -564
  160. package/src/pages/setup/setup-prompt.tsx +0 -181
  161. package/src/providers/dyrected-provider.tsx +0 -122
  162. package/src/providers/query-provider.tsx +0 -19
  163. package/src/types/jexl.d.ts +0 -11
  164. package/tailwind.config.ts +0 -103
  165. package/tsconfig.app.json +0 -28
  166. package/tsconfig.json +0 -12
  167. package/tsconfig.node.json +0 -25
  168. package/vite.config.ts +0 -39
  169. /package/{public → dist}/favicon.svg +0 -0
  170. /package/{public → dist}/icons.svg +0 -0
@@ -1,280 +0,0 @@
1
- import { useState, useEffect } from "react"
2
- import { toast } from "sonner"
3
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
4
- import { useDyrected } from "../../providers/dyrected-provider"
5
- import { FormEngine } from "../../components/forms/form-engine"
6
- import { useNavigate, useParams } from "react-router-dom"
7
- import { ChevronLeft } from "lucide-react"
8
- import { Button } from "../../components/ui/button"
9
- import { Badge } from "../../components/ui/badge"
10
- import { cn } from "../../lib/utils"
11
- import { Archive, Eye, EyeOff, Save } from "lucide-react"
12
- import { LivePreviewPane } from "../../components/live-preview/LivePreviewPane"
13
- import jexl from 'jexl'
14
-
15
- export function EditEntryPage() {
16
- const { slug, id } = useParams()
17
- const { client } = useDyrected()
18
- const navigate = useNavigate()
19
- const queryClient = useQueryClient()
20
- const [showPreview, setShowPreview] = useState(false)
21
- const [isDirty, setIsDirty] = useState(false)
22
- const [previewData, setPreviewData] = useState<any>(null)
23
- const isEdit = !!id
24
-
25
- useEffect(() => {
26
- const handleBeforeUnload = (e: BeforeUnloadEvent) => {
27
- if (isDirty) {
28
- e.preventDefault()
29
- e.returnValue = ""
30
- }
31
- }
32
- window.addEventListener("beforeunload", handleBeforeUnload)
33
- return () => window.removeEventListener("beforeunload", handleBeforeUnload)
34
- }, [isDirty])
35
-
36
- // Cmd+S to save
37
- useEffect(() => {
38
- const handleSave = (e: KeyboardEvent) => {
39
- if ((e.metaKey || e.ctrlKey) && e.key === "s") {
40
- e.preventDefault()
41
- document.getElementById('dyrected-form-submit')?.click()
42
- }
43
- }
44
- window.addEventListener("keydown", handleSave)
45
- return () => window.removeEventListener("keydown", handleSave)
46
- }, [])
47
-
48
- // Fetch schema
49
- const { data: schemas } = useQuery({
50
- queryKey: ["schemas"],
51
- queryFn: () => client!.getSchemas(),
52
- enabled: !!client,
53
- })
54
-
55
- const schema = schemas?.collections.find((c: any) => c.slug === slug)
56
-
57
- // Effect to default preview if available
58
- useEffect(() => {
59
- if (schema?.admin?.previewUrl) {
60
- setShowPreview(true)
61
- }
62
- }, [schema])
63
-
64
- // Fetch entry data if in edit mode
65
- const { data: entry, isLoading: isEntryLoading } = useQuery({
66
- queryKey: ["entry", slug, id],
67
- queryFn: () => client!.collection(slug!).findOne(id!),
68
- enabled: !!client && isEdit,
69
- })
70
-
71
- useEffect(() => {
72
- if (entry) {
73
- setPreviewData(entry)
74
- }
75
- }, [entry])
76
-
77
- const saveMutation = useMutation({
78
- mutationFn: (data: any) => {
79
- if (isEdit) {
80
- return client!.collection(slug!).update(id!, data)
81
- } else {
82
- return client!.collection(slug!).create(data)
83
- }
84
- },
85
- onSuccess: (data: any) => {
86
- setIsDirty(false)
87
- queryClient.invalidateQueries({ queryKey: ["collection", slug] })
88
- if (isEdit) {
89
- queryClient.invalidateQueries({ queryKey: ["entry", slug, id] })
90
- }
91
-
92
- toast.success(isEdit ? "Entry updated successfully" : "Entry created successfully", {
93
- description: `${schema.label || schema.slug} has been saved.`
94
- })
95
-
96
- if (!isEdit && data?.id) {
97
- navigate(`/collections/${slug}/edit/${data.id}`, { replace: true })
98
- }
99
- },
100
- onError: (error: any) => {
101
- toast.error("Failed to save entry", {
102
- description: error.message || "An unexpected error occurred."
103
- })
104
- }
105
- })
106
-
107
- if (!schema) return <div>Collection not found</div>
108
- if (isEdit && isEntryLoading) return <div>Loading entry...</div>
109
-
110
- const hasStatus = schema?.fields.some((f: any) => f.name === "status")
111
- const currentStatus = entry?.status || "draft"
112
-
113
- let previewUrl = typeof schema.admin?.previewUrl === 'function'
114
- ? schema.admin.previewUrl(previewData || entry, { locale: 'en' })
115
- : schema.admin?.previewUrl
116
-
117
- if (typeof previewUrl === 'string' && previewUrl.includes('{{')) {
118
- previewUrl = previewUrl.replace(/{{(.*?)}}/g, (_, key) => entry?.[key.trim()] || "")
119
- } else if (typeof previewUrl === 'string' && (previewData || entry)) {
120
- try {
121
- // Provide current window origin to Jexl context so users can use it in expressions
122
- const context = { ...(previewData || entry), siteUrl: window.location.origin };
123
-
124
- if (previewUrl.includes('+') || previewUrl.includes('?') || previewUrl.includes('==') || previewUrl.includes('siteUrl')) {
125
- previewUrl = jexl.evalSync(previewUrl, context)
126
- }
127
- } catch (e) {
128
- console.error("[PreviewDebug] Jexl Evaluation Failed:", e)
129
- }
130
- }
131
-
132
- // If the resolved URL is relative, prepend the current origin
133
- if (typeof previewUrl === 'string' && previewUrl.startsWith('/')) {
134
- previewUrl = `${window.location.origin}${previewUrl}`
135
- }
136
-
137
- const canCreate = (schema.access as any)?.create !== false
138
- const canUpdate = (schema.access as any)?.update !== false
139
-
140
- return (
141
- <div className="dy-flex dy-h-[calc(100vh-0px)] dy-overflow-hidden dy--mt-6 dy--mx-4 lg:dy--mt-10 lg:dy--mx-6">
142
- {/* Left Column: Header + Form */}
143
- <div className={cn(
144
- "dy-flex-1 dy-overflow-y-auto dy-px-6 dy-py-6 lg:dy-px-10 lg:dy-py-10 dy-transition-all dy-duration-500",
145
- showPreview ? "dy-max-w-2xl xl:dy-max-w-3xl" : "dy-max-w-5xl dy-mx-auto dy-w-full"
146
- )}>
147
- <div className="dy-space-y-8">
148
- {/* Header */}
149
- <div className="dy-flex dy-items-center dy-justify-between dy-gap-4 dy-border-b dy-border-border/50 dy-pb-6">
150
- <div className="dy-flex dy-items-center dy-gap-4">
151
- <Button
152
- variant="ghost"
153
- size="icon"
154
- className="dy-h-8 dy-w-8 dy-rounded-lg hover:dy-bg-muted dy-shrink-0"
155
- onClick={() => navigate(`/collections/${slug}`)}
156
- >
157
- <ChevronLeft className="dy-h-4 dy-w-4" />
158
- </Button>
159
- <div>
160
- <div className="dy-flex dy-items-center dy-gap-3">
161
- <h1 className="dy-text-lg dy-font-serif dy-font-bold dy-tracking-tight dy-text-foreground dy-truncate">
162
- {isEdit ? `Edit ${schema.label || schema.slug}` : `New ${schema.label || schema.slug}`}
163
- </h1>
164
- {hasStatus && (
165
- <Badge className={cn(
166
- "dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider",
167
- currentStatus === "published" ? "dy-bg-emerald-100 dy-text-emerald-700 dy-border-emerald-200" : "dy-bg-amber-100 dy-text-amber-700 dy-border-amber-200"
168
- )} variant="outline">
169
- {currentStatus === "published" ? "Live" : "Draft"}
170
- </Badge>
171
- )}
172
- </div>
173
- </div>
174
- </div>
175
-
176
- <div className="dy-flex dy-items-center dy-gap-2">
177
- {previewUrl && (
178
- <Button
179
- variant="ghost"
180
- size="icon"
181
- className={cn(
182
- "dy-h-9 dy-w-9 dy-rounded-lg dy-transition-colors",
183
- showPreview ? "dy-bg-primary/10 dy-text-primary hover:dy-bg-primary/20" : "hover:dy-bg-muted"
184
- )}
185
- onClick={() => setShowPreview(!showPreview)}
186
- title={showPreview ? "Hide Preview" : "Live Preview"}
187
- >
188
- {showPreview ? <EyeOff className="dy-h-4 dy-w-4" /> : <Eye className="dy-h-4 dy-w-4" />}
189
- </Button>
190
- )}
191
- <Button
192
- size="icon"
193
- className="dy-h-9 dy-w-9 dy-rounded-lg dy-shadow-sm"
194
- onClick={() => document.getElementById('dyrected-form-submit')?.click()}
195
- disabled={saveMutation.isPending || (isEdit ? !canUpdate : !canCreate)}
196
- title={isEdit ? "Save Changes (⌘S)" : "Create Entry (⌘S)"}
197
- >
198
- {saveMutation.isPending ? (
199
- <div className="dy-h-4 dy-w-4 dy-animate-spin dy-border-2 dy-border-current dy-border-t-transparent dy-rounded-full" />
200
- ) : (
201
- <Save className="dy-h-4 dy-w-4" />
202
- )}
203
- </Button>
204
- </div>
205
- </div>
206
-
207
- {/* Form */}
208
- <div className="dy-animate-in dy-space-y-8 dy-pb-20">
209
- {!canUpdate && isEdit && (
210
- <div className="dy-p-4 dy-rounded-lg dy-bg-amber-50 dy-border dy-border-amber-200 dy-text-amber-800 dy-text-sm dy-flex dy-items-center dy-gap-3">
211
- <Archive className="dy-h-4 dy-w-4" />
212
- You have read-only access to this collection.
213
- </div>
214
- )}
215
- <FormEngine
216
- collection={slug!}
217
- fields={schema.fields}
218
- defaultValues={entry}
219
- onSubmit={(data) => saveMutation.mutate(data)}
220
- onDataChange={(newData) => setPreviewData({ ...entry, ...newData })}
221
- onChange={(dirty) => setIsDirty(dirty)}
222
- isLoading={saveMutation.isPending}
223
- submitLabel={isEdit ? "Save Changes" : "Create Entry"}
224
- readOnly={isEdit ? !canUpdate : !canCreate}
225
- />
226
- <button id="dyrected-form-submit" type="submit" className="dy-hidden" />
227
-
228
- {/* Document Meta */}
229
- <div className="dy-pt-8 dy-border-t dy-border-border/40">
230
- <div className="dy-flex dy-flex-wrap dy-items-center dy-gap-x-8 dy-gap-y-4">
231
- <div className="dy-space-y-1">
232
- <p className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-text-muted-foreground/40 dy-text-nowrap">Document ID</p>
233
- <code className="dy-text-xs dy-font-mono dy-text-muted-foreground/80 dy-select-all">
234
- {isEdit ? id : "Pending..."}
235
- </code>
236
- </div>
237
-
238
- {isEdit && (
239
- <>
240
- <div className="dy-space-y-1">
241
- <p className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-text-muted-foreground/40 dy-text-nowrap">Created At</p>
242
- <p className="dy-text-xs dy-font-medium dy-text-muted-foreground/80">
243
- {entry?.createdAt ? new Date(entry.createdAt).toLocaleString() : 'N/A'}
244
- </p>
245
- </div>
246
- <div className="dy-space-y-1">
247
- <p className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-text-muted-foreground/40 dy-text-nowrap">Last Updated</p>
248
- <p className="dy-text-xs dy-font-medium dy-text-muted-foreground/80">
249
- {entry?.updatedAt ? new Date(entry.updatedAt).toLocaleString() : 'N/A'}
250
- </p>
251
- </div>
252
- </>
253
- )}
254
- </div>
255
- </div>
256
- </div>
257
- </div>
258
- </div>
259
-
260
- {/* Right Column: Preview (starts from top) */}
261
- {previewUrl && (
262
- <div className={cn(
263
- "dy-hidden lg:dy-block dy-border-l dy-border-border/50 dy-bg-muted/5 dy-transition-all dy-duration-500 dy-overflow-hidden",
264
- showPreview ? "dy-flex-1 dy-opacity-100" : "dy-w-0 dy-opacity-0 dy-border-l-0"
265
- )}>
266
- {/* We use negative margins to pull the preview up and out to the shell's padding edges if possible,
267
- but since we're inside a parent with padding, we'll just make it height-full.
268
- */}
269
- <div className="dy-h-full">
270
- <LivePreviewPane
271
- previewUrl={previewUrl}
272
- data={previewData || entry}
273
- mode={schema.admin?.previewMode}
274
- />
275
- </div>
276
- </div>
277
- )}
278
- </div>
279
- )
280
- }
@@ -1,343 +0,0 @@
1
- import * as React from "react"
2
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
3
- import { toast } from "sonner"
4
- import { Link } from "react-router-dom"
5
- import { useDyrected } from "../../providers/dyrected-provider"
6
- import { DataTable } from "../../components/ui/data-table"
7
- import { type ColumnDef } from "@tanstack/react-table"
8
- import { Badge } from "../../components/ui/badge"
9
- import { Button } from "../../components/ui/button"
10
- import { Checkbox } from "../../components/ui/checkbox"
11
- import {
12
- MoreHorizontal,
13
- Plus,
14
- Pencil,
15
- Trash2,
16
- Calendar,
17
- Database,
18
- Image as ImageIcon,
19
- } from "lucide-react"
20
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../components/ui/dropdown-menu"
21
- import { RenderCell } from "../../components/ui/render-cell"
22
- import { PageHeader } from "../../components/ui/page-header"
23
- import { Pagination } from "../../components/ui/pagination"
24
- import { MediaGrid } from "../../components/media/media-grid"
25
-
26
- interface CollectionListPageProps {
27
- slug: string
28
- }
29
-
30
- export function CollectionListPage({ slug }: CollectionListPageProps) {
31
- const { client } = useDyrected()
32
- const queryClient = useQueryClient()
33
- const [page, setPage] = React.useState(1)
34
-
35
- // Reset to page 1 when collection slug changes
36
- React.useEffect(() => { setPage(1) }, [slug])
37
-
38
- // Fetch schema to know fields
39
- const { data: schemas } = useQuery({
40
- queryKey: ["schemas"],
41
- queryFn: () => client!.getSchemas(),
42
- enabled: !!client,
43
- })
44
-
45
- const schema = schemas?.collections.find((c: any) => c.slug === slug)
46
-
47
- // Fetch collection data
48
- const { data: response, isLoading } = useQuery({
49
- queryKey: ["collection", slug, page],
50
- queryFn: () => client!.collection(slug).find({ page, limit: 20, depth: 1 }).exec(),
51
- enabled: !!client,
52
- })
53
-
54
- const totalPages = response?.totalPages ?? 1
55
- const hasNextPage = page < totalPages
56
- const hasPrevPage = page > 1
57
-
58
- const deleteMutation = useMutation({
59
- mutationFn: (id: string) => client!.collection(slug).delete(id),
60
- onSuccess: () => {
61
- queryClient.invalidateQueries({ queryKey: ["collection", slug] })
62
- setRowSelection({})
63
- toast.success("Entry deleted successfully")
64
- },
65
- onError: (error: any) => {
66
- toast.error("Failed to delete entry", {
67
- description: error.message
68
- })
69
- }
70
- })
71
-
72
- const bulkDeleteMutation = useMutation({
73
- mutationFn: async (ids: string[]) => {
74
- for (const id of ids) {
75
- await client!.collection(slug).delete(id)
76
- }
77
- },
78
- onSuccess: () => {
79
- queryClient.invalidateQueries({ queryKey: ["collection", slug] })
80
- setRowSelection({})
81
- toast.success("Selected entries deleted")
82
- },
83
- onError: (error: any) => {
84
- toast.error("Failed to delete entries", {
85
- description: error.message
86
- })
87
- }
88
- })
89
-
90
- function handleDelete(id: string) {
91
- if (window.confirm("Delete this entry? This cannot be undone.")) {
92
- deleteMutation.mutate(id)
93
- }
94
- }
95
-
96
- function handleBulkDelete(ids: string[]) {
97
- if (window.confirm(`Delete ${ids.length} entries? This cannot be undone.`)) {
98
- bulkDeleteMutation.mutate(ids)
99
- }
100
- }
101
-
102
- const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({})
103
-
104
- const columns: ColumnDef<any>[] = React.useMemo(() => {
105
- if (!schema) return []
106
-
107
- const cols: ColumnDef<any>[] = [
108
- {
109
- id: "select",
110
- header: ({ table }) => (
111
- <Checkbox
112
- checked={table.getIsAllPageRowsSelected() ? true : table.getIsSomePageRowsSelected() ? "indeterminate" : false}
113
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
114
- aria-label="Select all"
115
- />
116
- ),
117
- cell: ({ row }) => (
118
- <Checkbox
119
- checked={row.getIsSelected()}
120
- onCheckedChange={(value) => row.toggleSelected(!!value)}
121
- aria-label="Select row"
122
- />
123
- ),
124
- enableSorting: false,
125
- enableHiding: false,
126
- },
127
- {
128
- accessorKey: "id",
129
- header: "ID",
130
- cell: ({ row }) => <span className="dy-font-mono dy-text-xs">{row.getValue("id")}</span>,
131
- },
132
- ]
133
-
134
- const hasStatus = schema.fields.some((f: any) => f.name === "status")
135
-
136
- if (hasStatus) {
137
- cols.push({
138
- accessorKey: "status",
139
- header: "Status",
140
- cell: ({ row }) => {
141
- const status = row.getValue("status")
142
- return (
143
- <Badge variant={status === "published" ? "default" : "secondary"}>
144
- {status === "published" ? "Published" : "Draft"}
145
- </Badge>
146
- )
147
- }
148
- })
149
- }
150
-
151
- // Include all non-hidden fields as columns
152
- const allDisplayFields = schema.fields.filter((f: any) => f.name !== "status" && !f.admin?.hidden)
153
-
154
- allDisplayFields.forEach((field: any) => {
155
- cols.push({
156
- accessorKey: field.name,
157
- header: field.label || field.name,
158
- cell: ({ row }) => (
159
- <RenderCell
160
- value={row.getValue(field.name)}
161
- field={field}
162
- client={client}
163
- schemas={schemas}
164
- />
165
- ),
166
- })
167
- })
168
-
169
- // Add metadata columns
170
- cols.push({
171
- accessorKey: "updatedAt",
172
- header: "Last Updated",
173
- cell: ({ row }) => {
174
- const date = new Date(row.getValue("updatedAt"))
175
- return (
176
- <div className="dy-flex dy-items-center dy-gap-2 dy-text-muted-foreground">
177
- <Calendar className="dy-h-3 dy-w-3" />
178
- <span className="dy-text-xs">{date.toLocaleDateString()}</span>
179
- </div>
180
- )
181
- }
182
- })
183
-
184
- // Actions
185
- cols.push({
186
- id: "actions",
187
- cell: ({ row }) => {
188
- const item = row.original
189
- return (
190
- <DropdownMenu>
191
- <DropdownMenuTrigger asChild>
192
- <Button variant="ghost" className="dy-h-8 dy-w-8 dy-p-0">
193
- <span className="dy-sr-only">Open menu</span>
194
- <MoreHorizontal className="dy-h-4 dy-w-4" />
195
- </Button>
196
- </DropdownMenuTrigger>
197
- <DropdownMenuContent align="end">
198
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
199
- <DropdownMenuItem onClick={() => navigator.clipboard.writeText(item.id)}>
200
- Copy ID
201
- </DropdownMenuItem>
202
- <DropdownMenuSeparator />
203
- <Link to={`/collections/${slug}/edit/${item.id}`}>
204
- <DropdownMenuItem className="dy-flex dy-gap-2">
205
- <Pencil className="dy-h-4 dy-w-4" />
206
- Edit
207
- </DropdownMenuItem>
208
- </Link>
209
- <DropdownMenuItem
210
- className="dy-flex dy-gap-2 dy-text-destructive focus:dy-text-destructive"
211
- onClick={() => handleDelete(item.id)}
212
- disabled={deleteMutation.isPending}
213
- >
214
- <Trash2 className="dy-h-4 dy-w-4" />
215
- {deleteMutation.isPending ? "Deleting..." : "Delete"}
216
- </DropdownMenuItem>
217
- </DropdownMenuContent>
218
- </DropdownMenu>
219
- )
220
- },
221
- })
222
-
223
- return cols
224
- }, [schema, client, deleteMutation.isPending])
225
-
226
- const initialColumnVisibility = React.useMemo(() => {
227
- if (!schema) return {}
228
-
229
- const visibility: Record<string, boolean> = {}
230
- const displayFields = schema.fields.filter((f: any) => f.name !== "status" && !f.admin?.hidden)
231
-
232
- let visibleFieldNames: string[] = []
233
- if (schema.admin?.defaultColumns && Array.isArray(schema.admin.defaultColumns)) {
234
- visibleFieldNames = schema.admin.defaultColumns
235
- } else {
236
- visibleFieldNames = displayFields.slice(0, 3).map((f: any) => f.name)
237
- }
238
-
239
- // Set visibility for all fields
240
- displayFields.forEach((f: any) => {
241
- visibility[f.name] = visibleFieldNames.includes(f.name)
242
- })
243
-
244
- return visibility
245
- }, [schema])
246
-
247
- if (isLoading) {
248
- return (
249
- <div className="dy-flex dy-h-[400px] dy-items-center dy-justify-center">
250
- <div className="dy-animate-spin dy-rounded-full dy-border-4 dy-border-primary dy-border-t-transparent dy-h-8 dy-w-8"></div>
251
- </div>
252
- )
253
- }
254
-
255
- if (!schema) {
256
- return <div>Collection not found: {slug}</div>
257
- }
258
-
259
- if (slug === "media") {
260
- return (
261
- <div className="dy-space-y-8 dy-animate-in">
262
- <PageHeader
263
- title="Media Library"
264
- description="Manage your media assets and uploads."
265
- icon={ImageIcon}
266
- >
267
- <Link to={`/collections/${slug}/new`}>
268
- <Button className="dy-h-8 dy-px-4 dy-text-[11px] dy-rounded-md dy-bg-primary hover:dy-bg-primary/90 dy-shadow-sm dy-transition-all active:dy-scale-95">
269
- <Plus className="dy-mr-1.5 dy-h-3 dy-w-3" />
270
- Upload New
271
- </Button>
272
- </Link>
273
- </PageHeader>
274
-
275
- <MediaGrid
276
- items={response?.docs || []}
277
- baseUrl={client?.getBaseUrl() || ""}
278
- onDelete={handleDelete}
279
- slug={slug}
280
- />
281
-
282
- <Pagination
283
- page={page}
284
- totalPages={totalPages}
285
- hasPrevPage={hasPrevPage}
286
- hasNextPage={hasNextPage}
287
- onPageChange={setPage}
288
- className="dy-mt-8"
289
- />
290
- </div>
291
- )
292
- }
293
-
294
- return (
295
- <div className="dy-space-y-8 dy-animate-in">
296
- <PageHeader
297
- title={schema.labels?.plural || schema.label || schema.slug}
298
- description={`Manage your ${schema.slug} entries and update content.`}
299
- icon={Database}
300
- >
301
- <Link to={`/collections/${slug}/new`}>
302
- <Button className="dy-h-8 dy-px-4 dy-text-[11px] dy-rounded-md dy-bg-primary hover:dy-bg-primary/90 dy-shadow-sm dy-transition-all active:dy-scale-95">
303
- <Plus className="dy-mr-1.5 dy-h-3 dy-w-3" />
304
- Create New
305
- </Button>
306
- </Link>
307
- </PageHeader>
308
-
309
- <div className="dy-overflow-hidden">
310
- <DataTable
311
- key={slug}
312
- columns={columns}
313
- data={response?.docs || []}
314
- searchKey={schema.admin?.useAsTitle || schema.fields.find((f: any) => !f.admin?.hidden)?.name || "id"}
315
- onRowSelectionChange={setRowSelection}
316
- rowSelection={rowSelection}
317
- persistenceKey={slug}
318
- initialColumnVisibility={initialColumnVisibility}
319
- bulkActions={(selectedIds) => (
320
- <Button
321
- variant="destructive"
322
- size="sm"
323
- className="dy-h-8"
324
- onClick={() => handleBulkDelete(selectedIds)}
325
- disabled={bulkDeleteMutation.isPending}
326
- >
327
- <Trash2 className="dy-h-4 dy-w-4 dy-mr-2" />
328
- Delete Selected ({selectedIds.length})
329
- </Button>
330
- )}
331
- />
332
- <Pagination
333
- page={page}
334
- totalPages={totalPages}
335
- total={response?.total}
336
- hasPrevPage={hasPrevPage}
337
- hasNextPage={hasNextPage}
338
- onPageChange={setPage}
339
- />
340
- </div>
341
- </div>
342
- )
343
- }