convex-cms 0.0.9-alpha.8 → 0.0.10
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/Header.tsx +1 -1
- package/admin/src/components/RouteGuard.tsx +1 -1
- package/admin/src/components/UploadDropzone.tsx +1 -1
- 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/components/ui/sidebar.tsx +1 -1
- package/admin/src/contexts/AuthContext.tsx +1 -1
- 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 +16 -13
- package/admin/src/embed/contexts/ApiContext.tsx +1 -1
- package/admin/src/embed/index.tsx +3 -2
- package/admin/src/embed/types.ts +6 -0
- package/admin/src/hooks/usePermissions.ts +1 -1
- package/admin/src/index.css +432 -0
- package/admin/src/lib/cmsExports.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 +15 -55
- 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/routes/__root.tsx +1 -1
- package/admin-dist/nitro.json +1 -1
- package/admin-dist/public/assets/{CmsEmptyState-DTlpzjOI.js → CmsEmptyState-BKeL4DBB.js} +1 -1
- package/admin-dist/public/assets/CmsFilterBar-CEpMHd_c.js +1 -0
- package/admin-dist/public/assets/{CmsPageHeader-0REGRH4X.js → CmsPageHeader-CIEkTbyH.js} +1 -1
- package/admin-dist/public/assets/{CmsStatusBadge-D_n8u8xa.js → CmsStatusBadge-BFMOsfMW.js} +1 -1
- package/admin-dist/public/assets/{CmsSurface-BHmvNai4.js → CmsSurface-kqqaFKUI.js} +1 -1
- package/admin-dist/public/assets/CmsTable-Db53Exq0.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-Ct7cHayy.js +4 -0
- package/admin-dist/public/assets/TaxonomyFilter-Bm1DI1A7.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-BekeCblX.js +1 -0
- package/admin-dist/public/assets/{_entryId-jPXz4z9T.js → _entryId-CoZDE0l0.js} +1 -1
- package/admin-dist/public/assets/{alert-CG97cMfC.js → alert-CpLdsTGU.js} +1 -1
- package/admin-dist/public/assets/{badge-C6qt24oj.js → badge-BQAotc5B.js} +1 -1
- package/admin-dist/public/assets/{circle-check-big-PltpxuB1.js → circle-check-big-BF3Y5nES.js} +1 -1
- package/admin-dist/public/assets/{command-CJ8i86fd.js → command-lEq6f_Ee.js} +1 -1
- package/admin-dist/public/assets/content-DH6k0dN6.js +1 -0
- package/admin-dist/public/assets/content-types-DHr9tc2V.js +1 -0
- package/admin-dist/public/assets/index-Cf0lbl0G.js +1 -0
- package/admin-dist/public/assets/index-D-4wFfgU.css +1 -0
- package/admin-dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/admin-dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/admin-dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/admin-dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/admin-dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/admin-dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/admin-dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/admin-dist/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/admin-dist/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/admin-dist/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/admin-dist/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/admin-dist/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/admin-dist/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/admin-dist/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/admin-dist/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/admin-dist/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/admin-dist/public/assets/main-B-6700eG.js +137 -0
- package/admin-dist/public/assets/media-DY5zD52L.js +1 -0
- package/admin-dist/public/assets/{new._contentTypeId-qsvo01mH.js → new._contentTypeId-Dq_NqTQV.js} +1 -1
- package/admin-dist/public/assets/{pencil-gAL0R34f.js → pencil-CI_KfxSx.js} +1 -1
- package/admin-dist/public/assets/refresh-cw-BrXg9a2r.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-PwzxdPxd.js +1 -0
- package/admin-dist/public/assets/{scroll-area-CJBhf9pf.js → scroll-area-DX_nZYp8.js} +1 -1
- package/admin-dist/public/assets/{search-WXp6KxDJ.js → search-DlwBH4C5.js} +1 -1
- package/admin-dist/public/assets/settings-2mx3_ORG.js +1 -0
- package/admin-dist/public/assets/{switch-Ck9ecqEX.js → switch-CjPi4DKH.js} +1 -1
- package/admin-dist/public/assets/{tabs-vQYu8rjC.js → tabs-B5X37GEM.js} +1 -1
- package/admin-dist/public/assets/tanstack-adapter-KSm-nO5L.js +1 -0
- package/admin-dist/public/assets/{taxonomies-DvILUNvr.js → taxonomies-CHjJKNlR.js} +1 -1
- package/admin-dist/public/assets/trash-Cle-tcqq.js +1 -0
- package/admin-dist/public/assets/{useBreadcrumbLabel-tlSh7dtO.js → useBreadcrumbLabel-yZQG_N_3.js} +1 -1
- package/admin-dist/public/assets/{usePermissions-BTGdTOJS.js → usePermissions-D6vsoaJf.js} +1 -1
- package/admin-dist/server/_libs/convex-helpers.mjs +1077 -2
- package/admin-dist/server/_libs/convex.mjs +222 -13
- package/admin-dist/server/_libs/lucide-react.mjs +57 -51
- package/admin-dist/server/_ssr/{CmsEmptyState-CB6e53i5.mjs → CmsEmptyState-DzzuQG0S.mjs} +1 -1
- package/admin-dist/server/_ssr/CmsFilterBar-C5XADS12.mjs +81 -0
- package/admin-dist/server/_ssr/{CmsPageHeader-COUHuECp.mjs → CmsPageHeader-DZ6h7smh.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsStatusBadge-kMTL6koE.mjs → CmsStatusBadge-D-YFSAa1.mjs} +3 -3
- package/admin-dist/server/_ssr/{CmsSurface-D1HDYjRg.mjs → CmsSurface-Cv51NBLZ.mjs} +1 -1
- package/admin-dist/server/_ssr/CmsTable-DG88C5nO.mjs +189 -0
- package/admin-dist/server/_ssr/{ContentEntryEditor-Bq8FR_uK.mjs → ContentEntryEditor-CRjwXB17.mjs} +10 -10
- package/admin-dist/server/_ssr/{TaxonomyFilter-bm_p4ADg.mjs → TaxonomyFilter-xGwcgtjr.mjs} +3 -3
- package/admin-dist/server/_ssr/{_contentTypeId-B7obLmi_.mjs → _contentTypeId-DRCfeKkm.mjs} +53 -12
- package/admin-dist/server/_ssr/{_entryId-B4zhQqFg.mjs → _entryId-DULm2TDy.mjs} +11 -11
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-iX3K33p1.mjs +4 -0
- package/admin-dist/server/_ssr/{badge-NOEC9bkk.mjs → badge-CbjIvhb6.mjs} +1 -1
- package/admin-dist/server/_ssr/{command-h4-OYNBo.mjs → command-xB2uiYps.mjs} +2 -2
- package/admin-dist/server/_ssr/{content-CShtLuhK.mjs → content-BfLBaJCZ.mjs} +108 -138
- package/admin-dist/server/_ssr/{content-types-PeyRyfbc.mjs → content-types-DZbF6O2q.mjs} +130 -119
- package/admin-dist/server/_ssr/{index-CplFXpGg.mjs → index-Cfe8sZv5.mjs} +65 -39
- package/admin-dist/server/_ssr/index.mjs +2 -2
- package/admin-dist/server/_ssr/{media-QAkNdX54.mjs → media-Bds2AnPC.mjs} +36 -56
- package/admin-dist/server/_ssr/{new._contentTypeId-DEJyMphJ.mjs → new._contentTypeId-DGvz_tlW.mjs} +10 -10
- package/admin-dist/server/_ssr/{router-CQXMuGMF.mjs → router-DxF7GBcO.mjs} +8804 -4995
- package/admin-dist/server/_ssr/{scroll-area-B7zoNyWB.mjs → scroll-area-DLDlXI07.mjs} +1 -1
- package/admin-dist/server/_ssr/{settings-CNaqVa4D.mjs → settings-BbaiS6z9.mjs} +13 -10
- package/admin-dist/server/_ssr/{switch-BKZhvryc.mjs → switch-Bl89Pfxu.mjs} +1 -1
- package/admin-dist/server/_ssr/{tabs-DtIIQxiD.mjs → tabs-QkbR0iir.mjs} +3 -3
- package/admin-dist/server/_ssr/{tanstack-adapter-CLavdbUY.mjs → tanstack-adapter-CKknPtcU.mjs} +19 -1
- package/admin-dist/server/_ssr/{taxonomies-vIZYICzr.mjs → taxonomies-S_Ontd0z.mjs} +9 -9
- package/admin-dist/server/_ssr/{trash-7yGR4-dF.mjs → trash-BzAIsbbN.mjs} +109 -132
- package/admin-dist/server/_ssr/{useBreadcrumbLabel-DR5FaAMf.mjs → useBreadcrumbLabel-BjiR1fM_.mjs} +1 -1
- package/admin-dist/server/_ssr/{usePermissions-DKkpETj_.mjs → usePermissions-CDHN95Nz.mjs} +1 -1
- package/admin-dist/server/index.mjs +284 -165
- package/package.json +3 -2
- package/admin/src/styles/globals.css +0 -104
- package/admin/src/styles/tailwind-config.css +0 -99
- package/admin/src/styles/theme.css +0 -261
- package/admin-dist/public/assets/CmsToolbar-CY6GV2L8.js +0 -1
- package/admin-dist/public/assets/ContentEntryEditor-CRgcRkk5.js +0 -4
- package/admin-dist/public/assets/TaxonomyFilter-Ohv5Jg9c.js +0 -1
- package/admin-dist/public/assets/_contentTypeId-C_vJq22X.js +0 -1
- package/admin-dist/public/assets/content-pKaIL2ru.js +0 -1
- package/admin-dist/public/assets/content-types-Bl_8I1Re.js +0 -1
- package/admin-dist/public/assets/globals-CoCRjt0K.css +0 -1
- package/admin-dist/public/assets/index-CtHq_P5q.js +0 -1
- package/admin-dist/public/assets/main-CA-4LyFT.js +0 -107
- package/admin-dist/public/assets/media-Bl1tBbJQ.js +0 -1
- package/admin-dist/public/assets/refresh-cw-sdVUGJNs.js +0 -1
- package/admin-dist/public/assets/rotate-ccw-6OcXCcxb.js +0 -1
- package/admin-dist/public/assets/settings-D8crrFCn.js +0 -1
- package/admin-dist/public/assets/tanstack-adapter-BRt2CUCw.js +0 -1
- package/admin-dist/public/assets/trash-YyYaC3L9.js +0 -1
- package/admin-dist/server/_ssr/CmsToolbar-NB014hsd.mjs +0 -48
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DndoqCo7.mjs +0 -4
|
@@ -8,28 +8,19 @@
|
|
|
8
8
|
import { useState, useMemo, useEffect, useCallback } from "react";
|
|
9
9
|
import { useQuery, useMutation } from "convex/react";
|
|
10
10
|
import { usePermissions } from "~/hooks";
|
|
11
|
-
import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
|
|
12
|
-
import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
|
|
13
|
-
import { CmsButton } from "~/components/cmsds/CmsButton";
|
|
14
|
-
import { CmsStatusBadge, type ContentStatus } from "~/components/cmsds/CmsStatusBadge";
|
|
15
|
-
import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
|
|
16
|
-
import { CmsConfirmDialog } from "~/components/cmsds/CmsDialog";
|
|
17
|
-
import { Input } from "~/components/ui/input";
|
|
18
11
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ArrowUpDown,
|
|
32
|
-
} from "lucide-react";
|
|
12
|
+
CmsPageHeader,
|
|
13
|
+
CmsButton,
|
|
14
|
+
CmsStatusBadge,
|
|
15
|
+
type ContentStatus,
|
|
16
|
+
CmsEmptyState,
|
|
17
|
+
CmsConfirmDialog,
|
|
18
|
+
CmsFilterBar,
|
|
19
|
+
CmsTable,
|
|
20
|
+
type CmsTableColumn,
|
|
21
|
+
CmsPagination,
|
|
22
|
+
} from "~/components/cmsds";
|
|
23
|
+
import { Plus, FileText } from "lucide-react";
|
|
33
24
|
import type { AdminNavigation } from "~/lib/navigation";
|
|
34
25
|
import type { CmsAdminApi } from "~/embed/contexts/ApiContext";
|
|
35
26
|
|
|
@@ -143,7 +134,8 @@ export function ContentTypeEntriesPage({
|
|
|
143
134
|
});
|
|
144
135
|
};
|
|
145
136
|
|
|
146
|
-
const handleSort = (
|
|
137
|
+
const handleSort = (columnKey: string) => {
|
|
138
|
+
const field = columnKey as SortField;
|
|
147
139
|
if (sortField === field) {
|
|
148
140
|
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
|
149
141
|
} else {
|
|
@@ -152,17 +144,6 @@ export function ContentTypeEntriesPage({
|
|
|
152
144
|
}
|
|
153
145
|
};
|
|
154
146
|
|
|
155
|
-
const getSortIcon = (field: SortField) => {
|
|
156
|
-
if (sortField !== field) {
|
|
157
|
-
return <ArrowUpDown className="size-3.5 text-muted-foreground/50" />;
|
|
158
|
-
}
|
|
159
|
-
return sortDirection === "asc" ? (
|
|
160
|
-
<ChevronUp className="size-3.5" />
|
|
161
|
-
) : (
|
|
162
|
-
<ChevronDown className="size-3.5" />
|
|
163
|
-
);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
147
|
const handleDeleteClick = useCallback(
|
|
167
148
|
(entry: { _id: string; data: Record<string, unknown> }) => {
|
|
168
149
|
const title = getEntryTitle(entry);
|
|
@@ -212,6 +193,71 @@ export function ContentTypeEntriesPage({
|
|
|
212
193
|
setCurrentPage(0);
|
|
213
194
|
}, []);
|
|
214
195
|
|
|
196
|
+
type Entry = (typeof paginatedEntries)[number];
|
|
197
|
+
|
|
198
|
+
const entryColumns: CmsTableColumn<Entry>[] = useMemo(
|
|
199
|
+
() => [
|
|
200
|
+
{
|
|
201
|
+
key: "title",
|
|
202
|
+
header: "Title",
|
|
203
|
+
sortable: true,
|
|
204
|
+
cell: (entry) => (
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
208
|
+
className="block text-left"
|
|
209
|
+
>
|
|
210
|
+
<span className="font-medium text-foreground hover:text-primary hover:underline">
|
|
211
|
+
{getEntryTitle(entry)}
|
|
212
|
+
</span>
|
|
213
|
+
<span className="block text-xs text-muted-foreground">{entry.slug}</span>
|
|
214
|
+
</button>
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "status",
|
|
219
|
+
header: "Status",
|
|
220
|
+
sortable: true,
|
|
221
|
+
cell: (entry) => <CmsStatusBadge status={entry.status as ContentStatus} />,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
key: "updatedAt",
|
|
225
|
+
header: "Updated",
|
|
226
|
+
sortable: true,
|
|
227
|
+
cell: (entry) => (
|
|
228
|
+
<span className="text-sm text-muted-foreground">
|
|
229
|
+
{formatDate(entry.lastPublishedAt ?? entry._creationTime)}
|
|
230
|
+
</span>
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: "actions",
|
|
235
|
+
header: "Actions",
|
|
236
|
+
cell: (entry) => (
|
|
237
|
+
<div className="flex items-center gap-2">
|
|
238
|
+
<CmsButton
|
|
239
|
+
variant="outline"
|
|
240
|
+
size="sm"
|
|
241
|
+
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
242
|
+
>
|
|
243
|
+
{canUpdate("contentEntries") ? "Edit" : "View"}
|
|
244
|
+
</CmsButton>
|
|
245
|
+
{canDelete("contentEntries") && (
|
|
246
|
+
<CmsButton
|
|
247
|
+
variant="danger"
|
|
248
|
+
size="sm"
|
|
249
|
+
onClick={() => handleDeleteClick(entry)}
|
|
250
|
+
>
|
|
251
|
+
Delete
|
|
252
|
+
</CmsButton>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
[navigation, getEntryTitle, formatDate, canUpdate, canDelete, handleDeleteClick]
|
|
259
|
+
);
|
|
260
|
+
|
|
215
261
|
if (contentType === undefined || entriesResult === undefined) {
|
|
216
262
|
return (
|
|
217
263
|
<div className="space-y-6 p-6">
|
|
@@ -256,44 +302,32 @@ export function ContentTypeEntriesPage({
|
|
|
256
302
|
}
|
|
257
303
|
/>
|
|
258
304
|
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<SelectItem value="published">Published</SelectItem>
|
|
286
|
-
<SelectItem value="scheduled">Scheduled</SelectItem>
|
|
287
|
-
<SelectItem value="archived">Archived</SelectItem>
|
|
288
|
-
</SelectContent>
|
|
289
|
-
</Select>
|
|
290
|
-
</div>
|
|
291
|
-
}
|
|
292
|
-
right={
|
|
293
|
-
<span className="text-sm text-muted-foreground">
|
|
294
|
-
{sortedEntries.length} {sortedEntries.length === 1 ? "entry" : "entries"}
|
|
295
|
-
</span>
|
|
296
|
-
}
|
|
305
|
+
<CmsFilterBar
|
|
306
|
+
search={{
|
|
307
|
+
value: searchQuery,
|
|
308
|
+
onChange: setSearchQuery,
|
|
309
|
+
placeholder: "Search entries...",
|
|
310
|
+
className: "w-64",
|
|
311
|
+
}}
|
|
312
|
+
filters={[
|
|
313
|
+
{
|
|
314
|
+
key: "status",
|
|
315
|
+
value: selectedStatus,
|
|
316
|
+
onChange: (v) => {
|
|
317
|
+
setSelectedStatus(v as ContentStatus | "all");
|
|
318
|
+
setCurrentPage(0);
|
|
319
|
+
},
|
|
320
|
+
options: [
|
|
321
|
+
{ value: "all", label: "All Statuses" },
|
|
322
|
+
{ value: "draft", label: "Draft" },
|
|
323
|
+
{ value: "published", label: "Published" },
|
|
324
|
+
{ value: "scheduled", label: "Scheduled" },
|
|
325
|
+
{ value: "archived", label: "Archived" },
|
|
326
|
+
],
|
|
327
|
+
className: "w-36",
|
|
328
|
+
},
|
|
329
|
+
]}
|
|
330
|
+
onClearFilters={hasFilters ? clearFilters : undefined}
|
|
297
331
|
/>
|
|
298
332
|
|
|
299
333
|
{sortedEntries.length === 0 ? (
|
|
@@ -313,129 +347,21 @@ export function ContentTypeEntriesPage({
|
|
|
313
347
|
/>
|
|
314
348
|
) : (
|
|
315
349
|
<>
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
332
|
-
onClick={() => handleSort("status")}
|
|
333
|
-
>
|
|
334
|
-
Status
|
|
335
|
-
{getSortIcon("status")}
|
|
336
|
-
</button>
|
|
337
|
-
</th>
|
|
338
|
-
<th className="p-3 text-left">
|
|
339
|
-
<button
|
|
340
|
-
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
341
|
-
onClick={() => handleSort("updatedAt")}
|
|
342
|
-
>
|
|
343
|
-
Updated
|
|
344
|
-
{getSortIcon("updatedAt")}
|
|
345
|
-
</button>
|
|
346
|
-
</th>
|
|
347
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
348
|
-
Actions
|
|
349
|
-
</th>
|
|
350
|
-
</tr>
|
|
351
|
-
</thead>
|
|
352
|
-
<tbody>
|
|
353
|
-
{paginatedEntries.map((entry) => (
|
|
354
|
-
<tr
|
|
355
|
-
key={entry._id}
|
|
356
|
-
className="border-b last:border-0 transition-colors hover:bg-muted/50"
|
|
357
|
-
>
|
|
358
|
-
<td className="p-3">
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
362
|
-
className="text-left font-medium text-foreground hover:text-primary hover:underline"
|
|
363
|
-
>
|
|
364
|
-
{getEntryTitle(entry)}
|
|
365
|
-
</button>
|
|
366
|
-
<p className="text-xs text-muted-foreground">{entry.slug}</p>
|
|
367
|
-
</td>
|
|
368
|
-
<td className="p-3">
|
|
369
|
-
<CmsStatusBadge status={entry.status as ContentStatus} />
|
|
370
|
-
</td>
|
|
371
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
372
|
-
{formatDate(entry.lastPublishedAt ?? entry._creationTime)}
|
|
373
|
-
</td>
|
|
374
|
-
<td className="p-3">
|
|
375
|
-
<div className="flex items-center gap-2">
|
|
376
|
-
<CmsButton
|
|
377
|
-
variant="outline"
|
|
378
|
-
size="sm"
|
|
379
|
-
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
380
|
-
>
|
|
381
|
-
{canUpdate("contentEntries") ? "Edit" : "View"}
|
|
382
|
-
</CmsButton>
|
|
383
|
-
{canDelete("contentEntries") && (
|
|
384
|
-
<CmsButton
|
|
385
|
-
variant="danger"
|
|
386
|
-
size="sm"
|
|
387
|
-
onClick={() => handleDeleteClick(entry)}
|
|
388
|
-
>
|
|
389
|
-
Delete
|
|
390
|
-
</CmsButton>
|
|
391
|
-
)}
|
|
392
|
-
</div>
|
|
393
|
-
</td>
|
|
394
|
-
</tr>
|
|
395
|
-
))}
|
|
396
|
-
</tbody>
|
|
397
|
-
</table>
|
|
398
|
-
</div>
|
|
399
|
-
|
|
400
|
-
{totalPages > 1 && (
|
|
401
|
-
<div className="flex items-center justify-center gap-2">
|
|
402
|
-
<CmsButton
|
|
403
|
-
variant="outline"
|
|
404
|
-
size="sm"
|
|
405
|
-
onClick={() => setCurrentPage(0)}
|
|
406
|
-
disabled={currentPage === 0}
|
|
407
|
-
>
|
|
408
|
-
First
|
|
409
|
-
</CmsButton>
|
|
410
|
-
<CmsButton
|
|
411
|
-
variant="outline"
|
|
412
|
-
size="sm"
|
|
413
|
-
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
|
|
414
|
-
disabled={currentPage === 0}
|
|
415
|
-
>
|
|
416
|
-
Previous
|
|
417
|
-
</CmsButton>
|
|
418
|
-
<span className="px-3 text-sm text-muted-foreground">
|
|
419
|
-
Page {currentPage + 1} of {totalPages}
|
|
420
|
-
</span>
|
|
421
|
-
<CmsButton
|
|
422
|
-
variant="outline"
|
|
423
|
-
size="sm"
|
|
424
|
-
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
|
425
|
-
disabled={currentPage >= totalPages - 1}
|
|
426
|
-
>
|
|
427
|
-
Next
|
|
428
|
-
</CmsButton>
|
|
429
|
-
<CmsButton
|
|
430
|
-
variant="outline"
|
|
431
|
-
size="sm"
|
|
432
|
-
onClick={() => setCurrentPage(totalPages - 1)}
|
|
433
|
-
disabled={currentPage >= totalPages - 1}
|
|
434
|
-
>
|
|
435
|
-
Last
|
|
436
|
-
</CmsButton>
|
|
437
|
-
</div>
|
|
438
|
-
)}
|
|
350
|
+
<CmsTable
|
|
351
|
+
columns={entryColumns}
|
|
352
|
+
data={paginatedEntries}
|
|
353
|
+
getRowId={(e) => e._id}
|
|
354
|
+
sortColumn={sortField}
|
|
355
|
+
sortDirection={sortDirection}
|
|
356
|
+
onSort={handleSort}
|
|
357
|
+
emptyMessage="No entries found"
|
|
358
|
+
/>
|
|
359
|
+
|
|
360
|
+
<CmsPagination
|
|
361
|
+
currentPage={currentPage + 1}
|
|
362
|
+
totalPages={totalPages}
|
|
363
|
+
onPageChange={(page) => setCurrentPage(page - 1)}
|
|
364
|
+
/>
|
|
439
365
|
</>
|
|
440
366
|
)}
|
|
441
367
|
|
|
@@ -27,16 +27,18 @@ interface ContentTypeWithCount {
|
|
|
27
27
|
_creationTime: number;
|
|
28
28
|
source?: "code" | "database";
|
|
29
29
|
}
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
import {
|
|
31
|
+
CmsPageHeader,
|
|
32
|
+
CmsEmptyState,
|
|
33
|
+
CmsButton,
|
|
34
|
+
CmsFilterBar,
|
|
35
|
+
CmsTable,
|
|
36
|
+
type CmsTableColumn,
|
|
37
|
+
} from "~/components/cmsds";
|
|
35
38
|
import { Checkbox } from "~/components/ui/checkbox";
|
|
36
39
|
import { Badge } from "~/components/ui/badge";
|
|
37
40
|
import { cn } from "~/lib/cn";
|
|
38
41
|
import {
|
|
39
|
-
Search,
|
|
40
42
|
Grid3X3,
|
|
41
43
|
List,
|
|
42
44
|
Plus,
|
|
@@ -150,6 +152,120 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
150
152
|
}
|
|
151
153
|
};
|
|
152
154
|
|
|
155
|
+
const contentTypeColumns: CmsTableColumn<ContentTypeWithCount>[] = useMemo(
|
|
156
|
+
() => [
|
|
157
|
+
{
|
|
158
|
+
key: "name",
|
|
159
|
+
header: "Name",
|
|
160
|
+
cell: (contentType) => (
|
|
161
|
+
<>
|
|
162
|
+
<p className="font-medium text-foreground">{contentType.displayName}</p>
|
|
163
|
+
<p className="text-xs text-muted-foreground">{contentType.name}</p>
|
|
164
|
+
</>
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: "fields",
|
|
169
|
+
header: "Fields",
|
|
170
|
+
cell: (contentType) => (
|
|
171
|
+
<span className="text-sm text-muted-foreground">
|
|
172
|
+
{contentType.fields.length}
|
|
173
|
+
</span>
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "entries",
|
|
178
|
+
header: "Entries",
|
|
179
|
+
cell: (contentType) => (
|
|
180
|
+
<span className="text-sm text-muted-foreground">
|
|
181
|
+
{contentType.entryCount ?? 0}
|
|
182
|
+
</span>
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: "status",
|
|
187
|
+
header: "Status",
|
|
188
|
+
cell: (contentType) => (
|
|
189
|
+
<div className="flex items-center gap-1.5">
|
|
190
|
+
{contentType.source === "code" && (
|
|
191
|
+
<Badge
|
|
192
|
+
variant="secondary"
|
|
193
|
+
className="border-violet-200 bg-violet-50 text-xs font-normal text-violet-700"
|
|
194
|
+
title="Managed by code"
|
|
195
|
+
>
|
|
196
|
+
<Code2 className="mr-1 size-3" />
|
|
197
|
+
Code
|
|
198
|
+
</Badge>
|
|
199
|
+
)}
|
|
200
|
+
<Badge
|
|
201
|
+
variant={contentType.isActive ? "default" : "secondary"}
|
|
202
|
+
className={cn(
|
|
203
|
+
"text-xs font-normal",
|
|
204
|
+
contentType.isActive &&
|
|
205
|
+
"border-diff-added-border bg-diff-added-bg text-diff-added-foreground",
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
{contentType.isActive ? "Active" : "Inactive"}
|
|
209
|
+
</Badge>
|
|
210
|
+
{contentType.singleton && (
|
|
211
|
+
<Badge
|
|
212
|
+
variant="secondary"
|
|
213
|
+
className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
|
|
214
|
+
>
|
|
215
|
+
Singleton
|
|
216
|
+
</Badge>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "updated",
|
|
223
|
+
header: "Last Updated",
|
|
224
|
+
cell: (contentType) => (
|
|
225
|
+
<span className="text-sm text-muted-foreground">
|
|
226
|
+
{formatDate(contentType._creationTime)}
|
|
227
|
+
</span>
|
|
228
|
+
),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
key: "actions",
|
|
232
|
+
header: "Actions",
|
|
233
|
+
cell: (contentType) => (
|
|
234
|
+
<div className="flex items-center gap-2">
|
|
235
|
+
{contentType.source === "code" ? (
|
|
236
|
+
<CmsButton
|
|
237
|
+
variant="outline"
|
|
238
|
+
size="sm"
|
|
239
|
+
onClick={() => setEditingContentType(contentType)}
|
|
240
|
+
title="View content type (managed by code)"
|
|
241
|
+
>
|
|
242
|
+
<Eye className="size-3.5" />
|
|
243
|
+
View
|
|
244
|
+
</CmsButton>
|
|
245
|
+
) : (
|
|
246
|
+
<CmsButton
|
|
247
|
+
variant="outline"
|
|
248
|
+
size="sm"
|
|
249
|
+
onClick={() => setEditingContentType(contentType)}
|
|
250
|
+
>
|
|
251
|
+
<Pencil className="size-3.5" />
|
|
252
|
+
Edit
|
|
253
|
+
</CmsButton>
|
|
254
|
+
)}
|
|
255
|
+
<CmsButton
|
|
256
|
+
variant="outline"
|
|
257
|
+
size="sm"
|
|
258
|
+
onClick={() => navigation.navigateToContentType(contentType._id)}
|
|
259
|
+
>
|
|
260
|
+
View Entries
|
|
261
|
+
</CmsButton>
|
|
262
|
+
</div>
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
[navigation, formatDate, setEditingContentType]
|
|
267
|
+
);
|
|
268
|
+
|
|
153
269
|
return (
|
|
154
270
|
<div className="space-y-6 p-6">
|
|
155
271
|
<CmsPageHeader
|
|
@@ -157,19 +273,15 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
157
273
|
description="Define the structure of your content with custom fields and validation rules."
|
|
158
274
|
/>
|
|
159
275
|
|
|
160
|
-
<
|
|
161
|
-
|
|
276
|
+
<CmsFilterBar
|
|
277
|
+
search={{
|
|
278
|
+
value: searchQuery,
|
|
279
|
+
onChange: setSearchQuery,
|
|
280
|
+
placeholder: "Search content types...",
|
|
281
|
+
className: "w-64",
|
|
282
|
+
}}
|
|
283
|
+
actions={
|
|
162
284
|
<div className="flex items-center gap-3">
|
|
163
|
-
<div className="relative">
|
|
164
|
-
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
165
|
-
<Input
|
|
166
|
-
type="search"
|
|
167
|
-
placeholder="Search content types..."
|
|
168
|
-
value={searchQuery}
|
|
169
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
170
|
-
className="w-64 pl-9"
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
285
|
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
|
174
286
|
<Checkbox
|
|
175
287
|
checked={showActiveOnly}
|
|
@@ -179,10 +291,6 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
179
291
|
/>
|
|
180
292
|
Active only
|
|
181
293
|
</label>
|
|
182
|
-
</div>
|
|
183
|
-
}
|
|
184
|
-
right={
|
|
185
|
-
<div className="flex items-center gap-2">
|
|
186
294
|
<div className="flex rounded-md border bg-muted/30">
|
|
187
295
|
<button
|
|
188
296
|
className={cn(
|
|
@@ -361,123 +469,12 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
361
469
|
))}
|
|
362
470
|
</div>
|
|
363
471
|
) : (
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
</th>
|
|
371
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
372
|
-
Fields
|
|
373
|
-
</th>
|
|
374
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
375
|
-
Entries
|
|
376
|
-
</th>
|
|
377
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
378
|
-
Status
|
|
379
|
-
</th>
|
|
380
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
381
|
-
Last Updated
|
|
382
|
-
</th>
|
|
383
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
384
|
-
Actions
|
|
385
|
-
</th>
|
|
386
|
-
</tr>
|
|
387
|
-
</thead>
|
|
388
|
-
<tbody>
|
|
389
|
-
{filteredContentTypes.map((contentType) => (
|
|
390
|
-
<tr
|
|
391
|
-
key={contentType._id}
|
|
392
|
-
className="border-b last:border-0 transition-colors hover:bg-muted/50"
|
|
393
|
-
>
|
|
394
|
-
<td className="p-3">
|
|
395
|
-
<p className="font-medium text-foreground">
|
|
396
|
-
{contentType.displayName}
|
|
397
|
-
</p>
|
|
398
|
-
<p className="text-xs text-muted-foreground">
|
|
399
|
-
{contentType.name}
|
|
400
|
-
</p>
|
|
401
|
-
</td>
|
|
402
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
403
|
-
{contentType.fields.length}
|
|
404
|
-
</td>
|
|
405
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
406
|
-
{contentType.entryCount ?? 0}
|
|
407
|
-
</td>
|
|
408
|
-
<td className="p-3">
|
|
409
|
-
<div className="flex items-center gap-1.5">
|
|
410
|
-
{contentType.source === "code" && (
|
|
411
|
-
<Badge
|
|
412
|
-
variant="secondary"
|
|
413
|
-
className="border-violet-200 bg-violet-50 text-xs font-normal text-violet-700"
|
|
414
|
-
title="Managed by code"
|
|
415
|
-
>
|
|
416
|
-
<Code2 className="mr-1 size-3" />
|
|
417
|
-
Code
|
|
418
|
-
</Badge>
|
|
419
|
-
)}
|
|
420
|
-
<Badge
|
|
421
|
-
variant={contentType.isActive ? "default" : "secondary"}
|
|
422
|
-
className={cn(
|
|
423
|
-
"text-xs font-normal",
|
|
424
|
-
contentType.isActive &&
|
|
425
|
-
"border-diff-added-border bg-diff-added-bg text-diff-added-foreground",
|
|
426
|
-
)}
|
|
427
|
-
>
|
|
428
|
-
{contentType.isActive ? "Active" : "Inactive"}
|
|
429
|
-
</Badge>
|
|
430
|
-
{contentType.singleton && (
|
|
431
|
-
<Badge
|
|
432
|
-
variant="secondary"
|
|
433
|
-
className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
|
|
434
|
-
>
|
|
435
|
-
Singleton
|
|
436
|
-
</Badge>
|
|
437
|
-
)}
|
|
438
|
-
</div>
|
|
439
|
-
</td>
|
|
440
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
441
|
-
{formatDate(contentType._creationTime)}
|
|
442
|
-
</td>
|
|
443
|
-
<td className="p-3">
|
|
444
|
-
<div className="flex items-center gap-2">
|
|
445
|
-
{contentType.source === "code" ? (
|
|
446
|
-
<CmsButton
|
|
447
|
-
variant="outline"
|
|
448
|
-
size="sm"
|
|
449
|
-
onClick={() => setEditingContentType(contentType)}
|
|
450
|
-
title="View content type (managed by code)"
|
|
451
|
-
>
|
|
452
|
-
<Eye className="size-3.5" />
|
|
453
|
-
View
|
|
454
|
-
</CmsButton>
|
|
455
|
-
) : (
|
|
456
|
-
<CmsButton
|
|
457
|
-
variant="outline"
|
|
458
|
-
size="sm"
|
|
459
|
-
onClick={() => setEditingContentType(contentType)}
|
|
460
|
-
>
|
|
461
|
-
<Pencil className="size-3.5" />
|
|
462
|
-
Edit
|
|
463
|
-
</CmsButton>
|
|
464
|
-
)}
|
|
465
|
-
<CmsButton
|
|
466
|
-
variant="outline"
|
|
467
|
-
size="sm"
|
|
468
|
-
onClick={() =>
|
|
469
|
-
navigation.navigateToContentType(contentType._id)
|
|
470
|
-
}
|
|
471
|
-
>
|
|
472
|
-
View Entries
|
|
473
|
-
</CmsButton>
|
|
474
|
-
</div>
|
|
475
|
-
</td>
|
|
476
|
-
</tr>
|
|
477
|
-
))}
|
|
478
|
-
</tbody>
|
|
479
|
-
</table>
|
|
480
|
-
</div>
|
|
472
|
+
<CmsTable
|
|
473
|
+
columns={contentTypeColumns}
|
|
474
|
+
data={filteredContentTypes}
|
|
475
|
+
getRowId={(ct) => ct._id}
|
|
476
|
+
emptyMessage="No content types found"
|
|
477
|
+
/>
|
|
481
478
|
)}
|
|
482
479
|
|
|
483
480
|
{!isLoading && filteredContentTypes.length > 0 && (
|