@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.
- package/dist/App.d.ts +1 -0
- package/dist/admin.css +2 -0
- package/dist/components/auth/auth-gate.d.ts +13 -0
- package/dist/components/error-boundary.d.ts +16 -0
- package/dist/components/forms/field-renderer.d.ts +22 -0
- package/dist/components/forms/fields/block-builder.d.ts +9 -0
- package/dist/components/forms/fields/date-picker.d.ts +8 -0
- package/dist/components/forms/fields/json-editor.d.ts +8 -0
- package/dist/components/forms/fields/media-picker.d.ts +12 -0
- package/dist/components/forms/fields/multi-select.d.ts +19 -0
- package/dist/components/forms/fields/radio-field.d.ts +8 -0
- package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
- package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
- package/dist/components/forms/fields/select-field.d.ts +8 -0
- package/dist/components/forms/fields/switch-field.d.ts +6 -0
- package/dist/components/forms/fields/text-area-field.d.ts +8 -0
- package/dist/components/forms/fields/text-field.d.ts +8 -0
- package/dist/components/forms/form-engine.d.ts +14 -0
- package/dist/components/forms/form-field-renderer.d.ts +20 -0
- package/dist/components/forms/utils.d.ts +11 -0
- package/dist/components/layout/admin-shell.d.ts +5 -0
- package/dist/components/layout/branding-provider.d.ts +4 -0
- package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
- package/dist/components/media/focal-point-picker.d.ts +12 -0
- package/dist/components/media/media-card.d.ts +8 -0
- package/dist/components/media/media-grid.d.ts +8 -0
- package/dist/components/media/media-library-dialog.d.ts +11 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/command.d.ts +80 -0
- package/dist/components/ui/data-table.d.ts +14 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/form.d.ts +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/page-header.d.ts +10 -0
- package/dist/components/ui/pagination.d.ts +11 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/render-cell.d.ts +8 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/select.d.ts +13 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/sheet.d.ts +25 -0
- package/dist/components/ui/sidebar.d.ts +65 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-preferences.d.ts +6 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +69091 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/main.d.ts +0 -0
- package/dist/pages/auth/first-user-page.d.ts +4 -0
- package/dist/pages/auth/login-page.d.ts +4 -0
- package/dist/pages/collections/edit-page.d.ts +1 -0
- package/dist/pages/collections/list-page.d.ts +5 -0
- package/dist/pages/dashboard/dashboard.d.ts +1 -0
- package/dist/pages/globals/editor-page.d.ts +1 -0
- package/dist/pages/media/media-page.d.ts +4 -0
- package/dist/pages/setup/setup-prompt.d.ts +6 -0
- package/dist/providers/dyrected-provider.d.ts +29 -0
- package/dist/providers/query-provider.d.ts +3 -0
- package/package.json +6 -3
- package/CHANGELOG.md +0 -153
- package/components.json +0 -17
- package/eslint.config.js +0 -22
- package/index.html +0 -13
- package/postcss.config.js +0 -6
- package/scripts/prefix-tailwind-precision.py +0 -98
- package/scripts/prefix-tailwind.py +0 -67
- package/src/App.css +0 -184
- package/src/App.tsx +0 -25
- package/src/assets/dyrected.svg +0 -155
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/assets/vite.svg +0 -1
- package/src/components/auth/auth-gate.tsx +0 -64
- package/src/components/error-boundary.tsx +0 -45
- package/src/components/forms/field-renderer.tsx +0 -111
- package/src/components/forms/fields/block-builder.tsx +0 -213
- package/src/components/forms/fields/date-picker.tsx +0 -60
- package/src/components/forms/fields/json-editor.tsx +0 -62
- package/src/components/forms/fields/media-picker.tsx +0 -286
- package/src/components/forms/fields/multi-select.tsx +0 -145
- package/src/components/forms/fields/radio-field.tsx +0 -51
- package/src/components/forms/fields/relationship-picker.tsx +0 -143
- package/src/components/forms/fields/rich-text-editor.tsx +0 -224
- package/src/components/forms/fields/select-field.tsx +0 -35
- package/src/components/forms/fields/switch-field.tsx +0 -16
- package/src/components/forms/fields/text-area-field.tsx +0 -15
- package/src/components/forms/fields/text-field.tsx +0 -24
- package/src/components/forms/form-engine.tsx +0 -87
- package/src/components/forms/form-field-renderer.tsx +0 -269
- package/src/components/forms/utils.ts +0 -97
- package/src/components/layout/admin-shell.tsx +0 -479
- package/src/components/layout/branding-provider.tsx +0 -112
- package/src/components/live-preview/LivePreviewPane.tsx +0 -128
- package/src/components/media/focal-point-picker.tsx +0 -66
- package/src/components/media/media-card.tsx +0 -44
- package/src/components/media/media-grid.tsx +0 -32
- package/src/components/media/media-library-dialog.tsx +0 -465
- package/src/components/ui/aspect-ratio.tsx +0 -7
- package/src/components/ui/badge.tsx +0 -36
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/calendar.tsx +0 -214
- package/src/components/ui/card.tsx +0 -79
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/command.tsx +0 -151
- package/src/components/ui/data-table.tsx +0 -219
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -200
- package/src/components/ui/form.tsx +0 -178
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/page-header.tsx +0 -30
- package/src/components/ui/pagination.tsx +0 -57
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -26
- package/src/components/ui/radio-group.tsx +0 -42
- package/src/components/ui/render-cell.tsx +0 -110
- package/src/components/ui/scroll-area.tsx +0 -46
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -29
- package/src/components/ui/sheet.tsx +0 -140
- package/src/components/ui/sidebar.tsx +0 -771
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/sonner.tsx +0 -27
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -117
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -22
- package/src/components/ui/toggle.tsx +0 -43
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-preferences.ts +0 -56
- package/src/index.css +0 -111
- package/src/index.tsx +0 -198
- package/src/lib/utils.ts +0 -36
- package/src/main.tsx +0 -10
- package/src/pages/auth/first-user-page.tsx +0 -115
- package/src/pages/auth/login-page.tsx +0 -91
- package/src/pages/collections/edit-page.tsx +0 -280
- package/src/pages/collections/list-page.tsx +0 -343
- package/src/pages/dashboard/dashboard.tsx +0 -150
- package/src/pages/globals/editor-page.tsx +0 -122
- package/src/pages/media/media-page.tsx +0 -564
- package/src/pages/setup/setup-prompt.tsx +0 -181
- package/src/providers/dyrected-provider.tsx +0 -122
- package/src/providers/query-provider.tsx +0 -19
- package/src/types/jexl.d.ts +0 -11
- package/tailwind.config.ts +0 -103
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -12
- package/tsconfig.node.json +0 -25
- package/vite.config.ts +0 -39
- /package/{public → dist}/favicon.svg +0 -0
- /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
|
-
}
|