@cedros/data-react 0.1.0
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/CHANGELOG.md +24 -0
- package/README.md +177 -0
- package/dist/admin/api.d.ts +19 -0
- package/dist/admin/api.js +108 -0
- package/dist/admin/components.d.ts +36 -0
- package/dist/admin/components.js +22 -0
- package/dist/admin/history.d.ts +17 -0
- package/dist/admin/history.js +103 -0
- package/dist/admin/icons.d.ts +15 -0
- package/dist/admin/icons.js +18 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/admin/index.js +12 -0
- package/dist/admin/permissions.d.ts +4 -0
- package/dist/admin/permissions.js +45 -0
- package/dist/admin/plugin.d.ts +4 -0
- package/dist/admin/plugin.js +180 -0
- package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
- package/dist/admin/primitives/ConfirmDialog.js +7 -0
- package/dist/admin/primitives/DataTable.d.ts +14 -0
- package/dist/admin/primitives/DataTable.js +7 -0
- package/dist/admin/primitives/DiffViewer.d.ts +11 -0
- package/dist/admin/primitives/DiffViewer.js +8 -0
- package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
- package/dist/admin/primitives/FormFieldRow.js +16 -0
- package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
- package/dist/admin/primitives/JsonCodeEditor.js +42 -0
- package/dist/admin/primitives/Pagination.d.ts +8 -0
- package/dist/admin/primitives/Pagination.js +8 -0
- package/dist/admin/primitives/Toolbar.d.ts +23 -0
- package/dist/admin/primitives/Toolbar.js +10 -0
- package/dist/admin/primitives/alerts.d.ts +21 -0
- package/dist/admin/primitives/alerts.js +44 -0
- package/dist/admin/sectionIds.d.ts +20 -0
- package/dist/admin/sectionIds.js +33 -0
- package/dist/admin/sections/CollectionsSection.d.ts +2 -0
- package/dist/admin/sections/CollectionsSection.js +125 -0
- package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
- package/dist/admin/sections/ContractVerifySection.js +98 -0
- package/dist/admin/sections/CustomDataSection.d.ts +2 -0
- package/dist/admin/sections/CustomDataSection.js +256 -0
- package/dist/admin/sections/DataOpsSection.d.ts +26 -0
- package/dist/admin/sections/DataOpsSection.js +245 -0
- package/dist/admin/sections/HistorySection.d.ts +2 -0
- package/dist/admin/sections/HistorySection.js +26 -0
- package/dist/admin/sections/MonetizationSection.d.ts +2 -0
- package/dist/admin/sections/MonetizationSection.js +140 -0
- package/dist/admin/sections/NavigationSection.d.ts +13 -0
- package/dist/admin/sections/NavigationSection.js +195 -0
- package/dist/admin/sections/PagesSection.d.ts +2 -0
- package/dist/admin/sections/PagesSection.js +157 -0
- package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
- package/dist/admin/sections/SchemaDesignerSection.js +167 -0
- package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
- package/dist/admin/sections/SiteSettingsSection.js +122 -0
- package/dist/admin/sections/TippingSection.d.ts +2 -0
- package/dist/admin/sections/TippingSection.js +178 -0
- package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
- package/dist/admin/sections/media/MediaDetail.js +74 -0
- package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
- package/dist/admin/sections/media/MediaGrid.js +22 -0
- package/dist/admin/sections/media/MediaSection.d.ts +2 -0
- package/dist/admin/sections/media/MediaSection.js +97 -0
- package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
- package/dist/admin/sections/media/MediaUploader.js +72 -0
- package/dist/admin/sections/media/types.d.ts +33 -0
- package/dist/admin/sections/media/types.js +1 -0
- package/dist/admin/styles.css +533 -0
- package/dist/admin/types.d.ts +85 -0
- package/dist/admin/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/react/CmsContent.d.ts +20 -0
- package/dist/react/CmsContent.js +31 -0
- package/dist/react/entries.d.ts +9 -0
- package/dist/react/entries.js +25 -0
- package/dist/react/fetch.d.ts +11 -0
- package/dist/react/fetch.js +32 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +9 -0
- package/dist/react/metadata.d.ts +44 -0
- package/dist/react/metadata.js +142 -0
- package/dist/react/sanitize.d.ts +17 -0
- package/dist/react/sanitize.js +326 -0
- package/dist/react/server.d.ts +14 -0
- package/dist/react/server.js +13 -0
- package/dist/react/sitemap.d.ts +28 -0
- package/dist/react/sitemap.js +91 -0
- package/dist/react/slugs.d.ts +27 -0
- package/dist/react/slugs.js +52 -0
- package/dist/react/types.d.ts +85 -0
- package/dist/react/types.js +1 -0
- package/dist/react/visitor.d.ts +7 -0
- package/dist/react/visitor.js +18 -0
- package/dist/site-templates/BlogTemplates.d.ts +95 -0
- package/dist/site-templates/BlogTemplates.js +64 -0
- package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
- package/dist/site-templates/ContactPageTemplate.js +5 -0
- package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
- package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
- package/dist/site-templates/DashboardShell.d.ts +28 -0
- package/dist/site-templates/DashboardShell.js +10 -0
- package/dist/site-templates/DocsSidebar.d.ts +14 -0
- package/dist/site-templates/DocsSidebar.js +13 -0
- package/dist/site-templates/DocsTemplates.d.ts +60 -0
- package/dist/site-templates/DocsTemplates.js +47 -0
- package/dist/site-templates/HomePageTemplate.d.ts +15 -0
- package/dist/site-templates/HomePageTemplate.js +10 -0
- package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
- package/dist/site-templates/LegalPageTemplate.js +6 -0
- package/dist/site-templates/MarkdownContent.d.ts +7 -0
- package/dist/site-templates/MarkdownContent.js +24 -0
- package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
- package/dist/site-templates/NotFoundTemplate.js +5 -0
- package/dist/site-templates/SiteFooter.d.ts +13 -0
- package/dist/site-templates/SiteFooter.js +4 -0
- package/dist/site-templates/SiteLayout.d.ts +14 -0
- package/dist/site-templates/SiteLayout.js +6 -0
- package/dist/site-templates/TopNav.d.ts +10 -0
- package/dist/site-templates/TopNav.js +8 -0
- package/dist/site-templates/blogControls.d.ts +19 -0
- package/dist/site-templates/blogControls.js +37 -0
- package/dist/site-templates/codeBlock.d.ts +9 -0
- package/dist/site-templates/codeBlock.js +31 -0
- package/dist/site-templates/content-styles.css +410 -0
- package/dist/site-templates/contentIndex.d.ts +65 -0
- package/dist/site-templates/contentIndex.js +181 -0
- package/dist/site-templates/contentUi.d.ts +14 -0
- package/dist/site-templates/contentUi.js +24 -0
- package/dist/site-templates/docs-styles.css +259 -0
- package/dist/site-templates/docsNavigation.d.ts +18 -0
- package/dist/site-templates/docsNavigation.js +50 -0
- package/dist/site-templates/index.d.ts +28 -0
- package/dist/site-templates/index.js +25 -0
- package/dist/site-templates/monetization-styles.css +154 -0
- package/dist/site-templates/paywallControls.d.ts +22 -0
- package/dist/site-templates/paywallControls.js +9 -0
- package/dist/site-templates/routing.d.ts +12 -0
- package/dist/site-templates/routing.js +36 -0
- package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
- package/dist/site-templates/solanaAtaSetup.js +38 -0
- package/dist/site-templates/solanaMicropayments.d.ts +65 -0
- package/dist/site-templates/solanaMicropayments.js +115 -0
- package/dist/site-templates/styles.css +332 -0
- package/dist/site-templates/tipControls.d.ts +24 -0
- package/dist/site-templates/tipControls.js +43 -0
- package/dist/site-templates/tocExtractor.d.ts +16 -0
- package/dist/site-templates/tocExtractor.js +58 -0
- package/dist/site-templates/tocScrollSpy.d.ts +16 -0
- package/dist/site-templates/tocScrollSpy.js +37 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +20 -0
- package/package.json +58 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useDeferredValue, useMemo, useState } from "react";
|
|
3
|
+
import { requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, StatusNotice } from "../components.js";
|
|
5
|
+
import { appendDataOpsHistory } from "../history.js";
|
|
6
|
+
export default function DataOpsSection({ pluginContext }) {
|
|
7
|
+
const canWriteOps = pluginContext.hasPermission("data:ops:write");
|
|
8
|
+
const [status, setStatus] = useState("");
|
|
9
|
+
const [statusTone, setStatusTone] = useState("neutral");
|
|
10
|
+
const [importPayload, setImportPayload] = useState("");
|
|
11
|
+
const [overwriteContracts, setOverwriteContracts] = useState(true);
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const deferredImportPayload = useDeferredValue(importPayload);
|
|
14
|
+
const parsedImport = useMemo(() => parseSiteExport(deferredImportPayload), [deferredImportPayload]);
|
|
15
|
+
const importParsePending = isImportPayloadPending(importPayload, deferredImportPayload);
|
|
16
|
+
const importBlocked = loading ||
|
|
17
|
+
importPayload.trim().length === 0 ||
|
|
18
|
+
(!importParsePending && !!parsedImport.error) ||
|
|
19
|
+
!canWriteOps;
|
|
20
|
+
const runBootstrap = useCallback(async () => {
|
|
21
|
+
if (!canWriteOps) {
|
|
22
|
+
setStatus("You do not have permission to run data operations.");
|
|
23
|
+
setStatusTone("error");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setStatus("");
|
|
28
|
+
setStatusTone("neutral");
|
|
29
|
+
try {
|
|
30
|
+
await requestJson(pluginContext, "/admin/bootstrap", {
|
|
31
|
+
method: "POST"
|
|
32
|
+
});
|
|
33
|
+
const historyResult = appendDataOpsHistory({
|
|
34
|
+
operation: "bootstrap",
|
|
35
|
+
status: "success",
|
|
36
|
+
message: "Defaults initialized."
|
|
37
|
+
});
|
|
38
|
+
setStatus(withHistoryWarning("Defaults initialized.", historyResult.warning));
|
|
39
|
+
setStatusTone("success");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const historyResult = appendDataOpsHistory({
|
|
43
|
+
operation: "bootstrap",
|
|
44
|
+
status: "error",
|
|
45
|
+
message: error.message
|
|
46
|
+
});
|
|
47
|
+
setStatus(withHistoryWarning(error.message, historyResult.warning));
|
|
48
|
+
setStatusTone("error");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, [canWriteOps, pluginContext]);
|
|
54
|
+
const exportSite = useCallback(async () => {
|
|
55
|
+
if (!canWriteOps) {
|
|
56
|
+
setStatus("You do not have permission to run data operations.");
|
|
57
|
+
setStatusTone("error");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setStatus("");
|
|
62
|
+
setStatusTone("neutral");
|
|
63
|
+
try {
|
|
64
|
+
const payload = await requestJson(pluginContext, "/site/export");
|
|
65
|
+
const payloadJson = JSON.stringify(payload, null, 2);
|
|
66
|
+
setImportPayload(payloadJson);
|
|
67
|
+
const historyResult = appendDataOpsHistory({
|
|
68
|
+
operation: "export",
|
|
69
|
+
status: "success",
|
|
70
|
+
message: "Site export generated.",
|
|
71
|
+
artifact: formatHistoryArtifact("export", summarizeSiteExport(payload))
|
|
72
|
+
});
|
|
73
|
+
setStatus(withHistoryWarning("Export loaded into editor.", historyResult.warning));
|
|
74
|
+
setStatusTone("success");
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const historyResult = appendDataOpsHistory({
|
|
78
|
+
operation: "export",
|
|
79
|
+
status: "error",
|
|
80
|
+
message: error.message
|
|
81
|
+
});
|
|
82
|
+
setStatus(withHistoryWarning(error.message, historyResult.warning));
|
|
83
|
+
setStatusTone("error");
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, [canWriteOps, pluginContext]);
|
|
89
|
+
const importSite = useCallback(async () => {
|
|
90
|
+
if (!canWriteOps) {
|
|
91
|
+
setStatus("You do not have permission to run data operations.");
|
|
92
|
+
setStatusTone("error");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const currentImport = resolveImportPayload(importPayload, deferredImportPayload, parsedImport);
|
|
96
|
+
if (currentImport.error) {
|
|
97
|
+
setStatus(currentImport.error);
|
|
98
|
+
setStatusTone("error");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!currentImport.value) {
|
|
102
|
+
setStatus("Import payload is required.");
|
|
103
|
+
setStatusTone("error");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setLoading(true);
|
|
107
|
+
setStatus("");
|
|
108
|
+
setStatusTone("neutral");
|
|
109
|
+
try {
|
|
110
|
+
await requestJson(pluginContext, "/import", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: {
|
|
113
|
+
export: currentImport.value,
|
|
114
|
+
overwrite_contracts: overwriteContracts
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const historyResult = appendDataOpsHistory({
|
|
118
|
+
operation: "import",
|
|
119
|
+
status: "success",
|
|
120
|
+
message: "Import complete.",
|
|
121
|
+
artifact: currentImport.summary === null
|
|
122
|
+
? undefined
|
|
123
|
+
: formatHistoryArtifact("import", currentImport.summary, overwriteContracts)
|
|
124
|
+
});
|
|
125
|
+
const successMessage = overwriteContracts
|
|
126
|
+
? "Import complete. Contracts were overwritten from payload."
|
|
127
|
+
: "Import complete. Existing strict contracts and contract history were preserved.";
|
|
128
|
+
setStatus(withHistoryWarning(successMessage, historyResult.warning));
|
|
129
|
+
setStatusTone("success");
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const historyResult = appendDataOpsHistory({
|
|
133
|
+
operation: "import",
|
|
134
|
+
status: "error",
|
|
135
|
+
message: error.message,
|
|
136
|
+
artifact: currentImport.summary === null
|
|
137
|
+
? undefined
|
|
138
|
+
: formatHistoryArtifact("import", currentImport.summary, overwriteContracts)
|
|
139
|
+
});
|
|
140
|
+
setStatus(withHistoryWarning(error.message, historyResult.warning));
|
|
141
|
+
setStatusTone("error");
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
setLoading(false);
|
|
145
|
+
}
|
|
146
|
+
}, [
|
|
147
|
+
canWriteOps,
|
|
148
|
+
deferredImportPayload,
|
|
149
|
+
importPayload,
|
|
150
|
+
overwriteContracts,
|
|
151
|
+
parsedImport,
|
|
152
|
+
pluginContext
|
|
153
|
+
]);
|
|
154
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Data Operations" }), _jsx("p", { className: "cedros-data__subtitle", children: "Bootstrap defaults and run import/export for this deployment." }), !canWriteOps && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:ops:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void runBootstrap(), disabled: loading || !canWriteOps, title: !canWriteOps ? "Requires data:ops:write" : undefined, children: "Initialize Defaults" }), _jsx(AdminButton, { variant: "secondary", onClick: () => void exportSite(), disabled: loading || !canWriteOps, title: !canWriteOps ? "Requires data:ops:write" : undefined, children: "Export Site" }), _jsx(AdminButton, { variant: "primary", onClick: () => void importSite(), disabled: importBlocked, title: !canWriteOps ? "Requires data:ops:write" : undefined, children: "Import JSON" })] })] }), _jsx(Card, { title: "Import / Export Payload", subtitle: "Review exported JSON or paste a payload to import.", children: _jsx(JsonEditor, { label: "SiteExport JSON", value: importPayload, onChange: (event) => setImportPayload(event.target.value), placeholder: "Paste SiteExport JSON here to import", disabled: !canWriteOps }) }), _jsxs(Card, { title: "Import Options", subtitle: "Choose safe import behavior before applying payloads.", children: [_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: overwriteContracts, onChange: (event) => setOverwriteContracts(event.target.checked), disabled: loading || !canWriteOps }), _jsx("span", { children: "Overwrite stored strict contracts and contract history from import payload" })] }), _jsx("div", { className: "cedros-data-key", children: importParsePending ? ("Parsing payload") : parsedImport.summary ? (_jsxs(_Fragment, { children: ["Payload summary", parsedImport.summary.displayName
|
|
155
|
+
? ` • deployment: ${parsedImport.summary.displayName}`
|
|
156
|
+
: "", " ", "\u2022 collections: ", parsedImport.summary.collections, " ", "\u2022 contracts: ", parsedImport.summary.contracts, " ", "\u2022 entries: ", parsedImport.summary.entries, " ", "\u2022 custom schema: ", parsedImport.summary.hasCustomSchema ? "yes" : "no"] })) : ("Payload not parsed") }), !importParsePending && parsedImport.error && (_jsx(StatusNotice, { tone: "error", message: parsedImport.error }))] }), status && _jsx(StatusNotice, { tone: statusTone, message: status })] }));
|
|
157
|
+
}
|
|
158
|
+
export function parseSiteExport(raw) {
|
|
159
|
+
if (!raw.trim()) {
|
|
160
|
+
return { value: null, summary: null, error: null };
|
|
161
|
+
}
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return { value: null, summary: null, error: `Invalid import JSON: ${error.message}` };
|
|
168
|
+
}
|
|
169
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
170
|
+
return {
|
|
171
|
+
value: null,
|
|
172
|
+
summary: null,
|
|
173
|
+
error: "Invalid import JSON: root value must be an object matching SiteExport."
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const value = parsed;
|
|
177
|
+
return {
|
|
178
|
+
value,
|
|
179
|
+
summary: {
|
|
180
|
+
displayName: readDisplayName(value.site),
|
|
181
|
+
collections: Array.isArray(value.collections) ? value.collections.length : 0,
|
|
182
|
+
contracts: Array.isArray(value.contracts) ? value.contracts.length : 0,
|
|
183
|
+
entries: countEntries(value.entries),
|
|
184
|
+
hasCustomSchema: value.custom_schema !== null && value.custom_schema !== undefined
|
|
185
|
+
},
|
|
186
|
+
error: null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function isImportPayloadPending(raw, deferredRaw) {
|
|
190
|
+
return raw.trim().length > 0 && raw !== deferredRaw;
|
|
191
|
+
}
|
|
192
|
+
export function resolveImportPayload(raw, deferredRaw, deferredParsed) {
|
|
193
|
+
if (!isImportPayloadPending(raw, deferredRaw)) {
|
|
194
|
+
return deferredParsed;
|
|
195
|
+
}
|
|
196
|
+
return parseSiteExport(raw);
|
|
197
|
+
}
|
|
198
|
+
function readDisplayName(site) {
|
|
199
|
+
if (!site || typeof site !== "object" || Array.isArray(site)) {
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
const displayName = site.display_name;
|
|
203
|
+
return typeof displayName === "string" ? displayName : "";
|
|
204
|
+
}
|
|
205
|
+
function countEntries(entries) {
|
|
206
|
+
if (!Array.isArray(entries)) {
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
return entries.reduce((total, item) => {
|
|
210
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
211
|
+
return total;
|
|
212
|
+
}
|
|
213
|
+
const entryList = item.entries;
|
|
214
|
+
if (!Array.isArray(entryList)) {
|
|
215
|
+
return total;
|
|
216
|
+
}
|
|
217
|
+
return total + entryList.length;
|
|
218
|
+
}, 0);
|
|
219
|
+
}
|
|
220
|
+
function summarizeSiteExport(payload) {
|
|
221
|
+
return {
|
|
222
|
+
displayName: readDisplayName(payload.site),
|
|
223
|
+
collections: Array.isArray(payload.collections) ? payload.collections.length : 0,
|
|
224
|
+
contracts: Array.isArray(payload.contracts) ? payload.contracts.length : 0,
|
|
225
|
+
entries: countEntries(payload.entries),
|
|
226
|
+
hasCustomSchema: payload.custom_schema !== null && payload.custom_schema !== undefined
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function withHistoryWarning(message, warning) {
|
|
230
|
+
if (!warning) {
|
|
231
|
+
return message;
|
|
232
|
+
}
|
|
233
|
+
return `${message} ${warning}`;
|
|
234
|
+
}
|
|
235
|
+
export function formatHistoryArtifact(operation, summary, overwriteContracts) {
|
|
236
|
+
return JSON.stringify({
|
|
237
|
+
operation,
|
|
238
|
+
displayName: summary.displayName || undefined,
|
|
239
|
+
collections: summary.collections,
|
|
240
|
+
contracts: summary.contracts,
|
|
241
|
+
entries: summary.entries,
|
|
242
|
+
hasCustomSchema: summary.hasCustomSchema,
|
|
243
|
+
...(operation === "import" ? { overwriteContracts: overwriteContracts === true } : {})
|
|
244
|
+
}, null, 2);
|
|
245
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { AdminButton, Card, JsonEditor, StatusNotice } from "../components.js";
|
|
4
|
+
import { clearDataOpsHistory, readDataOpsHistory } from "../history.js";
|
|
5
|
+
export default function HistorySection({ pluginContext }) {
|
|
6
|
+
const [records, setRecords] = useState([]);
|
|
7
|
+
const [activeId, setActiveId] = useState("");
|
|
8
|
+
const [status, setStatus] = useState("");
|
|
9
|
+
const [tone, setTone] = useState("neutral");
|
|
10
|
+
const load = useCallback(() => {
|
|
11
|
+
const history = readDataOpsHistory();
|
|
12
|
+
setRecords(history);
|
|
13
|
+
setActiveId((current) => current || history[0]?.id || "");
|
|
14
|
+
}, []);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
load();
|
|
17
|
+
}, [load]);
|
|
18
|
+
const active = useMemo(() => records.find((record) => record.id === activeId) ?? null, [activeId, records]);
|
|
19
|
+
const clear = useCallback(() => {
|
|
20
|
+
const result = clearDataOpsHistory();
|
|
21
|
+
load();
|
|
22
|
+
setStatus(result.warning ? `History cleared. ${result.warning}` : "History cleared.");
|
|
23
|
+
setTone("success");
|
|
24
|
+
}, [load]);
|
|
25
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Import / Export History" }), _jsx("p", { className: "cedros-data__subtitle", children: "Local operation history for this deployment." })] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: load, children: "Refresh" }), _jsx(AdminButton, { variant: "danger", onClick: clear, children: "Clear History" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--page", children: [_jsx(Card, { title: `Operations (${records.length})`, subtitle: "Newest first.", children: _jsxs("div", { className: "cedros-data-list", children: [records.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "No history records yet." }), records.map((record) => (_jsxs("button", { className: `cedros-data-list-item ${record.id === activeId ? "cedros-data-list-item--active" : ""}`, onClick: () => setActiveId(record.id), children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: "0.5rem" }, children: [_jsx("strong", { children: record.operation }), _jsx("span", { className: "cedros-data-pill", children: record.status })] }), _jsx("div", { className: "cedros-data-key", children: new Date(record.createdAt).toLocaleString() }), _jsx("div", { className: "cedros-data-key", children: record.message })] }, record.id)))] }) }), _jsx(Card, { title: "Artifact", subtitle: "Stored operation summary; full payloads are not persisted in local history.", children: active ? (_jsx(JsonEditor, { label: `${active.operation} artifact`, value: active.artifact || "// No summary captured for this operation.", readOnly: true })) : (_jsx("div", { className: "cedros-data-empty", children: "Select an operation to inspect." })) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, SelectInput, StatusNotice, TextInput } from "../components.js";
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
enabled: false,
|
|
7
|
+
defaultAccessMode: "free",
|
|
8
|
+
defaultPreviewParagraphs: 3,
|
|
9
|
+
subscriptionProductId: "",
|
|
10
|
+
creditsPerArticle: 0,
|
|
11
|
+
freeReadsPerMonth: 0
|
|
12
|
+
};
|
|
13
|
+
export default function MonetizationSection({ pluginContext }) {
|
|
14
|
+
const canWrite = pluginContext.hasPermission("data:settings:write");
|
|
15
|
+
const [payload, setPayload] = useState({ ...DEFAULT_CONFIG });
|
|
16
|
+
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
|
17
|
+
const [rawEditor, setRawEditor] = useState(JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
18
|
+
const [status, setStatus] = useState("");
|
|
19
|
+
const [tone, setTone] = useState("neutral");
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const load = useCallback(async () => {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setStatus("");
|
|
24
|
+
setTone("neutral");
|
|
25
|
+
try {
|
|
26
|
+
const entries = await requestJson(pluginContext, "/entries/query", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: {
|
|
29
|
+
collection_name: "site_settings",
|
|
30
|
+
entry_keys: ["monetization"],
|
|
31
|
+
contains: null,
|
|
32
|
+
limit: 1,
|
|
33
|
+
offset: 0
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const raw = normalizePayload(entries[0]?.payload ?? DEFAULT_CONFIG);
|
|
37
|
+
const typed = toConfig(raw);
|
|
38
|
+
setPayload(raw);
|
|
39
|
+
setConfig(typed);
|
|
40
|
+
setRawEditor(JSON.stringify(raw, null, 2));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
setStatus(error.message);
|
|
44
|
+
setTone("error");
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
}, [pluginContext]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
void load();
|
|
52
|
+
}, [load]);
|
|
53
|
+
const save = useCallback(async (nextPayload) => {
|
|
54
|
+
if (!canWrite) {
|
|
55
|
+
setStatus("You do not have permission to edit monetization settings.");
|
|
56
|
+
setTone("error");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setLoading(true);
|
|
60
|
+
setStatus("");
|
|
61
|
+
setTone("neutral");
|
|
62
|
+
try {
|
|
63
|
+
await requestJson(pluginContext, "/entries/upsert", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: {
|
|
66
|
+
collection_name: "site_settings",
|
|
67
|
+
entry_key: "monetization",
|
|
68
|
+
payload: nextPayload
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
setPayload(nextPayload);
|
|
72
|
+
setConfig(toConfig(nextPayload));
|
|
73
|
+
setRawEditor(JSON.stringify(nextPayload, null, 2));
|
|
74
|
+
setStatus("Monetization settings saved.");
|
|
75
|
+
setTone("success");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
setStatus(error.message);
|
|
79
|
+
setTone("error");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
}, [canWrite, pluginContext]);
|
|
85
|
+
const saveForm = useCallback(async () => {
|
|
86
|
+
await save(mergeConfigIntoPayload(payload, config));
|
|
87
|
+
}, [save, config, payload]);
|
|
88
|
+
const applyRaw = useCallback(async () => {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(rawEditor);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
setStatus(`Invalid JSON: ${error.message}`);
|
|
95
|
+
setTone("error");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await save(normalizePayload(parsed));
|
|
99
|
+
}, [rawEditor, save]);
|
|
100
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Monetization" }), _jsx("p", { className: "cedros-data__subtitle", children: "Configure content monetization and paywall defaults." }), !canWrite && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:settings:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void saveForm(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save Form" }), _jsx(AdminButton, { variant: "secondary", onClick: () => void applyRaw(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save JSON" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Monetization Settings", subtitle: "Default content access and pricing.", children: [_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig((prev) => ({ ...prev, enabled: e.target.checked })), disabled: !canWrite }), "Enabled"] }), _jsxs(SelectInput, { label: "Default Access Mode", value: config.defaultAccessMode, onChange: (e) => setConfig((prev) => ({ ...prev, defaultAccessMode: e.target.value })), disabled: !canWrite, children: [_jsx("option", { value: "free", children: "Free" }), _jsx("option", { value: "preview", children: "Preview" }), _jsx("option", { value: "locked", children: "Locked" })] }), _jsx(TextInput, { label: "Preview Paragraphs", type: "number", value: String(config.defaultPreviewParagraphs), onChange: (e) => setConfig((prev) => ({
|
|
101
|
+
...prev,
|
|
102
|
+
defaultPreviewParagraphs: parseInt(e.target.value, 10) || 3
|
|
103
|
+
})), placeholder: "3", disabled: !canWrite }), _jsx(TextInput, { label: "Subscription Product ID", value: config.subscriptionProductId, onChange: (e) => setConfig((prev) => ({ ...prev, subscriptionProductId: e.target.value })), placeholder: "product_xxx", disabled: !canWrite }), _jsx(TextInput, { label: "Credits per Article", type: "number", value: String(config.creditsPerArticle), onChange: (e) => setConfig((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
creditsPerArticle: parseInt(e.target.value, 10) || 0
|
|
106
|
+
})), placeholder: "0", disabled: !canWrite }), _jsx(TextInput, { label: "Free Reads per Month", type: "number", value: String(config.freeReadsPerMonth), onChange: (e) => setConfig((prev) => ({
|
|
107
|
+
...prev,
|
|
108
|
+
freeReadsPerMonth: Math.max(0, parseInt(e.target.value, 10) || 0)
|
|
109
|
+
})), placeholder: "0", disabled: !canWrite }), _jsx("p", { className: "cedros-data__subtitle", style: { margin: 0 }, children: "Number of articles readers can view for free each month before the paywall activates. Set to 0 to disable." })] }), _jsx(Card, { title: "Monetization JSON", subtitle: "Advanced editor for pricing, payment methods, and more.", children: _jsx(JsonEditor, { label: "site_settings/monetization", value: rawEditor, onChange: (e) => setRawEditor(e.target.value), disabled: !canWrite }) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
110
|
+
}
|
|
111
|
+
function normalizePayload(value) {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
113
|
+
return { ...DEFAULT_CONFIG };
|
|
114
|
+
}
|
|
115
|
+
return { ...value };
|
|
116
|
+
}
|
|
117
|
+
function mergeConfigIntoPayload(payload, config) {
|
|
118
|
+
return { ...normalizePayload(payload), ...config };
|
|
119
|
+
}
|
|
120
|
+
function toConfig(value) {
|
|
121
|
+
const source = normalizePayload(value);
|
|
122
|
+
return {
|
|
123
|
+
enabled: typeof source.enabled === "boolean" ? source.enabled : DEFAULT_CONFIG.enabled,
|
|
124
|
+
defaultAccessMode: typeof source.defaultAccessMode === "string" && source.defaultAccessMode
|
|
125
|
+
? source.defaultAccessMode
|
|
126
|
+
: DEFAULT_CONFIG.defaultAccessMode,
|
|
127
|
+
defaultPreviewParagraphs: typeof source.defaultPreviewParagraphs === "number"
|
|
128
|
+
? source.defaultPreviewParagraphs
|
|
129
|
+
: DEFAULT_CONFIG.defaultPreviewParagraphs,
|
|
130
|
+
subscriptionProductId: typeof source.subscriptionProductId === "string"
|
|
131
|
+
? source.subscriptionProductId
|
|
132
|
+
: DEFAULT_CONFIG.subscriptionProductId,
|
|
133
|
+
creditsPerArticle: typeof source.creditsPerArticle === "number"
|
|
134
|
+
? source.creditsPerArticle
|
|
135
|
+
: DEFAULT_CONFIG.creditsPerArticle,
|
|
136
|
+
freeReadsPerMonth: typeof source.freeReadsPerMonth === "number"
|
|
137
|
+
? source.freeReadsPerMonth
|
|
138
|
+
: DEFAULT_CONFIG.freeReadsPerMonth
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AdminSectionProps } from "../types.js";
|
|
2
|
+
interface NavigationItem {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
route: string;
|
|
6
|
+
}
|
|
7
|
+
type NavigationPayload = Record<string, unknown>;
|
|
8
|
+
export default function NavigationSection({ pluginContext }: AdminSectionProps): React.JSX.Element;
|
|
9
|
+
export declare function normalizeNavigationPayload(value: unknown): NavigationPayload;
|
|
10
|
+
export declare function addNavigationItemToPayload(payload: NavigationPayload, item: NavigationItem): NavigationPayload;
|
|
11
|
+
export declare function removeNavigationItemFromPayload(payload: NavigationPayload, key: string): NavigationPayload;
|
|
12
|
+
export declare function moveNavigationItemInPayload(payload: NavigationPayload, from: number, to: number): NavigationPayload;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, StatusNotice, TextInput } from "../components.js";
|
|
5
|
+
const DEFAULT_NAV = {
|
|
6
|
+
key: "main",
|
|
7
|
+
items: []
|
|
8
|
+
};
|
|
9
|
+
export default function NavigationSection({ pluginContext }) {
|
|
10
|
+
const canWriteNavigation = pluginContext.hasPermission("data:navigation:write");
|
|
11
|
+
const [payload, setPayload] = useState(normalizeNavigationPayload(DEFAULT_NAV));
|
|
12
|
+
const [rawEditor, setRawEditor] = useState(JSON.stringify(DEFAULT_NAV, null, 2));
|
|
13
|
+
const [newLabel, setNewLabel] = useState("");
|
|
14
|
+
const [newRoute, setNewRoute] = useState("");
|
|
15
|
+
const [status, setStatus] = useState("");
|
|
16
|
+
const [tone, setTone] = useState("neutral");
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const record = useMemo(() => toNavigationRecord(payload), [payload]);
|
|
19
|
+
const load = useCallback(async () => {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
setStatus("");
|
|
22
|
+
setTone("neutral");
|
|
23
|
+
try {
|
|
24
|
+
const entries = await requestJson(pluginContext, "/entries/query", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: {
|
|
27
|
+
collection_name: "navigation",
|
|
28
|
+
entry_keys: ["main"],
|
|
29
|
+
contains: null,
|
|
30
|
+
limit: 1,
|
|
31
|
+
offset: 0
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const nextPayload = normalizeNavigationPayload(entries[0]?.payload ?? DEFAULT_NAV);
|
|
35
|
+
setPayload(nextPayload);
|
|
36
|
+
setRawEditor(JSON.stringify(nextPayload, null, 2));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
setStatus(error.message);
|
|
40
|
+
setTone("error");
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}, [pluginContext]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void load();
|
|
48
|
+
}, [load]);
|
|
49
|
+
const save = useCallback(async (nextPayload) => {
|
|
50
|
+
if (!canWriteNavigation) {
|
|
51
|
+
setStatus("You do not have permission to edit navigation.");
|
|
52
|
+
setTone("error");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setLoading(true);
|
|
56
|
+
setStatus("");
|
|
57
|
+
setTone("neutral");
|
|
58
|
+
try {
|
|
59
|
+
await requestJson(pluginContext, "/entries/upsert", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: {
|
|
62
|
+
collection_name: "navigation",
|
|
63
|
+
entry_key: "main",
|
|
64
|
+
payload: nextPayload
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
setPayload(nextPayload);
|
|
68
|
+
setRawEditor(JSON.stringify(nextPayload, null, 2));
|
|
69
|
+
setStatus("Navigation saved.");
|
|
70
|
+
setTone("success");
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
setStatus(error.message);
|
|
74
|
+
setTone("error");
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, [canWriteNavigation, pluginContext]);
|
|
80
|
+
const addItem = useCallback(async () => {
|
|
81
|
+
if (!canWriteNavigation) {
|
|
82
|
+
setStatus("You do not have permission to edit navigation.");
|
|
83
|
+
setTone("error");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const label = newLabel.trim();
|
|
87
|
+
const route = newRoute.trim();
|
|
88
|
+
if (!label || !route) {
|
|
89
|
+
setStatus("Both label and route are required.");
|
|
90
|
+
setTone("error");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const key = `${label.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "item"}-${record.items.length + 1}`;
|
|
94
|
+
await save(addNavigationItemToPayload(payload, { key, label, route }));
|
|
95
|
+
setNewLabel("");
|
|
96
|
+
setNewRoute("");
|
|
97
|
+
}, [canWriteNavigation, newLabel, newRoute, payload, record.items.length, save]);
|
|
98
|
+
const removeItem = useCallback(async (key) => {
|
|
99
|
+
await save(removeNavigationItemFromPayload(payload, key));
|
|
100
|
+
}, [payload, save]);
|
|
101
|
+
const applyRaw = useCallback(async () => {
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(rawEditor);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
setStatus(`Invalid JSON: ${error.message}`);
|
|
108
|
+
setTone("error");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const next = normalizeNavigationPayload(parsed);
|
|
112
|
+
await save(next);
|
|
113
|
+
}, [rawEditor, save]);
|
|
114
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Navigation" }), _jsx("p", { className: "cedros-data__subtitle", children: "Edit the main menu links for this site." }), !canWriteNavigation && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:navigation:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void applyRaw(), disabled: loading || !canWriteNavigation, title: !canWriteNavigation ? "Requires data:navigation:write" : undefined, children: "Save JSON" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Main Menu Items", subtitle: `${record.items.length} item(s)`, children: [_jsxs("div", { className: "cedros-data-actions", children: [_jsx(TextInput, { label: "Label", value: newLabel, onChange: (event) => setNewLabel(event.target.value), placeholder: "Pricing", disabled: !canWriteNavigation }), _jsx(TextInput, { label: "Route", value: newRoute, onChange: (event) => setNewRoute(event.target.value), placeholder: "/pricing", disabled: !canWriteNavigation }), _jsx(AdminButton, { variant: "secondary", onClick: () => void addItem(), disabled: loading || !canWriteNavigation, title: !canWriteNavigation ? "Requires data:navigation:write" : undefined, children: "Add Item" })] }), _jsxs("div", { className: "cedros-data-list", children: [record.items.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "No navigation items." }), record.items.map((item, index) => (_jsxs("div", { className: "cedros-data-list-item cedros-data-list-item--active", children: [_jsx("div", { children: item.label }), _jsx("div", { className: "cedros-data-key", children: item.route }), _jsxs("div", { className: "cedros-data-actions", style: { marginTop: "0.4rem" }, children: [_jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => void save(moveNavigationItemInPayload(payload, index, index - 1)), disabled: loading || !canWriteNavigation || index === 0, children: "Up" }), _jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => void save(moveNavigationItemInPayload(payload, index, index + 1)), disabled: loading || !canWriteNavigation || index === record.items.length - 1, children: "Down" }), _jsx(AdminButton, { variant: "danger", size: "sm", onClick: () => void removeItem(item.key), disabled: loading || !canWriteNavigation, children: "Remove" })] })] }, item.key)))] })] }), _jsx(Card, { title: "Navigation JSON", subtitle: "Advanced editor for full navigation payload.", children: _jsx(JsonEditor, { label: "navigation/main", value: rawEditor, onChange: (event) => setRawEditor(event.target.value), disabled: !canWriteNavigation }) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
115
|
+
}
|
|
116
|
+
export function normalizeNavigationPayload(value) {
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
118
|
+
return {
|
|
119
|
+
key: DEFAULT_NAV.key,
|
|
120
|
+
items: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const source = value;
|
|
124
|
+
return {
|
|
125
|
+
...source,
|
|
126
|
+
items: Array.isArray(source.items)
|
|
127
|
+
? source.items.map((item) => item && typeof item === "object" && !Array.isArray(item)
|
|
128
|
+
? { ...item }
|
|
129
|
+
: item)
|
|
130
|
+
: []
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function addNavigationItemToPayload(payload, item) {
|
|
134
|
+
const next = normalizeNavigationPayload(payload);
|
|
135
|
+
const rawItems = readRawNavigationItems(next);
|
|
136
|
+
return {
|
|
137
|
+
...next,
|
|
138
|
+
key: typeof next.key === "string" ? next.key : DEFAULT_NAV.key,
|
|
139
|
+
items: [...rawItems, { ...item }]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function removeNavigationItemFromPayload(payload, key) {
|
|
143
|
+
const next = normalizeNavigationPayload(payload);
|
|
144
|
+
const rawItems = readRawNavigationItems(next).filter((item, index) => readNavigationItem(item, index).key !== key);
|
|
145
|
+
return {
|
|
146
|
+
...next,
|
|
147
|
+
key: typeof next.key === "string" ? next.key : DEFAULT_NAV.key,
|
|
148
|
+
items: rawItems
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function moveNavigationItemInPayload(payload, from, to) {
|
|
152
|
+
const next = normalizeNavigationPayload(payload);
|
|
153
|
+
const rawItems = readRawNavigationItems(next);
|
|
154
|
+
if (to < 0 || to >= rawItems.length || from === to) {
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
const reordered = [...rawItems];
|
|
158
|
+
const [moved] = reordered.splice(from, 1);
|
|
159
|
+
if (moved === undefined) {
|
|
160
|
+
return next;
|
|
161
|
+
}
|
|
162
|
+
reordered.splice(to, 0, moved);
|
|
163
|
+
return {
|
|
164
|
+
...next,
|
|
165
|
+
key: typeof next.key === "string" ? next.key : DEFAULT_NAV.key,
|
|
166
|
+
items: reordered
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function readRawNavigationItems(payload) {
|
|
170
|
+
return Array.isArray(payload.items) ? [...payload.items] : [];
|
|
171
|
+
}
|
|
172
|
+
function toNavigationRecord(value) {
|
|
173
|
+
const source = normalizeNavigationPayload(value);
|
|
174
|
+
const key = typeof source.key === "string" ? source.key : "main";
|
|
175
|
+
const items = readRawNavigationItems(source).map((item, index) => readNavigationItem(item, index));
|
|
176
|
+
return { key, items };
|
|
177
|
+
}
|
|
178
|
+
function readNavigationItem(value, index) {
|
|
179
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
180
|
+
return {
|
|
181
|
+
key: `item-${index + 1}`,
|
|
182
|
+
label: `Item ${index + 1}`,
|
|
183
|
+
route: "/"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const source = value;
|
|
187
|
+
const label = typeof source.label === "string" ? source.label : `Item ${index + 1}`;
|
|
188
|
+
return {
|
|
189
|
+
key: typeof source.key === "string"
|
|
190
|
+
? source.key
|
|
191
|
+
: `${label.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "item"}-${index + 1}`,
|
|
192
|
+
label,
|
|
193
|
+
route: typeof source.route === "string" ? source.route : "/"
|
|
194
|
+
};
|
|
195
|
+
}
|