convex-cms 0.0.9-alpha.8 → 0.0.9-alpha.9
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/README.md +27 -0
- package/admin/src/components/cmsds/CmsFilterBar.tsx +74 -0
- package/admin/src/components/cmsds/CmsInput.tsx +24 -0
- package/admin/src/components/cmsds/CmsPagination.tsx +79 -0
- package/admin/src/components/cmsds/CmsSelect.tsx +59 -0
- package/admin/src/components/cmsds/CmsStatCard.tsx +79 -0
- package/admin/src/components/cmsds/CmsStatusBadge.tsx +1 -1
- package/admin/src/components/cmsds/index.ts +5 -0
- package/admin/src/contexts/ThemeContext.tsx +85 -17
- package/admin/src/embed/components/EmbedHeader.tsx +11 -9
- package/admin/src/embed/components/EmbedLayout.tsx +2 -6
- package/admin/src/embed/components/EmbedSidebar.tsx +7 -4
- package/admin/src/embed/index.tsx +3 -2
- package/admin/src/embed/types.ts +6 -0
- package/admin/src/pages/ContentPage.tsx +116 -172
- package/admin/src/pages/ContentTypeEntriesPage.tsx +120 -194
- package/admin/src/pages/ContentTypesPage.tsx +136 -139
- package/admin/src/pages/DashboardPage.tsx +13 -52
- package/admin/src/pages/MediaPage.tsx +31 -57
- package/admin/src/pages/SettingsPage.tsx +5 -1
- package/admin/src/pages/TrashPage.tsx +115 -170
- package/admin/src/styles/globals.css +10 -32
- package/admin/src/styles/tailwind-config.css +12 -0
- package/admin/src/styles/theme.css +229 -106
- package/package.json +1 -1
|
@@ -13,8 +13,7 @@ import {
|
|
|
13
13
|
CardHeader,
|
|
14
14
|
CardTitle,
|
|
15
15
|
} from "~/components/ui/card";
|
|
16
|
-
import {
|
|
17
|
-
import { CmsPageHeader } from "~/components/cmsds";
|
|
16
|
+
import { CmsPageHeader, CmsStatCard } from "~/components/cmsds";
|
|
18
17
|
import { SchemaDriftWarning } from "~/components/SchemaDriftWarning";
|
|
19
18
|
import { FileText, Image, Layers, Settings, TrendingUp } from "lucide-react";
|
|
20
19
|
import type { AdminNavigation } from "~/lib/navigation";
|
|
@@ -72,40 +71,24 @@ export function DashboardPage({ api, navigation }: DashboardPageProps) {
|
|
|
72
71
|
<h2 className="text-lg font-semibold">Quick Stats</h2>
|
|
73
72
|
</div>
|
|
74
73
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
value={
|
|
78
|
-
isLoading
|
|
79
|
-
? undefined
|
|
80
|
-
: hasError
|
|
81
|
-
? "—"
|
|
82
|
-
: String(stats.contentTypes)
|
|
83
|
-
}
|
|
74
|
+
<CmsStatCard
|
|
75
|
+
title="Content Types"
|
|
76
|
+
value={hasError ? "—" : String(stats?.contentTypes ?? 0)}
|
|
84
77
|
isLoading={isLoading}
|
|
85
78
|
/>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
value={
|
|
89
|
-
isLoading
|
|
90
|
-
? undefined
|
|
91
|
-
: hasError
|
|
92
|
-
? "—"
|
|
93
|
-
: String(stats.contentEntries)
|
|
94
|
-
}
|
|
79
|
+
<CmsStatCard
|
|
80
|
+
title="Content Entries"
|
|
81
|
+
value={hasError ? "—" : String(stats?.contentEntries ?? 0)}
|
|
95
82
|
isLoading={isLoading}
|
|
96
83
|
/>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
value={
|
|
100
|
-
isLoading ? undefined : hasError ? "—" : String(stats.mediaAssets)
|
|
101
|
-
}
|
|
84
|
+
<CmsStatCard
|
|
85
|
+
title="Media Assets"
|
|
86
|
+
value={hasError ? "—" : String(stats?.mediaAssets ?? 0)}
|
|
102
87
|
isLoading={isLoading}
|
|
103
88
|
/>
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
value={
|
|
107
|
-
isLoading ? undefined : hasError ? "—" : String(stats.published)
|
|
108
|
-
}
|
|
89
|
+
<CmsStatCard
|
|
90
|
+
title="Published"
|
|
91
|
+
value={hasError ? "—" : String(stats?.published ?? 0)}
|
|
109
92
|
isLoading={isLoading}
|
|
110
93
|
/>
|
|
111
94
|
</div>
|
|
@@ -142,25 +125,3 @@ function DashboardCard({
|
|
|
142
125
|
);
|
|
143
126
|
}
|
|
144
127
|
|
|
145
|
-
function StatCard({
|
|
146
|
-
label,
|
|
147
|
-
value,
|
|
148
|
-
isLoading = false,
|
|
149
|
-
}: {
|
|
150
|
-
label: string;
|
|
151
|
-
value?: string;
|
|
152
|
-
isLoading?: boolean;
|
|
153
|
-
}) {
|
|
154
|
-
return (
|
|
155
|
-
<Card>
|
|
156
|
-
<CardContent className="p-4">
|
|
157
|
-
{isLoading ? (
|
|
158
|
-
<Skeleton className="mb-1 h-8 w-16" />
|
|
159
|
-
) : (
|
|
160
|
-
<div className="text-2xl font-bold">{value}</div>
|
|
161
|
-
)}
|
|
162
|
-
<div className="text-sm text-muted-foreground">{label}</div>
|
|
163
|
-
</CardContent>
|
|
164
|
-
</Card>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import { useState, useMemo, useCallback, useEffect } from "react";
|
|
9
9
|
import { useQuery, useMutation } from "convex/react";
|
|
10
10
|
import { UploadDropzone, type UploadedFile } from "~/components/UploadDropzone";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
CmsPageHeader,
|
|
13
|
+
CmsEmptyState,
|
|
14
|
+
CmsButton,
|
|
15
|
+
CmsFilterBar,
|
|
16
|
+
} from "~/components/cmsds";
|
|
15
17
|
import { TaxonomyFilter } from "~/components/filters/TaxonomyFilter";
|
|
16
18
|
import {
|
|
17
19
|
Dialog,
|
|
@@ -22,13 +24,6 @@ import {
|
|
|
22
24
|
} from "~/components/ui/dialog";
|
|
23
25
|
import { Input } from "~/components/ui/input";
|
|
24
26
|
import { Label } from "~/components/ui/label";
|
|
25
|
-
import {
|
|
26
|
-
Select,
|
|
27
|
-
SelectContent,
|
|
28
|
-
SelectItem,
|
|
29
|
-
SelectTrigger,
|
|
30
|
-
SelectValue,
|
|
31
|
-
} from "~/components/ui/select";
|
|
32
27
|
import { Checkbox } from "~/components/ui/checkbox";
|
|
33
28
|
import { cn } from "~/lib/cn";
|
|
34
29
|
import {
|
|
@@ -43,7 +38,6 @@ import {
|
|
|
43
38
|
FolderPlus,
|
|
44
39
|
Upload,
|
|
45
40
|
Search,
|
|
46
|
-
X,
|
|
47
41
|
Trash2,
|
|
48
42
|
RotateCcw,
|
|
49
43
|
} from "lucide-react";
|
|
@@ -544,48 +538,31 @@ export function MediaPage({ api, navigation, settings }: MediaPageProps) {
|
|
|
544
538
|
</nav>
|
|
545
539
|
)}
|
|
546
540
|
|
|
547
|
-
<
|
|
548
|
-
|
|
541
|
+
<CmsFilterBar
|
|
542
|
+
search={{
|
|
543
|
+
value: searchQuery,
|
|
544
|
+
onChange: setSearchQuery,
|
|
545
|
+
placeholder: "Search files...",
|
|
546
|
+
className: "w-64",
|
|
547
|
+
}}
|
|
548
|
+
filters={[
|
|
549
|
+
{
|
|
550
|
+
key: "type",
|
|
551
|
+
value: typeFilter || "all",
|
|
552
|
+
onChange: (v) => setTypeFilter(v === "all" ? "" : (v as MediaType)),
|
|
553
|
+
options: [
|
|
554
|
+
{ value: "all", label: "All Types" },
|
|
555
|
+
{ value: "image", label: "Images" },
|
|
556
|
+
{ value: "video", label: "Videos" },
|
|
557
|
+
{ value: "audio", label: "Audio" },
|
|
558
|
+
{ value: "document", label: "Documents" },
|
|
559
|
+
{ value: "other", label: "Other" },
|
|
560
|
+
],
|
|
561
|
+
className: "w-36",
|
|
562
|
+
},
|
|
563
|
+
]}
|
|
564
|
+
actions={
|
|
549
565
|
<div className="flex items-center gap-3">
|
|
550
|
-
<div className="relative">
|
|
551
|
-
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
552
|
-
<Input
|
|
553
|
-
type="search"
|
|
554
|
-
placeholder="Search files..."
|
|
555
|
-
value={searchQuery}
|
|
556
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
557
|
-
className="w-64 pl-9"
|
|
558
|
-
/>
|
|
559
|
-
{searchQuery && (
|
|
560
|
-
<button
|
|
561
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 hover:bg-muted"
|
|
562
|
-
onClick={() => setSearchQuery("")}
|
|
563
|
-
aria-label="Clear search"
|
|
564
|
-
>
|
|
565
|
-
<X className="size-4 text-muted-foreground" />
|
|
566
|
-
</button>
|
|
567
|
-
)}
|
|
568
|
-
</div>
|
|
569
|
-
|
|
570
|
-
<Select
|
|
571
|
-
value={typeFilter || "all"}
|
|
572
|
-
onValueChange={(v) =>
|
|
573
|
-
setTypeFilter(v === "all" ? "" : (v as MediaType))
|
|
574
|
-
}
|
|
575
|
-
>
|
|
576
|
-
<SelectTrigger className="w-36">
|
|
577
|
-
<SelectValue placeholder="All Types" />
|
|
578
|
-
</SelectTrigger>
|
|
579
|
-
<SelectContent>
|
|
580
|
-
<SelectItem value="all">All Types</SelectItem>
|
|
581
|
-
<SelectItem value="image">Images</SelectItem>
|
|
582
|
-
<SelectItem value="video">Videos</SelectItem>
|
|
583
|
-
<SelectItem value="audio">Audio</SelectItem>
|
|
584
|
-
<SelectItem value="document">Documents</SelectItem>
|
|
585
|
-
<SelectItem value="other">Other</SelectItem>
|
|
586
|
-
</SelectContent>
|
|
587
|
-
</Select>
|
|
588
|
-
|
|
589
566
|
<TaxonomyFilter
|
|
590
567
|
selectedTermIds={selectedTermIds}
|
|
591
568
|
onChange={setSelectedTermIds}
|
|
@@ -606,10 +583,7 @@ export function MediaPage({ api, navigation, settings }: MediaPageProps) {
|
|
|
606
583
|
Selection Mode
|
|
607
584
|
</label>
|
|
608
585
|
)}
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
right={
|
|
612
|
-
<div className="flex items-center gap-2">
|
|
586
|
+
|
|
613
587
|
{isSelectionMode && selectedAssets.size > 0 && (
|
|
614
588
|
<span className="text-sm text-muted-foreground">
|
|
615
589
|
{selectedAssets.size} selected
|
|
@@ -84,7 +84,11 @@ const THEME_OPTIONS: {
|
|
|
84
84
|
];
|
|
85
85
|
|
|
86
86
|
function AppearanceSection() {
|
|
87
|
-
const { theme, setTheme } = useTheme();
|
|
87
|
+
const { theme, setTheme, canToggleDarkMode } = useTheme();
|
|
88
|
+
|
|
89
|
+
if (!canToggleDarkMode) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
88
92
|
|
|
89
93
|
return (
|
|
90
94
|
<CmsSurface elevation="base" className="p-6">
|
|
@@ -5,27 +5,21 @@
|
|
|
5
5
|
* Used by both CLI routes and embed pages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useCallback } from "react";
|
|
8
|
+
import { useState, useCallback, useMemo } from "react";
|
|
9
9
|
import { useQuery, useMutation } from "convex/react";
|
|
10
|
-
import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
|
|
11
|
-
import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
|
|
12
|
-
import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
|
|
13
|
-
import { CmsSurface } from "~/components/cmsds/CmsSurface";
|
|
14
|
-
import { CmsButton } from "~/components/cmsds/CmsButton";
|
|
15
|
-
import { CmsConfirmDialog } from "~/components/cmsds/CmsDialog";
|
|
16
|
-
import { Input } from "~/components/ui/input";
|
|
17
10
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
CmsPageHeader,
|
|
12
|
+
CmsEmptyState,
|
|
13
|
+
CmsSurface,
|
|
14
|
+
CmsButton,
|
|
15
|
+
CmsConfirmDialog,
|
|
16
|
+
CmsFilterBar,
|
|
17
|
+
CmsTable,
|
|
18
|
+
type CmsTableColumn,
|
|
19
|
+
} from "~/components/cmsds";
|
|
25
20
|
import { Badge } from "~/components/ui/badge";
|
|
26
21
|
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
27
|
-
import {
|
|
28
|
-
import { Search, Trash2, RotateCcw, AlertTriangle, X } from "lucide-react";
|
|
22
|
+
import { Trash2, RotateCcw, AlertTriangle, X } from "lucide-react";
|
|
29
23
|
import type { AdminNavigation } from "~/lib/navigation";
|
|
30
24
|
import { CmsAdminApi } from "~/embed/contexts/ApiContext";
|
|
31
25
|
|
|
@@ -77,26 +71,6 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
|
|
|
77
71
|
const config = configQuery;
|
|
78
72
|
const stats = statsQuery;
|
|
79
73
|
|
|
80
|
-
const handleSelectItem = useCallback((itemId: string, selected: boolean) => {
|
|
81
|
-
setSelectedItems((prev) => {
|
|
82
|
-
const next = new Set(prev);
|
|
83
|
-
if (selected) {
|
|
84
|
-
next.add(itemId);
|
|
85
|
-
} else {
|
|
86
|
-
next.delete(itemId);
|
|
87
|
-
}
|
|
88
|
-
return next;
|
|
89
|
-
});
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
const handleSelectAll = useCallback(() => {
|
|
93
|
-
if (selectedItems.size === trashItems.length) {
|
|
94
|
-
setSelectedItems(new Set());
|
|
95
|
-
} else {
|
|
96
|
-
setSelectedItems(new Set(trashItems.map((item) => item._id)));
|
|
97
|
-
}
|
|
98
|
-
}, [selectedItems.size, trashItems]);
|
|
99
|
-
|
|
100
74
|
const handleRestore = useCallback(
|
|
101
75
|
async (ids: string[]) => {
|
|
102
76
|
setIsRestoring(true);
|
|
@@ -166,6 +140,79 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
|
|
|
166
140
|
return item.slug || item._id;
|
|
167
141
|
};
|
|
168
142
|
|
|
143
|
+
const trashColumns: CmsTableColumn<TrashItem>[] = useMemo(
|
|
144
|
+
() => [
|
|
145
|
+
{
|
|
146
|
+
key: "name",
|
|
147
|
+
header: "Name",
|
|
148
|
+
cell: (item) => (
|
|
149
|
+
<>
|
|
150
|
+
<span className="font-medium text-foreground">{getItemTitle(item)}</span>
|
|
151
|
+
{item.slug && (
|
|
152
|
+
<span className="block text-xs text-muted-foreground">{item.slug}</span>
|
|
153
|
+
)}
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: "type",
|
|
159
|
+
header: "Type",
|
|
160
|
+
cell: (item) => (
|
|
161
|
+
<span className="text-sm text-muted-foreground">
|
|
162
|
+
{item.contentTypeName || "Unknown"}
|
|
163
|
+
</span>
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: "deleted",
|
|
168
|
+
header: "Deleted",
|
|
169
|
+
cell: (item) => (
|
|
170
|
+
<>
|
|
171
|
+
<span className="text-sm text-muted-foreground">
|
|
172
|
+
{formatDate(item.deletedAt)}
|
|
173
|
+
</span>
|
|
174
|
+
{item.deletedBy && (
|
|
175
|
+
<span className="block text-xs text-muted-foreground">
|
|
176
|
+
by {item.deletedBy}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "expires",
|
|
184
|
+
header: "Expires In",
|
|
185
|
+
cell: (item) => {
|
|
186
|
+
const daysLeft = getDaysUntilDeletion(item.deletedAt);
|
|
187
|
+
return daysLeft !== null ? (
|
|
188
|
+
<Badge
|
|
189
|
+
variant={daysLeft <= 3 ? "destructive" : "secondary"}
|
|
190
|
+
className="font-normal"
|
|
191
|
+
>
|
|
192
|
+
{daysLeft} {daysLeft === 1 ? "day" : "days"}
|
|
193
|
+
</Badge>
|
|
194
|
+
) : null;
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
key: "actions",
|
|
199
|
+
header: "Actions",
|
|
200
|
+
cell: (item) => (
|
|
201
|
+
<CmsButton
|
|
202
|
+
variant="outline"
|
|
203
|
+
size="sm"
|
|
204
|
+
onClick={() => handleRestore([item._id])}
|
|
205
|
+
loading={isRestoring}
|
|
206
|
+
>
|
|
207
|
+
<RotateCcw className="size-4" />
|
|
208
|
+
Restore
|
|
209
|
+
</CmsButton>
|
|
210
|
+
),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
[isRestoring, handleRestore, config?.retentionDays]
|
|
214
|
+
);
|
|
215
|
+
|
|
169
216
|
return (
|
|
170
217
|
<div className="space-y-6 p-6">
|
|
171
218
|
<div className="flex items-start justify-between">
|
|
@@ -200,39 +247,28 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
|
|
|
200
247
|
</div>
|
|
201
248
|
)}
|
|
202
249
|
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
<SelectContent>
|
|
226
|
-
<SelectItem value="all">All Content Types</SelectItem>
|
|
227
|
-
{contentTypes.map((type: any) => (
|
|
228
|
-
<SelectItem key={type._id} value={type._id}>
|
|
229
|
-
{type.displayName}
|
|
230
|
-
</SelectItem>
|
|
231
|
-
))}
|
|
232
|
-
</SelectContent>
|
|
233
|
-
</Select>
|
|
234
|
-
</div>
|
|
235
|
-
}
|
|
250
|
+
<CmsFilterBar
|
|
251
|
+
search={{
|
|
252
|
+
value: searchQuery,
|
|
253
|
+
onChange: setSearchQuery,
|
|
254
|
+
placeholder: "Search deleted items...",
|
|
255
|
+
className: "w-64",
|
|
256
|
+
}}
|
|
257
|
+
filters={[
|
|
258
|
+
{
|
|
259
|
+
key: "contentType",
|
|
260
|
+
value: selectedContentType || "all",
|
|
261
|
+
onChange: (v) => setSelectedContentType(v === "all" ? "" : v),
|
|
262
|
+
options: [
|
|
263
|
+
{ value: "all", label: "All Content Types" },
|
|
264
|
+
...contentTypes.map((type: { _id: string; displayName: string }) => ({
|
|
265
|
+
value: type._id,
|
|
266
|
+
label: type.displayName,
|
|
267
|
+
})),
|
|
268
|
+
],
|
|
269
|
+
className: "w-48",
|
|
270
|
+
},
|
|
271
|
+
]}
|
|
236
272
|
/>
|
|
237
273
|
|
|
238
274
|
{restoreError && (
|
|
@@ -304,106 +340,15 @@ export function TrashPage({ api, navigation: _navigation }: TrashPageProps) {
|
|
|
304
340
|
description="Deleted items will appear here"
|
|
305
341
|
/>
|
|
306
342
|
) : (
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
onCheckedChange={handleSelectAll}
|
|
318
|
-
/>
|
|
319
|
-
</th>
|
|
320
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
321
|
-
Name
|
|
322
|
-
</th>
|
|
323
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
324
|
-
Type
|
|
325
|
-
</th>
|
|
326
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
327
|
-
Deleted
|
|
328
|
-
</th>
|
|
329
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
330
|
-
Expires In
|
|
331
|
-
</th>
|
|
332
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
333
|
-
Actions
|
|
334
|
-
</th>
|
|
335
|
-
</tr>
|
|
336
|
-
</thead>
|
|
337
|
-
<tbody>
|
|
338
|
-
{trashItems.map((item) => {
|
|
339
|
-
const daysLeft = getDaysUntilDeletion(item.deletedAt);
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<tr
|
|
343
|
-
key={item._id}
|
|
344
|
-
className={cn(
|
|
345
|
-
"border-b last:border-0 transition-colors hover:bg-muted/50",
|
|
346
|
-
selectedItems.has(item._id) && "bg-primary/5",
|
|
347
|
-
)}
|
|
348
|
-
>
|
|
349
|
-
<td className="p-3">
|
|
350
|
-
<Checkbox
|
|
351
|
-
checked={selectedItems.has(item._id)}
|
|
352
|
-
onCheckedChange={(checked) =>
|
|
353
|
-
handleSelectItem(item._id, checked as boolean)
|
|
354
|
-
}
|
|
355
|
-
/>
|
|
356
|
-
</td>
|
|
357
|
-
<td className="p-3">
|
|
358
|
-
<span className="font-medium text-foreground">
|
|
359
|
-
{getItemTitle(item)}
|
|
360
|
-
</span>
|
|
361
|
-
{item.slug && (
|
|
362
|
-
<span className="block text-xs text-muted-foreground">
|
|
363
|
-
{item.slug}
|
|
364
|
-
</span>
|
|
365
|
-
)}
|
|
366
|
-
</td>
|
|
367
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
368
|
-
{item.contentTypeName || "Unknown"}
|
|
369
|
-
</td>
|
|
370
|
-
<td className="p-3">
|
|
371
|
-
<span className="text-sm text-muted-foreground">
|
|
372
|
-
{formatDate(item.deletedAt)}
|
|
373
|
-
</span>
|
|
374
|
-
{item.deletedBy && (
|
|
375
|
-
<span className="block text-xs text-muted-foreground">
|
|
376
|
-
by {item.deletedBy}
|
|
377
|
-
</span>
|
|
378
|
-
)}
|
|
379
|
-
</td>
|
|
380
|
-
<td className="p-3">
|
|
381
|
-
{daysLeft !== null && (
|
|
382
|
-
<Badge
|
|
383
|
-
variant={daysLeft <= 3 ? "destructive" : "secondary"}
|
|
384
|
-
className="font-normal"
|
|
385
|
-
>
|
|
386
|
-
{daysLeft} {daysLeft === 1 ? "day" : "days"}
|
|
387
|
-
</Badge>
|
|
388
|
-
)}
|
|
389
|
-
</td>
|
|
390
|
-
<td className="p-3">
|
|
391
|
-
<CmsButton
|
|
392
|
-
variant="outline"
|
|
393
|
-
size="sm"
|
|
394
|
-
onClick={() => handleRestore([item._id])}
|
|
395
|
-
loading={isRestoring}
|
|
396
|
-
>
|
|
397
|
-
<RotateCcw className="size-4" />
|
|
398
|
-
Restore
|
|
399
|
-
</CmsButton>
|
|
400
|
-
</td>
|
|
401
|
-
</tr>
|
|
402
|
-
);
|
|
403
|
-
})}
|
|
404
|
-
</tbody>
|
|
405
|
-
</table>
|
|
406
|
-
</div>
|
|
343
|
+
<CmsTable
|
|
344
|
+
columns={trashColumns}
|
|
345
|
+
data={trashItems}
|
|
346
|
+
getRowId={(item) => item._id}
|
|
347
|
+
selectable
|
|
348
|
+
selectedIds={selectedItems}
|
|
349
|
+
onSelectionChange={setSelectedItems}
|
|
350
|
+
emptyMessage="No items in trash"
|
|
351
|
+
/>
|
|
407
352
|
)}
|
|
408
353
|
|
|
409
354
|
<CmsConfirmDialog
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
-moz-osx-font-smoothing: grayscale;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/* Embedded mode -
|
|
23
|
+
/* Embedded mode - scoped base styles */
|
|
24
24
|
[data-cms-admin] {
|
|
25
25
|
@apply bg-background text-foreground;
|
|
26
26
|
font-feature-settings: 'rlig' 1, 'calt' 1;
|
|
@@ -30,47 +30,25 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
@layer utilities {
|
|
33
|
-
/* Status badge colors - using
|
|
33
|
+
/* Status badge colors - using CSS variables for theme consistency */
|
|
34
34
|
.status-draft {
|
|
35
|
-
|
|
35
|
+
background-color: var(--muted);
|
|
36
|
+
color: var(--muted-foreground);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
.status-published {
|
|
39
|
-
|
|
40
|
+
background-color: var(--diff-added-bg);
|
|
41
|
+
color: var(--diff-added-foreground);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
.status-scheduled {
|
|
43
|
-
|
|
45
|
+
background-color: var(--info-bg);
|
|
46
|
+
color: var(--info-foreground);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
.status-archived {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/* Diff change indicators */
|
|
51
|
-
.diff-added {
|
|
52
|
-
@apply bg-diff-added-bg border-diff-added-border text-diff-added-foreground;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.diff-removed {
|
|
56
|
-
@apply bg-diff-removed-bg border-diff-removed-border text-diff-removed-foreground;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.diff-modified {
|
|
60
|
-
@apply bg-diff-modified-bg border-diff-modified-border text-diff-modified-foreground;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* Icon badges for diff */
|
|
64
|
-
.diff-icon-added {
|
|
65
|
-
@apply bg-diff-added/20 text-diff-added;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.diff-icon-removed {
|
|
69
|
-
@apply bg-diff-removed/20 text-diff-removed;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.diff-icon-modified {
|
|
73
|
-
@apply bg-diff-modified/20 text-diff-modified;
|
|
50
|
+
background-color: var(--diff-modified-bg);
|
|
51
|
+
color: var(--diff-modified-foreground);
|
|
74
52
|
}
|
|
75
53
|
|
|
76
54
|
/* Surface elevations */
|
|
@@ -96,4 +96,16 @@
|
|
|
96
96
|
--color-info: var(--info);
|
|
97
97
|
--color-info-bg: var(--info-bg);
|
|
98
98
|
--color-info-foreground: var(--info-foreground);
|
|
99
|
+
|
|
100
|
+
/* Purple (workflow states) */
|
|
101
|
+
--color-purple: var(--purple);
|
|
102
|
+
--color-purple-bg: var(--purple-bg);
|
|
103
|
+
--color-purple-foreground: var(--purple-foreground);
|
|
104
|
+
|
|
105
|
+
/* Chart colors */
|
|
106
|
+
--color-chart-1: var(--chart-1);
|
|
107
|
+
--color-chart-2: var(--chart-2);
|
|
108
|
+
--color-chart-3: var(--chart-3);
|
|
109
|
+
--color-chart-4: var(--chart-4);
|
|
110
|
+
--color-chart-5: var(--chart-5);
|
|
99
111
|
}
|