convex-cms 0.0.7-alpha.0 → 0.0.8
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 +22 -17
- package/admin/src/components/BreakingChangesWarningDialog.tsx +5 -5
- package/admin/src/components/BulkOperationModal.tsx +14 -14
- package/admin/src/components/ContentEntryEditor.tsx +6 -6
- package/admin/src/components/ContentTypeFormModal.tsx +1 -1
- package/admin/src/components/Header.tsx +5 -2
- package/admin/src/components/SchemaDriftWarning.tsx +126 -0
- package/admin/src/components/TaxonomyEditor.tsx +2 -2
- package/admin/src/components/TermTree.tsx +3 -3
- package/admin/src/components/UploadDropzone.tsx +7 -7
- package/admin/src/components/VersionCompare.tsx +13 -13
- package/admin/src/components/VersionHistory.tsx +2 -2
- package/admin/src/components/VersionRollbackModal.tsx +5 -5
- package/admin/src/components/cmsds/CmsButton.tsx +2 -2
- package/admin/src/components/cmsds/CmsDialog.tsx +4 -1
- package/admin/src/components/cmsds/CmsStatusBadge.tsx +5 -5
- package/admin/src/components/fields/JsonField.tsx +1 -1
- package/admin/src/components/fields/TagField.tsx +1 -1
- package/admin/src/contexts/SettingsConfigContext.tsx +10 -3
- package/admin/src/embed/index.tsx +29 -9
- package/admin/src/embed/pages/ContentTypeEntries.tsx +25 -0
- package/admin/src/embed/pages/Entry.tsx +114 -0
- package/admin/src/embed/pages/Media.tsx +3 -1
- package/admin/src/embed/pages/NewEntry.tsx +83 -0
- package/admin/src/embed/pages/index.ts +3 -0
- package/admin/src/pages/ContentPage.tsx +16 -1
- package/admin/src/pages/ContentTypeEntriesPage.tsx +466 -0
- package/admin/src/pages/ContentTypesPage.tsx +3 -3
- package/admin/src/pages/DashboardPage.tsx +3 -0
- package/admin/src/pages/SettingsPage.tsx +4 -4
- package/admin/src/pages/index.ts +1 -0
- package/admin/src/routes/__root.tsx +10 -10
- package/admin/src/styles/globals.css +31 -5
- package/admin/src/styles/tailwind-config.css +25 -0
- package/admin/src/styles/theme.css +50 -0
- package/admin-dist/nitro.json +1 -1
- package/admin-dist/public/assets/{CmsEmptyState-CXVkI3FZ.js → CmsEmptyState-6-PLaXtD.js} +1 -1
- package/admin-dist/public/assets/{CmsPageHeader-DU9fD34s.js → CmsPageHeader-SoF4Epu9.js} +1 -1
- package/admin-dist/public/assets/CmsStatusBadge-D7kYaohx.js +1 -0
- package/admin-dist/public/assets/{CmsSurface-DF7OcKg_.js → CmsSurface-BvksBm6W.js} +1 -1
- package/admin-dist/public/assets/{CmsToolbar-5S8FQrSx.js → CmsToolbar-DlZPMe2B.js} +1 -1
- package/admin-dist/public/assets/ContentEntryEditor-C6n9xLS9.js +4 -0
- package/admin-dist/public/assets/{TaxonomyFilter-DEN2Q9Lo.js → TaxonomyFilter-CFX1_g8s.js} +1 -1
- package/admin-dist/public/assets/{_contentTypeId-Ba5iowxH.js → _contentTypeId-DTv8UoTp.js} +1 -1
- package/admin-dist/public/assets/_entryId-D3lr5Dvy.js +1 -0
- package/admin-dist/public/assets/alert-BAHTL6ao.js +1 -0
- package/admin-dist/public/assets/badge-oJv4Eai8.js +1 -0
- package/admin-dist/public/assets/{circle-check-big-B7eCOM8r.js → circle-check-big-3OHxNDhO.js} +1 -1
- package/admin-dist/public/assets/{command-BIjzeKOv.js → command-DwgQs69u.js} +1 -1
- package/admin-dist/public/assets/content-CKQ4QwW2.js +1 -0
- package/admin-dist/public/assets/content-types-BrttaLpc.js +1 -0
- package/admin-dist/public/assets/globals-CoCRjt0K.css +1 -0
- package/admin-dist/public/assets/index-DOkgTSx0.js +1 -0
- package/admin-dist/public/assets/{main-BZB1uYTH.js → main-DV6oxWnU.js} +5 -5
- package/admin-dist/public/assets/media-B2i-mCbx.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-VF63rpic.js +1 -0
- package/admin-dist/public/assets/{pencil-BDQ1ZWRw.js → pencil-CX1CiTDD.js} +1 -1
- package/admin-dist/public/assets/refresh-cw-Cm-YOeFI.js +1 -0
- package/admin-dist/public/assets/{rotate-ccw-BWblSIsl.js → rotate-ccw-B45JsL5f.js} +1 -1
- package/admin-dist/public/assets/{scroll-area-BoaB6x8v.js → scroll-area-b3A1HHR7.js} +1 -1
- package/admin-dist/public/assets/{search-CYMIpd39.js → search-DKKh_DdH.js} +1 -1
- package/admin-dist/public/assets/settings-CGVDEV1r.js +1 -0
- package/admin-dist/public/assets/{switch-DN7TOCa5.js → switch-BTMY8Qnk.js} +1 -1
- package/admin-dist/public/assets/tabs-DUQwUoIb.js +1 -0
- package/admin-dist/public/assets/{tanstack-adapter-DQcKErwf.js → tanstack-adapter-f7AHmQ5L.js} +1 -1
- package/admin-dist/public/assets/taxonomies-DvMppdiD.js +1 -0
- package/admin-dist/public/assets/{trash-Dp_a2mpb.js → trash-D7e0uKd9.js} +1 -1
- package/admin-dist/public/assets/{useBreadcrumbLabel-BQ9dJI6T.js → useBreadcrumbLabel-CF2KYwsw.js} +1 -1
- package/admin-dist/public/assets/{usePermissions-WUBNg_Id.js → usePermissions-DWBImEOW.js} +1 -1
- package/admin-dist/server/_libs/lucide-react.mjs +50 -43
- package/admin-dist/server/_ssr/{CmsEmptyState-DYh_PPQE.mjs → CmsEmptyState-BM8DghTl.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsPageHeader-BcniLh49.mjs → CmsPageHeader-BHUmrIWD.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsStatusBadge-BShWDxwE.mjs → CmsStatusBadge-D0Zb0oRl.mjs} +7 -7
- package/admin-dist/server/_ssr/{CmsSurface-CHEv-Kba.mjs → CmsSurface-B2eBr-47.mjs} +1 -1
- package/admin-dist/server/_ssr/{CmsToolbar-Dqqb216_.mjs → CmsToolbar-BCrwg7OL.mjs} +1 -1
- package/admin-dist/server/_ssr/{ContentEntryEditor-DOIAyWME.mjs → ContentEntryEditor-Cjfm0uhr.mjs} +37 -37
- package/admin-dist/server/_ssr/{TaxonomyFilter-BfsPAZ-Y.mjs → TaxonomyFilter-C4pD0kfM.mjs} +3 -3
- package/admin-dist/server/_ssr/{_contentTypeId-CPjmri90.mjs → _contentTypeId-CiDiX-p7.mjs} +11 -11
- package/admin-dist/server/_ssr/{_entryId-D0yu8HuP.mjs → _entryId-9GxatOkL.mjs} +11 -11
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-CC7UrHKE.mjs +4 -0
- package/admin-dist/server/_ssr/{badge-Cp61aQNc.mjs → badge-EI998zba.mjs} +1 -1
- package/admin-dist/server/_ssr/{command-BfjE1WJf.mjs → command-BLAWQhUw.mjs} +1 -1
- package/admin-dist/server/_ssr/{content-DrODe6sA.mjs → content-BHX39L4D.mjs} +31 -22
- package/admin-dist/server/_ssr/{content-types-BPgMwxiT.mjs → content-types-DCzrBhTH.mjs} +9 -9
- package/admin-dist/server/_ssr/{index-BTHmIC9W.mjs → index-DwM_5VNP.mjs} +92 -6
- package/admin-dist/server/_ssr/index.mjs +2 -2
- package/admin-dist/server/_ssr/{media-DkvBfmD9.mjs → media-CbzgTRRQ.mjs} +9 -9
- package/admin-dist/server/_ssr/{new._contentTypeId-Co_73sDJ.mjs → new._contentTypeId-6Ph-Gtlw.mjs} +10 -10
- package/admin-dist/server/_ssr/{router-CaDgRHfQ.mjs → router-vd1nySeP.mjs} +45 -35
- package/admin-dist/server/_ssr/{scroll-area-D3v-O_jk.mjs → scroll-area--B9snFTJ.mjs} +1 -1
- package/admin-dist/server/_ssr/{settings-MaEXh2Hz.mjs → settings-DlTO2JSj.mjs} +11 -11
- package/admin-dist/server/_ssr/{switch-DmbR03dm.mjs → switch-C05NgNW0.mjs} +1 -1
- package/admin-dist/server/_ssr/{tabs-5oFlAGLz.mjs → tabs-DAk2J5xy.mjs} +8 -8
- package/admin-dist/server/_ssr/{tanstack-adapter-DNaUioIZ.mjs → tanstack-adapter-DWbaPByn.mjs} +15 -1
- package/admin-dist/server/_ssr/{taxonomies-D3xMK23a.mjs → taxonomies-B8nqce6u.mjs} +12 -12
- package/admin-dist/server/_ssr/{trash-CNw1mtF1.mjs → trash-zdlZgpTo.mjs} +7 -7
- package/admin-dist/server/_ssr/{useBreadcrumbLabel-BQGjOTcy.mjs → useBreadcrumbLabel-DpEKyG1h.mjs} +1 -1
- package/admin-dist/server/_ssr/{usePermissions-D0qtvmNi.mjs → usePermissions-olYRd9S9.mjs} +1 -1
- package/admin-dist/server/index.mjs +164 -157
- package/dist/client/admin/contentTypes.d.ts +25 -0
- package/dist/client/admin/contentTypes.d.ts.map +1 -1
- package/dist/client/admin/contentTypes.js +212 -6
- package/dist/client/admin/contentTypes.js.map +1 -1
- package/dist/client/admin/entries.d.ts.map +1 -1
- package/dist/client/admin/entries.js +27 -0
- package/dist/client/admin/entries.js.map +1 -1
- package/dist/client/admin/index.d.ts +4 -0
- package/dist/client/admin/index.d.ts.map +1 -1
- package/dist/client/admin/index.js +16 -0
- package/dist/client/admin/index.js.map +1 -1
- package/dist/client/admin/types.d.ts +4 -0
- package/dist/client/admin/types.d.ts.map +1 -1
- package/dist/client/schema/defineContentType.d.ts.map +1 -1
- package/dist/client/schema/defineContentType.js +99 -80
- package/dist/client/schema/defineContentType.js.map +1 -1
- package/dist/component/contentTypeMutations.d.ts.map +1 -1
- package/dist/component/contentTypeMutations.js +5 -4
- package/dist/component/contentTypeMutations.js.map +1 -1
- package/package.json +2 -2
- package/admin-dist/public/assets/CmsStatusBadge-nZ9TeLBL.js +0 -1
- package/admin-dist/public/assets/ContentEntryEditor-BDb44eTo.js +0 -4
- package/admin-dist/public/assets/_entryId-OY3sLz6O.js +0 -1
- package/admin-dist/public/assets/alert-BbW1Q9CR.js +0 -1
- package/admin-dist/public/assets/badge-DdM8Eua8.js +0 -1
- package/admin-dist/public/assets/content-BV3YeSSW.js +0 -1
- package/admin-dist/public/assets/content-types-Bm4b2tf8.js +0 -1
- package/admin-dist/public/assets/globals-D41WzvyZ.css +0 -1
- package/admin-dist/public/assets/index-DnJ5Twlv.js +0 -1
- package/admin-dist/public/assets/media-BIMN5jXt.js +0 -1
- package/admin-dist/public/assets/new._contentTypeId-DTWb8ZDl.js +0 -1
- package/admin-dist/public/assets/settings-DaNDUtr5.js +0 -1
- package/admin-dist/public/assets/tabs-RN__emeJ.js +0 -1
- package/admin-dist/public/assets/taxonomies-DylY9HE1.js +0 -1
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DgCpSt_y.mjs +0 -4
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Content Type Entries Page Component
|
|
3
|
+
*
|
|
4
|
+
* Lists entries for a specific content type with search, filtering, and actions.
|
|
5
|
+
* Used by both CLI routes and embed pages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useMemo, useEffect, useCallback } from "react";
|
|
9
|
+
import { useQuery, useMutation } from "convex/react";
|
|
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
|
+
import {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from "~/components/ui/select";
|
|
25
|
+
import {
|
|
26
|
+
Search,
|
|
27
|
+
Plus,
|
|
28
|
+
FileText,
|
|
29
|
+
ChevronUp,
|
|
30
|
+
ChevronDown,
|
|
31
|
+
ArrowUpDown,
|
|
32
|
+
} from "lucide-react";
|
|
33
|
+
import type { AdminNavigation } from "~/lib/navigation";
|
|
34
|
+
import type { CmsAdminApi } from "~/embed/contexts/ApiContext";
|
|
35
|
+
|
|
36
|
+
type SortField = "title" | "status" | "updatedAt" | "createdAt";
|
|
37
|
+
type SortDirection = "asc" | "desc";
|
|
38
|
+
|
|
39
|
+
export interface ContentTypeEntriesPageProps {
|
|
40
|
+
api: CmsAdminApi;
|
|
41
|
+
navigation: AdminNavigation;
|
|
42
|
+
contentTypeId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ContentTypeEntriesPage({
|
|
46
|
+
api,
|
|
47
|
+
navigation,
|
|
48
|
+
contentTypeId,
|
|
49
|
+
}: ContentTypeEntriesPageProps) {
|
|
50
|
+
const [selectedStatus, setSelectedStatus] = useState<ContentStatus | "all">("all");
|
|
51
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
52
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
53
|
+
const [sortField, setSortField] = useState<SortField>("updatedAt");
|
|
54
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
|
55
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
56
|
+
const pageSize = 25;
|
|
57
|
+
|
|
58
|
+
const { canCreate, canUpdate, canDelete } = usePermissions();
|
|
59
|
+
|
|
60
|
+
const deleteEntry = useMutation(api.deleteEntry);
|
|
61
|
+
|
|
62
|
+
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
63
|
+
const [entryToDelete, setEntryToDelete] = useState<{ _id: string; title: string } | null>(null);
|
|
64
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
65
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
setDebouncedSearch(searchQuery);
|
|
70
|
+
setCurrentPage(0);
|
|
71
|
+
}, 300);
|
|
72
|
+
return () => clearTimeout(timer);
|
|
73
|
+
}, [searchQuery]);
|
|
74
|
+
|
|
75
|
+
const contentType = useQuery(api.getContentType, { id: contentTypeId });
|
|
76
|
+
|
|
77
|
+
const entriesResult = useQuery(api.listEntries, {
|
|
78
|
+
contentTypeName: contentType?.name,
|
|
79
|
+
status: selectedStatus === "all" ? undefined : selectedStatus,
|
|
80
|
+
search: debouncedSearch || undefined,
|
|
81
|
+
paginationOpts: { numItems: 250, cursor: null },
|
|
82
|
+
});
|
|
83
|
+
const allEntries = entriesResult?.page ?? [];
|
|
84
|
+
|
|
85
|
+
const getEntryTitle = useCallback(
|
|
86
|
+
(entry: { data: Record<string, unknown> }) => {
|
|
87
|
+
const titleField = contentType?.titleField ?? "title";
|
|
88
|
+
const title = entry.data[titleField];
|
|
89
|
+
return typeof title === "string" && title ? title : "Untitled";
|
|
90
|
+
},
|
|
91
|
+
[contentType?.titleField]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const sortedEntries = useMemo(() => {
|
|
95
|
+
const entries = [...allEntries];
|
|
96
|
+
|
|
97
|
+
entries.sort((a, b) => {
|
|
98
|
+
let comparison = 0;
|
|
99
|
+
|
|
100
|
+
switch (sortField) {
|
|
101
|
+
case "title": {
|
|
102
|
+
const titleA = getEntryTitle(a).toLowerCase();
|
|
103
|
+
const titleB = getEntryTitle(b).toLowerCase();
|
|
104
|
+
comparison = titleA.localeCompare(titleB);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "status":
|
|
108
|
+
comparison = a.status.localeCompare(b.status);
|
|
109
|
+
break;
|
|
110
|
+
case "updatedAt": {
|
|
111
|
+
const updatedA = a.lastPublishedAt ?? a._creationTime ?? 0;
|
|
112
|
+
const updatedB = b.lastPublishedAt ?? b._creationTime ?? 0;
|
|
113
|
+
comparison = updatedA - updatedB;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "createdAt":
|
|
117
|
+
comparison = (a._creationTime ?? 0) - (b._creationTime ?? 0);
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
comparison = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return sortDirection === "desc" ? -comparison : comparison;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return entries;
|
|
127
|
+
}, [allEntries, sortField, sortDirection, getEntryTitle]);
|
|
128
|
+
|
|
129
|
+
const paginatedEntries = useMemo(() => {
|
|
130
|
+
const start = currentPage * pageSize;
|
|
131
|
+
return sortedEntries.slice(start, start + pageSize);
|
|
132
|
+
}, [sortedEntries, currentPage, pageSize]);
|
|
133
|
+
|
|
134
|
+
const totalPages = Math.ceil(sortedEntries.length / pageSize);
|
|
135
|
+
|
|
136
|
+
const formatDate = (timestamp: number) => {
|
|
137
|
+
return new Date(timestamp).toLocaleDateString("en-US", {
|
|
138
|
+
year: "numeric",
|
|
139
|
+
month: "short",
|
|
140
|
+
day: "numeric",
|
|
141
|
+
hour: "2-digit",
|
|
142
|
+
minute: "2-digit",
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleSort = (field: SortField) => {
|
|
147
|
+
if (sortField === field) {
|
|
148
|
+
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
|
149
|
+
} else {
|
|
150
|
+
setSortField(field);
|
|
151
|
+
setSortDirection("desc");
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
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
|
+
const handleDeleteClick = useCallback(
|
|
167
|
+
(entry: { _id: string; data: Record<string, unknown> }) => {
|
|
168
|
+
const title = getEntryTitle(entry);
|
|
169
|
+
setEntryToDelete({ _id: entry._id, title });
|
|
170
|
+
setDeleteError(null);
|
|
171
|
+
setDeleteModalOpen(true);
|
|
172
|
+
},
|
|
173
|
+
[getEntryTitle]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const handleDeleteConfirm = useCallback(async () => {
|
|
177
|
+
if (!entryToDelete) return;
|
|
178
|
+
|
|
179
|
+
setIsDeleting(true);
|
|
180
|
+
setDeleteError(null);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await deleteEntry({
|
|
184
|
+
id: entryToDelete._id,
|
|
185
|
+
hardDelete: false,
|
|
186
|
+
});
|
|
187
|
+
setDeleteModalOpen(false);
|
|
188
|
+
setEntryToDelete(null);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : "Failed to delete entry";
|
|
191
|
+
setDeleteError(message);
|
|
192
|
+
} finally {
|
|
193
|
+
setIsDeleting(false);
|
|
194
|
+
}
|
|
195
|
+
}, [entryToDelete, deleteEntry]);
|
|
196
|
+
|
|
197
|
+
const handleDeleteModalClose = useCallback(
|
|
198
|
+
(open: boolean) => {
|
|
199
|
+
if (!open && !isDeleting) {
|
|
200
|
+
setDeleteModalOpen(false);
|
|
201
|
+
setEntryToDelete(null);
|
|
202
|
+
setDeleteError(null);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
[isDeleting]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const clearFilters = useCallback(() => {
|
|
209
|
+
setSearchQuery("");
|
|
210
|
+
setDebouncedSearch("");
|
|
211
|
+
setSelectedStatus("all");
|
|
212
|
+
setCurrentPage(0);
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
if (contentType === undefined || entriesResult === undefined) {
|
|
216
|
+
return (
|
|
217
|
+
<div className="space-y-6 p-6">
|
|
218
|
+
<div className="flex flex-col items-center justify-center py-12">
|
|
219
|
+
<div className="size-8 animate-spin rounded-full border-2 border-muted border-t-primary" />
|
|
220
|
+
<p className="mt-4 text-sm text-muted-foreground">Loading entries...</p>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (contentType === null) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="space-y-6 p-6">
|
|
229
|
+
<CmsEmptyState
|
|
230
|
+
icon={<FileText className="size-6" />}
|
|
231
|
+
title="Content Type Not Found"
|
|
232
|
+
description="The content type you're looking for doesn't exist or has been deleted."
|
|
233
|
+
action={{
|
|
234
|
+
label: "Back to Content Types",
|
|
235
|
+
onClick: () => navigation.navigate("/content-types"),
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hasFilters = searchQuery || selectedStatus !== "all";
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div className="space-y-6 p-6">
|
|
246
|
+
<CmsPageHeader
|
|
247
|
+
title={contentType.displayName}
|
|
248
|
+
description={contentType.description}
|
|
249
|
+
actions={
|
|
250
|
+
canCreate("contentEntries") && (
|
|
251
|
+
<CmsButton onClick={() => navigation.navigateToNewEntry(contentTypeId)}>
|
|
252
|
+
<Plus className="size-4" />
|
|
253
|
+
Create {contentType.displayName}
|
|
254
|
+
</CmsButton>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
<CmsToolbar
|
|
260
|
+
left={
|
|
261
|
+
<div className="flex items-center gap-3">
|
|
262
|
+
<div className="relative">
|
|
263
|
+
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
264
|
+
<Input
|
|
265
|
+
type="search"
|
|
266
|
+
placeholder="Search entries..."
|
|
267
|
+
value={searchQuery}
|
|
268
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
269
|
+
className="w-64 pl-9"
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
<Select
|
|
273
|
+
value={selectedStatus}
|
|
274
|
+
onValueChange={(value) => {
|
|
275
|
+
setSelectedStatus(value as ContentStatus | "all");
|
|
276
|
+
setCurrentPage(0);
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<SelectTrigger className="w-36">
|
|
280
|
+
<SelectValue placeholder="All Statuses" />
|
|
281
|
+
</SelectTrigger>
|
|
282
|
+
<SelectContent>
|
|
283
|
+
<SelectItem value="all">All Statuses</SelectItem>
|
|
284
|
+
<SelectItem value="draft">Draft</SelectItem>
|
|
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
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
{sortedEntries.length === 0 ? (
|
|
300
|
+
<CmsEmptyState
|
|
301
|
+
icon={<FileText className="size-6" />}
|
|
302
|
+
title={hasFilters ? "No matching entries" : `No ${contentType.displayName} entries yet`}
|
|
303
|
+
description={
|
|
304
|
+
hasFilters
|
|
305
|
+
? "Try adjusting your search or filter criteria."
|
|
306
|
+
: `Click "Create ${contentType.displayName}" to add your first entry.`
|
|
307
|
+
}
|
|
308
|
+
action={
|
|
309
|
+
hasFilters
|
|
310
|
+
? { label: "Clear Filters", onClick: clearFilters, variant: "secondary" }
|
|
311
|
+
: undefined
|
|
312
|
+
}
|
|
313
|
+
/>
|
|
314
|
+
) : (
|
|
315
|
+
<>
|
|
316
|
+
<div className="rounded-lg border bg-card">
|
|
317
|
+
<table className="w-full">
|
|
318
|
+
<thead>
|
|
319
|
+
<tr className="border-b">
|
|
320
|
+
<th className="p-3 text-left">
|
|
321
|
+
<button
|
|
322
|
+
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
323
|
+
onClick={() => handleSort("title")}
|
|
324
|
+
>
|
|
325
|
+
Title
|
|
326
|
+
{getSortIcon("title")}
|
|
327
|
+
</button>
|
|
328
|
+
</th>
|
|
329
|
+
<th className="p-3 text-left">
|
|
330
|
+
<button
|
|
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
|
+
)}
|
|
439
|
+
</>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{sortedEntries.length > 0 && (
|
|
443
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
444
|
+
Showing {paginatedEntries.length} of {sortedEntries.length}{" "}
|
|
445
|
+
{sortedEntries.length === 1 ? "entry" : "entries"}
|
|
446
|
+
</p>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
<CmsConfirmDialog
|
|
450
|
+
open={deleteModalOpen}
|
|
451
|
+
onOpenChange={handleDeleteModalClose}
|
|
452
|
+
title="Delete Entry"
|
|
453
|
+
description={
|
|
454
|
+
entryToDelete
|
|
455
|
+
? `Are you sure you want to delete "${entryToDelete.title}"? It will be moved to the trash and can be restored within the retention period.`
|
|
456
|
+
: "Are you sure you want to delete this entry?"
|
|
457
|
+
}
|
|
458
|
+
confirmLabel="Delete"
|
|
459
|
+
variant="danger"
|
|
460
|
+
onConfirm={handleDeleteConfirm}
|
|
461
|
+
isLoading={isDeleting}
|
|
462
|
+
error={deleteError}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
@@ -273,7 +273,7 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
273
273
|
{contentType.singleton && (
|
|
274
274
|
<Badge
|
|
275
275
|
variant="secondary"
|
|
276
|
-
className="border-
|
|
276
|
+
className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
|
|
277
277
|
>
|
|
278
278
|
Singleton
|
|
279
279
|
</Badge>
|
|
@@ -422,7 +422,7 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
422
422
|
className={cn(
|
|
423
423
|
"text-xs font-normal",
|
|
424
424
|
contentType.isActive &&
|
|
425
|
-
"border-
|
|
425
|
+
"border-diff-added-border bg-diff-added-bg text-diff-added-foreground",
|
|
426
426
|
)}
|
|
427
427
|
>
|
|
428
428
|
{contentType.isActive ? "Active" : "Inactive"}
|
|
@@ -430,7 +430,7 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
|
|
|
430
430
|
{contentType.singleton && (
|
|
431
431
|
<Badge
|
|
432
432
|
variant="secondary"
|
|
433
|
-
className="border-
|
|
433
|
+
className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
|
|
434
434
|
>
|
|
435
435
|
Singleton
|
|
436
436
|
</Badge>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "~/components/ui/card";
|
|
16
16
|
import { Skeleton } from "~/components/ui/skeleton";
|
|
17
17
|
import { CmsPageHeader } from "~/components/cmsds";
|
|
18
|
+
import { SchemaDriftWarning } from "~/components/SchemaDriftWarning";
|
|
18
19
|
import { FileText, Image, Layers, Settings, TrendingUp } from "lucide-react";
|
|
19
20
|
import type { AdminNavigation } from "~/lib/navigation";
|
|
20
21
|
import type { CmsAdminApi } from "~/embed/contexts/ApiContext";
|
|
@@ -36,6 +37,8 @@ export function DashboardPage({ api, navigation }: DashboardPageProps) {
|
|
|
36
37
|
description="Welcome to Convex CMS Admin. Manage your content, media, and publishing workflows."
|
|
37
38
|
/>
|
|
38
39
|
|
|
40
|
+
<SchemaDriftWarning api={api} />
|
|
41
|
+
|
|
39
42
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
40
43
|
<DashboardCard
|
|
41
44
|
title="Content Entries"
|
|
@@ -135,7 +135,7 @@ export function SettingsPage({
|
|
|
135
135
|
const adminConfig = useAdminConfig();
|
|
136
136
|
|
|
137
137
|
const isConfigured = useMemo(() => {
|
|
138
|
-
return
|
|
138
|
+
return api.getSettings != null;
|
|
139
139
|
}, [api]);
|
|
140
140
|
|
|
141
141
|
const settings = useQuery(
|
|
@@ -371,7 +371,7 @@ export function SettingsPage({
|
|
|
371
371
|
{feedbackStatus === "saved" && (
|
|
372
372
|
<Badge
|
|
373
373
|
variant="secondary"
|
|
374
|
-
className="gap-1 bg-
|
|
374
|
+
className="gap-1 bg-diff-added-bg text-diff-added-foreground"
|
|
375
375
|
>
|
|
376
376
|
<Check className="size-3" />
|
|
377
377
|
Settings saved successfully
|
|
@@ -517,9 +517,9 @@ export function SettingsPage({
|
|
|
517
517
|
{canEdit && api.resetSettings && (
|
|
518
518
|
<CmsSurface
|
|
519
519
|
elevation="base"
|
|
520
|
-
className="border-
|
|
520
|
+
className="border-destructive/50 p-6"
|
|
521
521
|
>
|
|
522
|
-
<h2 className="mb-4 text-lg font-semibold text-
|
|
522
|
+
<h2 className="mb-4 text-lg font-semibold text-destructive">
|
|
523
523
|
Danger Zone
|
|
524
524
|
</h2>
|
|
525
525
|
<div className="flex items-center justify-between">
|
package/admin/src/pages/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { ContentPage, type ContentPageProps } from "./ContentPage";
|
|
9
|
+
export { ContentTypeEntriesPage, type ContentTypeEntriesPageProps } from "./ContentTypeEntriesPage";
|
|
9
10
|
export { ContentTypesPage, type ContentTypesPageProps } from "./ContentTypesPage";
|
|
10
11
|
export { DashboardPage } from "./DashboardPage";
|
|
11
12
|
export { MediaPage, type MediaPageProps } from "./MediaPage";
|
|
@@ -185,40 +185,40 @@ function ConvexProviderWrapper({
|
|
|
185
185
|
if (!convex) {
|
|
186
186
|
return (
|
|
187
187
|
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
|
188
|
-
<div className="max-w-lg space-y-4 rounded-lg border
|
|
189
|
-
<h2 className="text-xl font-semibold text-
|
|
188
|
+
<div className="diff-modified max-w-lg space-y-4 rounded-lg border p-6 text-center">
|
|
189
|
+
<h2 className="text-xl font-semibold text-diff-modified">
|
|
190
190
|
Convex Configuration Required
|
|
191
191
|
</h2>
|
|
192
|
-
<p className="text-sm text-
|
|
192
|
+
<p className="text-sm text-diff-modified-foreground">
|
|
193
193
|
Please provide a Convex deployment URL to connect to your backend.
|
|
194
194
|
</p>
|
|
195
|
-
<div className="space-y-2 text-left text-sm text-
|
|
195
|
+
<div className="space-y-2 text-left text-sm text-diff-modified-foreground">
|
|
196
196
|
<p className="font-medium">Options:</p>
|
|
197
197
|
<ul className="list-inside list-disc space-y-1">
|
|
198
198
|
<li>
|
|
199
199
|
Run with URL:{" "}
|
|
200
|
-
<code className="rounded bg-
|
|
200
|
+
<code className="rounded bg-diff-modified-bg/50 px-1">
|
|
201
201
|
npx convex-cms admin --url YOUR_URL
|
|
202
202
|
</code>
|
|
203
203
|
</li>
|
|
204
204
|
<li>
|
|
205
205
|
Set environment variable:{" "}
|
|
206
|
-
<code className="rounded bg-
|
|
206
|
+
<code className="rounded bg-diff-modified-bg/50 px-1">
|
|
207
207
|
CONVEX_URL=YOUR_URL
|
|
208
208
|
</code>
|
|
209
209
|
</li>
|
|
210
210
|
<li>
|
|
211
211
|
Add to{" "}
|
|
212
|
-
<code className="rounded bg-
|
|
213
|
-
<code className="rounded bg-
|
|
212
|
+
<code className="rounded bg-diff-modified-bg/50 px-1">.env.local</code>:{" "}
|
|
213
|
+
<code className="rounded bg-diff-modified-bg/50 px-1">
|
|
214
214
|
CONVEX_URL=YOUR_URL
|
|
215
215
|
</code>
|
|
216
216
|
</li>
|
|
217
217
|
</ul>
|
|
218
218
|
</div>
|
|
219
|
-
<p className="text-sm text-
|
|
219
|
+
<p className="text-sm text-diff-modified-foreground">
|
|
220
220
|
Run{" "}
|
|
221
|
-
<code className="rounded bg-
|
|
221
|
+
<code className="rounded bg-diff-modified-bg/50 px-1">npx convex dev</code> to
|
|
222
222
|
start Convex and get your URL.
|
|
223
223
|
</p>
|
|
224
224
|
</div>
|
|
@@ -21,21 +21,47 @@
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
@layer utilities {
|
|
24
|
-
/* Status badge colors */
|
|
24
|
+
/* Status badge colors - using semantic variables */
|
|
25
25
|
.status-draft {
|
|
26
|
-
@apply bg-
|
|
26
|
+
@apply bg-muted text-muted-foreground;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.status-published {
|
|
30
|
-
@apply bg-
|
|
30
|
+
@apply bg-diff-added-bg text-diff-added-foreground;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
.status-scheduled {
|
|
34
|
-
@apply bg-
|
|
34
|
+
@apply bg-info-bg text-info-foreground;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.status-archived {
|
|
38
|
-
@apply bg-
|
|
38
|
+
@apply bg-diff-modified-bg text-diff-modified-foreground;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Diff change indicators */
|
|
42
|
+
.diff-added {
|
|
43
|
+
@apply bg-diff-added-bg border-diff-added-border text-diff-added-foreground;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.diff-removed {
|
|
47
|
+
@apply bg-diff-removed-bg border-diff-removed-border text-diff-removed-foreground;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.diff-modified {
|
|
51
|
+
@apply bg-diff-modified-bg border-diff-modified-border text-diff-modified-foreground;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Icon badges for diff */
|
|
55
|
+
.diff-icon-added {
|
|
56
|
+
@apply bg-diff-added/20 text-diff-added;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.diff-icon-removed {
|
|
60
|
+
@apply bg-diff-removed/20 text-diff-removed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.diff-icon-modified {
|
|
64
|
+
@apply bg-diff-modified/20 text-diff-modified;
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
/* Surface elevations */
|
|
@@ -71,4 +71,29 @@
|
|
|
71
71
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
72
72
|
--color-sidebar-border: var(--sidebar-border);
|
|
73
73
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
74
|
+
|
|
75
|
+
/* Diff colors */
|
|
76
|
+
--color-diff-added: var(--diff-added);
|
|
77
|
+
--color-diff-added-bg: var(--diff-added-bg);
|
|
78
|
+
--color-diff-added-border: var(--diff-added-border);
|
|
79
|
+
--color-diff-added-foreground: var(--diff-added-foreground);
|
|
80
|
+
|
|
81
|
+
--color-diff-removed: var(--diff-removed);
|
|
82
|
+
--color-diff-removed-bg: var(--diff-removed-bg);
|
|
83
|
+
--color-diff-removed-border: var(--diff-removed-border);
|
|
84
|
+
--color-diff-removed-foreground: var(--diff-removed-foreground);
|
|
85
|
+
|
|
86
|
+
--color-diff-modified: var(--diff-modified);
|
|
87
|
+
--color-diff-modified-bg: var(--diff-modified-bg);
|
|
88
|
+
--color-diff-modified-border: var(--diff-modified-border);
|
|
89
|
+
--color-diff-modified-foreground: var(--diff-modified-foreground);
|
|
90
|
+
|
|
91
|
+
/* Semantic states */
|
|
92
|
+
--color-success: var(--success);
|
|
93
|
+
--color-success-foreground: var(--success-foreground);
|
|
94
|
+
--color-warning: var(--warning);
|
|
95
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
96
|
+
--color-info: var(--info);
|
|
97
|
+
--color-info-bg: var(--info-bg);
|
|
98
|
+
--color-info-foreground: var(--info-foreground);
|
|
74
99
|
}
|