@nitrogenbuilder/connector-payload 0.1.39 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/NitrogenComponentInventoryClient.d.ts +2 -1
- package/dist/components/NitrogenComponentInventoryClient.js +370 -63
- package/dist/components/NitrogenComponentInventoryField.js +12 -2
- package/dist/components/NitrogenComponentInventoryView.js +12 -2
- package/dist/endpoints/component-inventory.js +274 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ type InventoryClientProps = {
|
|
|
8
8
|
isUnknown: boolean;
|
|
9
9
|
usages: NitrogenComponentUsageDoc[];
|
|
10
10
|
}>;
|
|
11
|
+
previewUrl: string;
|
|
11
12
|
};
|
|
12
|
-
export default function NitrogenComponentInventoryClient({ adminRoute, components, }: InventoryClientProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export default function NitrogenComponentInventoryClient({ adminRoute, components, previewUrl, }: InventoryClientProps): import("react/jsx-runtime").JSX.Element;
|
|
13
14
|
export {};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
const EDITABLE_PROP_TYPES = new Set([
|
|
6
|
+
'boolean',
|
|
7
|
+
'color',
|
|
8
|
+
'color-select',
|
|
9
|
+
'color-swatch',
|
|
10
|
+
'enum',
|
|
11
|
+
'enum-inline',
|
|
12
|
+
'number',
|
|
13
|
+
'string',
|
|
14
|
+
'url',
|
|
15
|
+
'wysiwyg',
|
|
16
|
+
]);
|
|
5
17
|
function formatSourceLabel(slug) {
|
|
6
18
|
if (slug === 'nitrogen-templates') {
|
|
7
19
|
return 'Nitrogen Templates';
|
|
@@ -12,6 +24,86 @@ function formatSourceLabel(slug) {
|
|
|
12
24
|
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
13
25
|
.join(' ');
|
|
14
26
|
}
|
|
27
|
+
function getPreviewTargetOrigin(previewUrl) {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(previewUrl).origin;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '*';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeManifestOptions(options) {
|
|
36
|
+
if (!options?.length) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return options.map((option) => {
|
|
40
|
+
if (typeof option === 'string') {
|
|
41
|
+
return {
|
|
42
|
+
label: option,
|
|
43
|
+
value: option,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
label: option.label || option.value,
|
|
48
|
+
value: option.value,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function isEditableProp(prop) {
|
|
53
|
+
return EDITABLE_PROP_TYPES.has(prop.type);
|
|
54
|
+
}
|
|
55
|
+
function collectEditablePropOptions(definition) {
|
|
56
|
+
if (!definition) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const options = [];
|
|
60
|
+
Object.values(definition.categories).forEach((category) => {
|
|
61
|
+
Object.values(category.groups).forEach((group) => {
|
|
62
|
+
Object.entries(group.props).forEach(([propName, prop]) => {
|
|
63
|
+
if (!isEditableProp(prop)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
options.push({
|
|
67
|
+
defaultValue: prop.default,
|
|
68
|
+
label: `${category.label} / ${group.label} / ${prop.label || propName}`,
|
|
69
|
+
options: normalizeManifestOptions(prop.options),
|
|
70
|
+
path: `${group.key}.${propName}`,
|
|
71
|
+
type: prop.type,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
return options;
|
|
77
|
+
}
|
|
78
|
+
function stringifyPropValue(value, propType) {
|
|
79
|
+
if (propType === 'boolean') {
|
|
80
|
+
return Boolean(value);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === 'string') {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'number') {
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
function coercePropValue(value, propType) {
|
|
91
|
+
if (propType === 'boolean') {
|
|
92
|
+
return Boolean(value);
|
|
93
|
+
}
|
|
94
|
+
if (propType === 'number') {
|
|
95
|
+
const parsed = Number(value);
|
|
96
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
97
|
+
}
|
|
98
|
+
return String(value);
|
|
99
|
+
}
|
|
100
|
+
function getDefaultDraft(option) {
|
|
101
|
+
return {
|
|
102
|
+
propPath: option?.path || '',
|
|
103
|
+
status: 'idle',
|
|
104
|
+
value: option ? stringifyPropValue(option.defaultValue, option.type) : '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
15
107
|
function renderProp(name, prop, depth = 0) {
|
|
16
108
|
return (_jsxs("div", { style: { paddingLeft: depth * 16 }, children: [_jsxs("div", { style: { fontFamily: 'monospace', fontSize: 12, lineHeight: 1.5 }, children: [_jsx("strong", { children: name }), " ", _jsxs("span", { style: { color: '#6b7280' }, children: ["(", prop.type, ")"] }), prop.label ? _jsxs("span", { style: { color: '#6b7280' }, children: [" - ", prop.label] }) : null] }), prop.props ? (_jsx("div", { style: { marginTop: 4 }, children: Object.entries(prop.props).map(([childName, childProp]) => renderProp(childName, childProp, depth + 1)) })) : null] }, `${name}-${depth}`));
|
|
17
109
|
}
|
|
@@ -34,51 +126,22 @@ function buttonStyle(kind) {
|
|
|
34
126
|
padding: '8px 12px',
|
|
35
127
|
};
|
|
36
128
|
}
|
|
37
|
-
function
|
|
38
|
-
const relationshipMap = new Map();
|
|
39
|
-
usages.forEach((usage) => {
|
|
40
|
-
const relationshipName = getRelationshipName(usage);
|
|
41
|
-
if (!relationshipName) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const entry = relationshipMap.get(relationshipName) ?? {
|
|
45
|
-
documentKeys: new Set(),
|
|
46
|
-
occurrenceCount: 0,
|
|
47
|
-
};
|
|
48
|
-
entry.occurrenceCount += 1;
|
|
49
|
-
entry.documentKeys.add(`${usage.sourceCollection}:${usage.sourceDocumentId}`);
|
|
50
|
-
relationshipMap.set(relationshipName, entry);
|
|
51
|
-
});
|
|
52
|
-
return Array.from(relationshipMap.entries())
|
|
53
|
-
.map(([componentName, entry]) => ({
|
|
54
|
-
componentName,
|
|
55
|
-
documentCount: entry.documentKeys.size,
|
|
56
|
-
occurrenceCount: entry.occurrenceCount,
|
|
57
|
-
}))
|
|
58
|
-
.sort((a, b) => {
|
|
59
|
-
if (b.documentCount !== a.documentCount) {
|
|
60
|
-
return b.documentCount - a.documentCount;
|
|
61
|
-
}
|
|
62
|
-
if (b.occurrenceCount !== a.occurrenceCount) {
|
|
63
|
-
return b.occurrenceCount - a.occurrenceCount;
|
|
64
|
-
}
|
|
65
|
-
return a.componentName.localeCompare(b.componentName);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function renderRelationshipList(title, summaries, emptyMessage) {
|
|
69
|
-
return (_jsxs("div", { style: { marginTop: 16 }, children: [_jsx("h3", { style: { margin: '0 0 10px', fontSize: 15 }, children: title }), summaries.length ? (_jsx("div", { style: { display: 'grid', gap: 8 }, children: summaries.map((summary) => (_jsxs("div", { style: {
|
|
70
|
-
border: '1px solid #e5e7eb',
|
|
71
|
-
borderRadius: 6,
|
|
72
|
-
padding: 12,
|
|
73
|
-
}, children: [_jsx("div", { style: { fontWeight: 600 }, children: summary.componentName }), _jsxs("div", { style: { color: '#6b7280', fontSize: 12, marginTop: 2 }, children: [summary.documentCount, " doc", summary.documentCount === 1 ? '' : 's', " \u00B7", ' ', summary.occurrenceCount, " occurrence", summary.occurrenceCount === 1 ? '' : 's'] })] }, `${title}-${summary.componentName}`))) })) : (_jsx("p", { style: { color: '#6b7280', margin: 0 }, children: emptyMessage }))] }));
|
|
74
|
-
}
|
|
75
|
-
export default function NitrogenComponentInventoryClient({ adminRoute, components, }) {
|
|
129
|
+
export default function NitrogenComponentInventoryClient({ adminRoute, components, previewUrl, }) {
|
|
76
130
|
const sourceFilterRef = useRef(null);
|
|
131
|
+
const previewFrameRefs = useRef({});
|
|
77
132
|
const allComponentNames = useMemo(() => components.map(({ componentName }) => componentName), [components]);
|
|
78
133
|
const [searchValue, setSearchValue] = useState('');
|
|
79
134
|
const [sourceSearchValue, setSourceSearchValue] = useState('');
|
|
80
135
|
const [selectedSources, setSelectedSources] = useState([]);
|
|
81
136
|
const [openNames, setOpenNames] = useState({});
|
|
137
|
+
const [previewStates, setPreviewStates] = useState({});
|
|
138
|
+
const [propUpdateDrafts, setPropUpdateDrafts] = useState({});
|
|
139
|
+
const editablePropOptionsByComponent = useMemo(() => {
|
|
140
|
+
return new Map(components.map((component) => [
|
|
141
|
+
component.componentName,
|
|
142
|
+
collectEditablePropOptions(component.definition),
|
|
143
|
+
]));
|
|
144
|
+
}, [components]);
|
|
82
145
|
const sourceOptions = useMemo(() => {
|
|
83
146
|
const collectionOptions = new Map();
|
|
84
147
|
const documentOptions = new Map();
|
|
@@ -140,15 +203,6 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
|
|
|
140
203
|
return !hasSourceFilter || component.usages.length > 0;
|
|
141
204
|
});
|
|
142
205
|
}, [components, selectedSources]);
|
|
143
|
-
const allSourceFilteredUsages = useMemo(() => sourceFilteredComponents.flatMap((component) => component.usages), [sourceFilteredComponents]);
|
|
144
|
-
const containsByComponent = useMemo(() => {
|
|
145
|
-
const componentMap = new Map();
|
|
146
|
-
sourceFilteredComponents.forEach(({ componentName }) => {
|
|
147
|
-
const summaries = buildRelationshipSummaries(allSourceFilteredUsages.filter((usage) => usage.parentComponentName === componentName), (usage) => usage.componentName);
|
|
148
|
-
componentMap.set(componentName, summaries);
|
|
149
|
-
});
|
|
150
|
-
return componentMap;
|
|
151
|
-
}, [allSourceFilteredUsages, sourceFilteredComponents]);
|
|
152
206
|
const filteredComponents = useMemo(() => {
|
|
153
207
|
const normalizedQuery = searchValue.trim().toLowerCase();
|
|
154
208
|
return sourceFilteredComponents.filter((component) => {
|
|
@@ -170,6 +224,22 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
|
|
|
170
224
|
document.removeEventListener('mousedown', handlePointerDown);
|
|
171
225
|
};
|
|
172
226
|
}, []);
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
setPropUpdateDrafts((current) => {
|
|
229
|
+
const next = { ...current };
|
|
230
|
+
Object.entries(next).forEach(([componentName, draft]) => {
|
|
231
|
+
if (!draft.result) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
next[componentName] = {
|
|
235
|
+
...draft,
|
|
236
|
+
result: undefined,
|
|
237
|
+
status: 'idle',
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
}, [selectedSources]);
|
|
173
243
|
const expandAll = () => {
|
|
174
244
|
setOpenNames((current) => ({
|
|
175
245
|
...current,
|
|
@@ -196,6 +266,154 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
|
|
|
196
266
|
: selectedSourceLabels.length === 1
|
|
197
267
|
? selectedSourceLabels[0]
|
|
198
268
|
: `${selectedSourceLabels.length} sources`;
|
|
269
|
+
const postPreviewDataToFrame = useCallback((componentName, data) => {
|
|
270
|
+
if (!previewUrl || !data) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const frameWindow = previewFrameRefs.current[componentName]?.contentWindow;
|
|
274
|
+
if (!frameWindow) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
frameWindow.postMessage({
|
|
278
|
+
componentName,
|
|
279
|
+
payload: data,
|
|
280
|
+
type: 'nitrogen-component-preview',
|
|
281
|
+
}, getPreviewTargetOrigin(previewUrl));
|
|
282
|
+
}, [previewUrl]);
|
|
283
|
+
const loadPreviewData = useCallback(async (componentName, usages) => {
|
|
284
|
+
if (!previewUrl) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
setPreviewStates((current) => {
|
|
288
|
+
if (current[componentName]?.status === 'loading' || current[componentName]?.data) {
|
|
289
|
+
return current;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
...current,
|
|
293
|
+
[componentName]: {
|
|
294
|
+
status: 'loading',
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
const previewUsage = usages[0];
|
|
299
|
+
const params = new URLSearchParams({
|
|
300
|
+
componentName,
|
|
301
|
+
});
|
|
302
|
+
if (previewUsage) {
|
|
303
|
+
params.set('sourceCollection', previewUsage.sourceCollection);
|
|
304
|
+
params.set('sourceDocumentId', previewUsage.sourceDocumentId);
|
|
305
|
+
if (previewUsage.moduleId) {
|
|
306
|
+
params.set('moduleId', previewUsage.moduleId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch(`/api/nitrogen/v1/component-inventory/preview-data?${params}`);
|
|
311
|
+
const data = (await response.json());
|
|
312
|
+
if (!response.ok || !('success' in data)) {
|
|
313
|
+
throw new Error('error' in data && data.error ? data.error : 'Failed to load preview data.');
|
|
314
|
+
}
|
|
315
|
+
setPreviewStates((current) => ({
|
|
316
|
+
...current,
|
|
317
|
+
[componentName]: {
|
|
318
|
+
data,
|
|
319
|
+
status: 'loaded',
|
|
320
|
+
},
|
|
321
|
+
}));
|
|
322
|
+
window.setTimeout(() => postPreviewDataToFrame(componentName, data), 0);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
setPreviewStates((current) => ({
|
|
326
|
+
...current,
|
|
327
|
+
[componentName]: {
|
|
328
|
+
error: error instanceof Error ? error.message : 'Failed to load preview data.',
|
|
329
|
+
status: 'error',
|
|
330
|
+
},
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
}, [postPreviewDataToFrame, previewUrl]);
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
filteredComponents.forEach((component) => {
|
|
336
|
+
if (openNames[component.componentName]) {
|
|
337
|
+
void loadPreviewData(component.componentName, component.usages);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}, [filteredComponents, loadPreviewData, openNames]);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
function handlePreviewReady(event) {
|
|
343
|
+
if (!previewUrl || event.origin !== getPreviewTargetOrigin(previewUrl)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const data = event.data;
|
|
347
|
+
if (data.type !== 'nitrogen-component-preview-ready' || typeof data.componentName !== 'string') {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
postPreviewDataToFrame(data.componentName, previewStates[data.componentName]?.data);
|
|
351
|
+
}
|
|
352
|
+
window.addEventListener('message', handlePreviewReady);
|
|
353
|
+
return () => {
|
|
354
|
+
window.removeEventListener('message', handlePreviewReady);
|
|
355
|
+
};
|
|
356
|
+
}, [postPreviewDataToFrame, previewStates, previewUrl]);
|
|
357
|
+
function updatePropDraft(componentName, nextDraft) {
|
|
358
|
+
setPropUpdateDrafts((current) => ({
|
|
359
|
+
...current,
|
|
360
|
+
[componentName]: {
|
|
361
|
+
...(current[componentName] || getDefaultDraft()),
|
|
362
|
+
...nextDraft,
|
|
363
|
+
},
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
async function runPropUpdate(componentName, editableProps, mode, usages) {
|
|
367
|
+
const draft = propUpdateDrafts[componentName] || getDefaultDraft(editableProps[0]);
|
|
368
|
+
const selectedProp = editableProps.find((option) => option.path === draft.propPath);
|
|
369
|
+
if (!selectedProp) {
|
|
370
|
+
updatePropDraft(componentName, {
|
|
371
|
+
error: 'Select a supported prop first.',
|
|
372
|
+
status: 'error',
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
updatePropDraft(componentName, {
|
|
377
|
+
error: undefined,
|
|
378
|
+
status: mode === 'apply' ? 'applying' : 'previewing',
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch(`/api/nitrogen/v1/component-inventory/prop-update/${mode}`, {
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
componentName,
|
|
384
|
+
propPath: selectedProp.path,
|
|
385
|
+
sourceFilters: selectedSources,
|
|
386
|
+
value: coercePropValue(draft.value, selectedProp.type),
|
|
387
|
+
}),
|
|
388
|
+
headers: {
|
|
389
|
+
'Content-Type': 'application/json',
|
|
390
|
+
},
|
|
391
|
+
method: 'POST',
|
|
392
|
+
});
|
|
393
|
+
const result = (await response.json());
|
|
394
|
+
if (!response.ok || !('success' in result)) {
|
|
395
|
+
throw new Error('error' in result && result.error ? result.error : 'Prop update failed.');
|
|
396
|
+
}
|
|
397
|
+
updatePropDraft(componentName, {
|
|
398
|
+
result,
|
|
399
|
+
status: mode === 'apply' ? 'applied' : 'ready',
|
|
400
|
+
});
|
|
401
|
+
if (mode === 'apply') {
|
|
402
|
+
setPreviewStates((current) => {
|
|
403
|
+
const next = { ...current };
|
|
404
|
+
delete next[componentName];
|
|
405
|
+
return next;
|
|
406
|
+
});
|
|
407
|
+
void loadPreviewData(componentName, usages);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
updatePropDraft(componentName, {
|
|
412
|
+
error: error instanceof Error ? error.message : 'Prop update failed.',
|
|
413
|
+
status: 'error',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
199
417
|
return (_jsxs("div", { style: { display: 'grid', gap: 16 }, children: [_jsxs("div", { style: {
|
|
200
418
|
alignItems: 'center',
|
|
201
419
|
display: 'flex',
|
|
@@ -267,8 +485,13 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
|
|
|
267
485
|
}, children: option.kind === 'collection' ? 'Collection' : 'Item' })] }), _jsx("span", { style: { color: '#6b7280', fontSize: 12 }, children: option.description })] })] }, option.key));
|
|
268
486
|
})) : (_jsx("p", { style: { color: '#6b7280', fontSize: 13, margin: 0 }, children: "No sources match." })) })] }) })] }), _jsx("button", { onClick: expandAll, style: buttonStyle('secondary'), type: "button", children: "Expand All" }), _jsx("button", { onClick: collapseAll, style: buttonStyle('secondary'), type: "button", children: "Collapse All" })] })] }), filteredComponents.map(({ componentName, definition, isUnknown, usages }) => {
|
|
269
487
|
const isOpen = Boolean(openNames[componentName]);
|
|
270
|
-
const
|
|
271
|
-
const
|
|
488
|
+
const editableProps = editablePropOptionsByComponent.get(componentName) || [];
|
|
489
|
+
const propDraft = propUpdateDrafts[componentName] || getDefaultDraft(editableProps[0]);
|
|
490
|
+
const selectedEditableProp = editableProps.find((option) => option.path === propDraft.propPath);
|
|
491
|
+
const previewState = previewStates[componentName] || { status: 'idle' };
|
|
492
|
+
const componentPreviewUrl = previewUrl
|
|
493
|
+
? `${previewUrl}${previewUrl.includes('?') ? '&' : '?'}componentName=${encodeURIComponent(componentName)}`
|
|
494
|
+
: '';
|
|
272
495
|
return (_jsxs("section", { style: {
|
|
273
496
|
background: '#fff',
|
|
274
497
|
border: '1px solid #e5e7eb',
|
|
@@ -289,21 +512,105 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
|
|
|
289
512
|
padding: 16,
|
|
290
513
|
textAlign: 'left',
|
|
291
514
|
width: '100%',
|
|
292
|
-
}, type: "button", children: [_jsxs("div", { style: { display: 'grid', gap: 4 }, children: [_jsxs("div", { style: { alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 8 }, children: [_jsx("span", { style: { fontSize: 16, fontWeight: 700 }, children: componentName }), _jsxs("span", { style: { color: '#6b7280', fontSize: 13, fontWeight: 500 }, children: [usages.length, " usage", usages.length === 1 ? '' : 's'] }),
|
|
515
|
+
}, type: "button", children: [_jsxs("div", { style: { display: 'grid', gap: 4 }, children: [_jsxs("div", { style: { alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: 8 }, children: [_jsx("span", { style: { fontSize: 16, fontWeight: 700 }, children: componentName }), _jsxs("span", { style: { color: '#6b7280', fontSize: 13, fontWeight: 500 }, children: [usages.length, " usage", usages.length === 1 ? '' : 's'] }), isUnknown ? (_jsx("span", { style: { color: '#b45309', fontSize: 13, fontWeight: 600 }, children: "Missing from manifest" })) : null] }), definition ? (_jsxs("div", { style: { color: '#6b7280', fontSize: 13 }, children: [definition.sidebarCategory || 'Uncategorized', definition.scope ? ` - ${definition.scope}` : ''] })) : null] }), _jsx("span", { style: { color: '#6b7280', fontSize: 18, lineHeight: 1 }, children: isOpen ? '−' : '+' })] }), isOpen ? (_jsxs("div", { style: {
|
|
293
516
|
borderTop: '1px solid #e5e7eb',
|
|
294
517
|
display: 'grid',
|
|
295
518
|
gap: 0,
|
|
296
|
-
gridTemplateColumns: '
|
|
297
|
-
}, children: [_jsxs("div", { style: {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
519
|
+
gridTemplateColumns: 'minmax(0, 1.45fr) minmax(300px, 0.9fr)',
|
|
520
|
+
}, children: [_jsxs("div", { style: {
|
|
521
|
+
borderRight: '1px solid #e5e7eb',
|
|
522
|
+
display: 'grid',
|
|
523
|
+
gap: 18,
|
|
524
|
+
padding: 16,
|
|
525
|
+
}, children: [_jsxs("div", { children: [_jsx("h3", { style: { margin: '0 0 10px', fontSize: 15 }, children: "Used On" }), usages.length ? (_jsx("div", { style: { display: 'grid', gap: 8 }, children: usages.map((usage) => (_jsxs("a", { href: formatAdminURL({
|
|
526
|
+
adminRoute,
|
|
527
|
+
path: `/collections/${usage.sourceCollection}/${usage.sourceDocumentId}`,
|
|
528
|
+
}), style: {
|
|
529
|
+
border: '1px solid #e5e7eb',
|
|
530
|
+
borderRadius: 6,
|
|
531
|
+
color: 'inherit',
|
|
532
|
+
display: 'block',
|
|
533
|
+
padding: 12,
|
|
534
|
+
textDecoration: 'none',
|
|
535
|
+
}, children: [_jsx("div", { style: { fontWeight: 600 }, children: usage.sourceTitle || usage.sourceSlug || usage.sourceDocumentId }), _jsxs("div", { style: { color: '#6b7280', fontSize: 12, marginTop: 2 }, children: [usage.sourceCollection, " - ", usage.modulePath] })] }, usage.usageKey))) })) : (_jsx("p", { style: { color: '#6b7280', margin: 0 }, children: "No indexed usages yet." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { margin: '0 0 10px', fontSize: 15 }, children: "Props" }), definition ? (Object.values(definition.categories).map((category) => renderCategory(category))) : (_jsx("p", { style: { color: '#6b7280', margin: 0 }, children: "No manifest definition available." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { margin: '0 0 10px', fontSize: 15 }, children: "Update Prop Across Site" }), editableProps.length ? (_jsxs("div", { style: {
|
|
536
|
+
border: '1px solid #e5e7eb',
|
|
537
|
+
borderRadius: 8,
|
|
538
|
+
display: 'grid',
|
|
539
|
+
gap: 10,
|
|
540
|
+
padding: 12,
|
|
541
|
+
}, children: [_jsxs("label", { style: { display: 'grid', gap: 6, fontSize: 13, fontWeight: 600 }, children: ["Prop", _jsx("select", { onChange: (event) => {
|
|
542
|
+
const nextProp = editableProps.find((option) => option.path === event.target.value);
|
|
543
|
+
updatePropDraft(componentName, {
|
|
544
|
+
error: undefined,
|
|
545
|
+
propPath: event.target.value,
|
|
546
|
+
result: undefined,
|
|
547
|
+
status: 'idle',
|
|
548
|
+
value: nextProp
|
|
549
|
+
? stringifyPropValue(nextProp.defaultValue, nextProp.type)
|
|
550
|
+
: '',
|
|
551
|
+
});
|
|
552
|
+
}, style: {
|
|
553
|
+
border: '1px solid #d1d5db',
|
|
554
|
+
borderRadius: 6,
|
|
555
|
+
color: '#111827',
|
|
556
|
+
maxWidth: 520,
|
|
557
|
+
padding: '8px 10px',
|
|
558
|
+
width: '100%',
|
|
559
|
+
}, value: propDraft.propPath, children: editableProps.map((option) => (_jsx("option", { value: option.path, children: option.label }, option.path))) })] }), selectedEditableProp?.type === 'boolean' ? (_jsxs("label", { style: { alignItems: 'center', display: 'flex', gap: 8, fontSize: 13 }, children: [_jsx("input", { checked: Boolean(propDraft.value), onChange: (event) => updatePropDraft(componentName, {
|
|
560
|
+
result: undefined,
|
|
561
|
+
status: 'idle',
|
|
562
|
+
value: event.target.checked,
|
|
563
|
+
}), type: "checkbox" }), "Value"] })) : selectedEditableProp?.options?.length ? (_jsxs("label", { style: { display: 'grid', gap: 6, fontSize: 13, fontWeight: 600 }, children: ["Value", _jsx("select", { onChange: (event) => updatePropDraft(componentName, {
|
|
564
|
+
result: undefined,
|
|
565
|
+
status: 'idle',
|
|
566
|
+
value: event.target.value,
|
|
567
|
+
}), style: {
|
|
568
|
+
border: '1px solid #d1d5db',
|
|
569
|
+
borderRadius: 6,
|
|
570
|
+
color: '#111827',
|
|
571
|
+
maxWidth: 520,
|
|
572
|
+
padding: '8px 10px',
|
|
573
|
+
width: '100%',
|
|
574
|
+
}, value: String(propDraft.value), children: selectedEditableProp.options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] })) : (_jsxs("label", { style: { display: 'grid', gap: 6, fontSize: 13, fontWeight: 600 }, children: ["Value", _jsx("input", { onChange: (event) => updatePropDraft(componentName, {
|
|
575
|
+
result: undefined,
|
|
576
|
+
status: 'idle',
|
|
577
|
+
value: event.target.value,
|
|
578
|
+
}), style: {
|
|
579
|
+
border: '1px solid #d1d5db',
|
|
580
|
+
borderRadius: 6,
|
|
581
|
+
color: '#111827',
|
|
582
|
+
maxWidth: 520,
|
|
583
|
+
padding: '8px 10px',
|
|
584
|
+
width: '100%',
|
|
585
|
+
}, type: selectedEditableProp?.type === 'number' ? 'number' : 'text', value: String(propDraft.value) })] })), _jsxs("div", { style: { color: '#6b7280', fontSize: 12 }, children: ["Scope: ", selectedSources.length ? sourceFilterLabel : 'all indexed usages'] }), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 8 }, children: [_jsx("button", { disabled: propDraft.status === 'previewing' || propDraft.status === 'applying', onClick: () => void runPropUpdate(componentName, editableProps, 'preview', usages), style: buttonStyle('secondary'), type: "button", children: propDraft.status === 'previewing' ? 'Checking...' : 'Dry Run' }), _jsx("button", { disabled: propDraft.status === 'previewing' ||
|
|
586
|
+
propDraft.status === 'applying' ||
|
|
587
|
+
!propDraft.result, onClick: () => void runPropUpdate(componentName, editableProps, 'apply', usages), style: {
|
|
588
|
+
...buttonStyle('primary'),
|
|
589
|
+
opacity: propDraft.result ? 1 : 0.5,
|
|
590
|
+
}, type: "button", children: propDraft.status === 'applying' ? 'Applying...' : 'Apply Update' })] }), propDraft.error ? (_jsx("p", { style: { color: '#b91c1c', fontSize: 13, margin: 0 }, children: propDraft.error })) : null, propDraft.result ? (_jsxs("div", { style: { color: '#374151', fontSize: 13 }, children: [propDraft.result.mode === 'apply' ? 'Updated' : 'Would update', ' ', propDraft.result.occurrenceCount, " occurrence", propDraft.result.occurrenceCount === 1 ? '' : 's', " in", ' ', propDraft.result.documentCount, " document", propDraft.result.documentCount === 1 ? '' : 's', "."] })) : null] })) : (_jsx("p", { style: { color: '#6b7280', margin: 0 }, children: "No scalar props are available for bulk updates." }))] })] }), _jsxs("div", { style: { padding: 16 }, children: [_jsxs("div", { style: {
|
|
591
|
+
alignItems: 'center',
|
|
592
|
+
display: 'flex',
|
|
593
|
+
gap: 12,
|
|
594
|
+
justifyContent: 'space-between',
|
|
595
|
+
marginBottom: 10,
|
|
596
|
+
}, children: [_jsxs("div", { children: [_jsx("h3", { style: { margin: 0, fontSize: 15 }, children: "Preview" }), previewState.data?.source ? (_jsxs("div", { style: { color: '#6b7280', fontSize: 12, marginTop: 2 }, children: ["Rendering saved usage from ", previewState.data.source.title || previewState.data.source.documentId] })) : null] }), previewState.status === 'loading' ? (_jsx("span", { style: { color: '#6b7280', fontSize: 12 }, children: "Loading preview..." })) : null] }), !previewUrl ? (_jsx("p", { style: { color: '#6b7280', margin: 0 }, children: "Configure a Nitrogen development or frontend URL to enable component previews." })) : previewState.status === 'error' ? (_jsx("p", { style: { color: '#b91c1c', margin: 0 }, children: previewState.error })) : (_jsx("div", { style: {
|
|
597
|
+
background: '#f9fafb',
|
|
598
|
+
border: '1px solid #e5e7eb',
|
|
599
|
+
borderRadius: 8,
|
|
600
|
+
overflow: 'hidden',
|
|
601
|
+
position: 'relative',
|
|
602
|
+
width: '100%',
|
|
603
|
+
aspectRatio: '4 / 3',
|
|
604
|
+
}, children: _jsx("iframe", { ref: (element) => {
|
|
605
|
+
previewFrameRefs.current[componentName] = element;
|
|
606
|
+
}, onLoad: () => postPreviewDataToFrame(componentName, previewState.data), src: componentPreviewUrl, style: {
|
|
607
|
+
border: 0,
|
|
304
608
|
display: 'block',
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
609
|
+
height: '100%',
|
|
610
|
+
left: 0,
|
|
611
|
+
position: 'absolute',
|
|
612
|
+
top: 0,
|
|
613
|
+
width: '100%',
|
|
614
|
+
}, title: `${componentName} preview` }) }))] })] })) : null] }, componentName));
|
|
308
615
|
})] }));
|
|
309
616
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { findAllDocs, NITROGEN_COMPONENT_CATALOG_COLLECTION, NITROGEN_COMPONENT_USAGE_COLLECTION, } from '../inventory/indexing';
|
|
3
|
+
import { getNitrogenSettings } from '../endpoints/helpers';
|
|
3
4
|
import NitrogenComponentInventoryClient from './NitrogenComponentInventoryClient';
|
|
4
5
|
import NitrogenComponentInventoryReindexButton from './NitrogenComponentInventoryReindexButton';
|
|
6
|
+
function buildComponentPreviewUrl(settings) {
|
|
7
|
+
const frontendBaseUrl = String(settings.developmentUrl || settings.frontendUrl || '').replace(/\/$/, '');
|
|
8
|
+
if (!frontendBaseUrl) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
const url = new URL('/nitrogen-component-preview', frontendBaseUrl);
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
5
14
|
const NitrogenComponentInventoryField = async ({ payload, req, }) => {
|
|
6
|
-
const [catalogDocs, usageDocs] = await Promise.all([
|
|
15
|
+
const [catalogDocs, usageDocs, nitrogenSettings] = await Promise.all([
|
|
7
16
|
findAllDocs(payload, NITROGEN_COMPONENT_CATALOG_COLLECTION),
|
|
8
17
|
findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION),
|
|
18
|
+
getNitrogenSettings(payload).catch(() => null),
|
|
9
19
|
]);
|
|
10
20
|
const catalogByName = new Map(catalogDocs.map((doc) => [doc.componentName, doc.definition]));
|
|
11
21
|
const usageByName = new Map();
|
|
@@ -21,6 +31,6 @@ const NitrogenComponentInventoryField = async ({ payload, req, }) => {
|
|
|
21
31
|
isUnknown: !catalogByName.get(componentName),
|
|
22
32
|
usages: usageByName.get(componentName) || [],
|
|
23
33
|
}));
|
|
24
|
-
return (_jsxs("div", { style: { display: 'grid', gap: 24, paddingBottom: 24 }, children: [_jsxs("div", { style: { display: 'grid', gap: 12 }, children: [_jsx("p", { style: { margin: 0, color: '#4b5563', maxWidth: 920 }, children: "Inventory view for the registered Nitrogen component manifest and all indexed usages across Nitrogen-enabled documents and templates." }), _jsx(NitrogenComponentInventoryReindexButton, {})] }), _jsx(NitrogenComponentInventoryClient, { adminRoute: req.payload.config.routes.admin, components: components })] }));
|
|
34
|
+
return (_jsxs("div", { style: { display: 'grid', gap: 24, paddingBottom: 24 }, children: [_jsxs("div", { style: { display: 'grid', gap: 12 }, children: [_jsx("p", { style: { margin: 0, color: '#4b5563', maxWidth: 920 }, children: "Inventory view for the registered Nitrogen component manifest and all indexed usages across Nitrogen-enabled documents and templates." }), _jsx(NitrogenComponentInventoryReindexButton, {})] }), _jsx(NitrogenComponentInventoryClient, { adminRoute: req.payload.config.routes.admin, components: components, previewUrl: nitrogenSettings ? buildComponentPreviewUrl(nitrogenSettings) : '' })] }));
|
|
25
35
|
};
|
|
26
36
|
export default NitrogenComponentInventoryField;
|
|
@@ -2,13 +2,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
// @ts-expect-error The consuming Payload app provides @payloadcms/next at runtime.
|
|
3
3
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
4
4
|
import { findAllDocs, NITROGEN_COMPONENT_CATALOG_COLLECTION, NITROGEN_COMPONENT_USAGE_COLLECTION, } from '../inventory/indexing';
|
|
5
|
+
import { getNitrogenSettings } from '../endpoints/helpers';
|
|
5
6
|
import NitrogenComponentInventoryClient from './NitrogenComponentInventoryClient';
|
|
6
7
|
import NitrogenComponentInventoryReindexButton from './NitrogenComponentInventoryReindexButton';
|
|
8
|
+
function buildComponentPreviewUrl(settings) {
|
|
9
|
+
const frontendBaseUrl = String(settings.developmentUrl || settings.frontendUrl || '').replace(/\/$/, '');
|
|
10
|
+
if (!frontendBaseUrl) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
const url = new URL('/nitrogen-component-preview', frontendBaseUrl);
|
|
14
|
+
return url.toString();
|
|
15
|
+
}
|
|
7
16
|
export default async function NitrogenComponentInventoryView({ clientConfig, initPageResult, i18n, locale, params, searchParams, viewActions, viewType, }) {
|
|
8
17
|
const payload = initPageResult.req.payload;
|
|
9
|
-
const [catalogDocs, usageDocs] = await Promise.all([
|
|
18
|
+
const [catalogDocs, usageDocs, nitrogenSettings] = await Promise.all([
|
|
10
19
|
findAllDocs(payload, NITROGEN_COMPONENT_CATALOG_COLLECTION),
|
|
11
20
|
findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION),
|
|
21
|
+
getNitrogenSettings(payload).catch(() => null),
|
|
12
22
|
]);
|
|
13
23
|
const catalogByName = new Map(catalogDocs.map((doc) => [doc.componentName, doc.definition]));
|
|
14
24
|
const usageByName = new Map();
|
|
@@ -24,5 +34,5 @@ export default async function NitrogenComponentInventoryView({ clientConfig, ini
|
|
|
24
34
|
isUnknown: !catalogByName.get(componentName),
|
|
25
35
|
usages: usageByName.get(componentName) || [],
|
|
26
36
|
}));
|
|
27
|
-
return (_jsx(DefaultTemplate, { i18n: i18n, locale: locale, params: params, payload: payload, req: initPageResult.req, searchParams: searchParams, user: initPageResult.req.user, viewActions: viewActions, viewType: viewType, visibleEntities: initPageResult.visibleEntities, children: _jsxs("div", { style: { padding: 24 }, children: [_jsxs("div", { style: { display: 'grid', gap: 12, marginBottom: 24 }, children: [_jsx("h1", { style: { margin: 0, fontSize: 28 }, children: "Nitrogen Components" }), _jsx("p", { style: { margin: 0, color: '#4b5563', maxWidth: 920 }, children: "Inventory view for the registered Nitrogen component manifest and all indexed usages across Nitrogen-enabled documents and templates." }), _jsx(NitrogenComponentInventoryReindexButton, {})] }), _jsx(NitrogenComponentInventoryClient, { adminRoute: clientConfig.routes.admin, components: components })] }) }));
|
|
37
|
+
return (_jsx(DefaultTemplate, { i18n: i18n, locale: locale, params: params, payload: payload, req: initPageResult.req, searchParams: searchParams, user: initPageResult.req.user, viewActions: viewActions, viewType: viewType, visibleEntities: initPageResult.visibleEntities, children: _jsxs("div", { style: { padding: 24 }, children: [_jsxs("div", { style: { display: 'grid', gap: 12, marginBottom: 24 }, children: [_jsx("h1", { style: { margin: 0, fontSize: 28 }, children: "Nitrogen Components" }), _jsx("p", { style: { margin: 0, color: '#4b5563', maxWidth: 920 }, children: "Inventory view for the registered Nitrogen component manifest and all indexed usages across Nitrogen-enabled documents and templates." }), _jsx(NitrogenComponentInventoryReindexButton, {})] }), _jsx(NitrogenComponentInventoryClient, { adminRoute: clientConfig.routes.admin, components: components, previewUrl: nitrogenSettings ? buildComponentPreviewUrl(nitrogenSettings) : '' })] }) }));
|
|
28
38
|
}
|
|
@@ -1,5 +1,199 @@
|
|
|
1
1
|
import { requireAuth } from './helpers';
|
|
2
|
-
import { reindexAllComponentInventory } from '../inventory/indexing';
|
|
2
|
+
import { findAllDocs, NITROGEN_COMPONENT_CATALOG_COLLECTION, NITROGEN_COMPONENT_USAGE_COLLECTION, reindexAllComponentInventory, } from '../inventory/indexing';
|
|
3
|
+
function moduleNameFromNode(node) {
|
|
4
|
+
const value = node.module;
|
|
5
|
+
if (typeof value === 'string')
|
|
6
|
+
return value;
|
|
7
|
+
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
|
8
|
+
return String(value.name);
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function cloneJson(value) {
|
|
13
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
14
|
+
}
|
|
15
|
+
function isSourceFilterMatch(usage, sourceFilters) {
|
|
16
|
+
if (!sourceFilters.length) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return (sourceFilters.includes(`collection:${usage.sourceCollection}`) ||
|
|
20
|
+
sourceFilters.includes(`document:${usage.sourceCollection}:${usage.sourceDocumentId}`));
|
|
21
|
+
}
|
|
22
|
+
async function findUsageDocsForComponent(payload, componentName, sourceFilters) {
|
|
23
|
+
const usageDocs = await findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION, {
|
|
24
|
+
limit: 200,
|
|
25
|
+
where: {
|
|
26
|
+
componentName: {
|
|
27
|
+
equals: componentName,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return usageDocs.filter((usage) => isSourceFilterMatch(usage, sourceFilters));
|
|
32
|
+
}
|
|
33
|
+
function findModuleInTree(modules, componentName, moduleId) {
|
|
34
|
+
if (!Array.isArray(modules)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
for (const maybeNode of modules) {
|
|
38
|
+
if (!maybeNode || typeof maybeNode !== 'object') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const node = maybeNode;
|
|
42
|
+
const nodeModuleId = typeof node.id === 'string' || typeof node.id === 'number' ? String(node.id) : undefined;
|
|
43
|
+
const nodeComponentName = moduleNameFromNode(node);
|
|
44
|
+
if (nodeComponentName === componentName &&
|
|
45
|
+
(!moduleId || !nodeModuleId || nodeModuleId === moduleId)) {
|
|
46
|
+
return node;
|
|
47
|
+
}
|
|
48
|
+
const props = node.props;
|
|
49
|
+
if (props && typeof props === 'object') {
|
|
50
|
+
const children = props.children;
|
|
51
|
+
if (Array.isArray(children)) {
|
|
52
|
+
const found = findModuleInTree(children, componentName, moduleId);
|
|
53
|
+
if (found)
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
else if (children && typeof children === 'object') {
|
|
57
|
+
for (const slotChildren of Object.values(children)) {
|
|
58
|
+
const found = findModuleInTree(slotChildren, componentName, moduleId);
|
|
59
|
+
if (found)
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(node.children)) {
|
|
65
|
+
const found = findModuleInTree(node.children, componentName, moduleId);
|
|
66
|
+
if (found)
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function setValueAtPath(target, path, value) {
|
|
73
|
+
const parts = path.split('.').filter(Boolean);
|
|
74
|
+
let cursor = target;
|
|
75
|
+
parts.forEach((part, index) => {
|
|
76
|
+
if (index === parts.length - 1) {
|
|
77
|
+
cursor[part] = value;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const next = cursor[part];
|
|
81
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
|
82
|
+
cursor[part] = {};
|
|
83
|
+
}
|
|
84
|
+
cursor = cursor[part];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function updateModulesForComponent(modules, componentName, propPath, value, options) {
|
|
88
|
+
if (!Array.isArray(modules)) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
let occurrenceCount = 0;
|
|
92
|
+
modules.forEach((maybeNode) => {
|
|
93
|
+
if (!maybeNode || typeof maybeNode !== 'object') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const node = maybeNode;
|
|
97
|
+
if (moduleNameFromNode(node) === componentName) {
|
|
98
|
+
occurrenceCount += 1;
|
|
99
|
+
if (options.apply) {
|
|
100
|
+
if (!node.props || typeof node.props !== 'object' || Array.isArray(node.props)) {
|
|
101
|
+
node.props = {};
|
|
102
|
+
}
|
|
103
|
+
setValueAtPath(node.props, propPath, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const props = node.props;
|
|
107
|
+
if (props && typeof props === 'object') {
|
|
108
|
+
const children = props.children;
|
|
109
|
+
if (Array.isArray(children)) {
|
|
110
|
+
occurrenceCount += updateModulesForComponent(children, componentName, propPath, value, options);
|
|
111
|
+
}
|
|
112
|
+
else if (children && typeof children === 'object') {
|
|
113
|
+
Object.values(children).forEach((slotChildren) => {
|
|
114
|
+
occurrenceCount += updateModulesForComponent(slotChildren, componentName, propPath, value, options);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(node.children)) {
|
|
119
|
+
occurrenceCount += updateModulesForComponent(node.children, componentName, propPath, value, options);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return occurrenceCount;
|
|
123
|
+
}
|
|
124
|
+
async function getComponentCatalogDoc(payload, componentName) {
|
|
125
|
+
const docs = await findAllDocs(payload, NITROGEN_COMPONENT_CATALOG_COLLECTION, {
|
|
126
|
+
limit: 1,
|
|
127
|
+
where: {
|
|
128
|
+
componentName: {
|
|
129
|
+
equals: componentName,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return docs[0];
|
|
134
|
+
}
|
|
135
|
+
async function runComponentPropUpdate(payload, body, mode) {
|
|
136
|
+
const componentName = typeof body.componentName === 'string' ? body.componentName.trim() : '';
|
|
137
|
+
const propPath = typeof body.propPath === 'string' ? body.propPath.trim() : '';
|
|
138
|
+
const sourceFilters = Array.isArray(body.sourceFilters)
|
|
139
|
+
? body.sourceFilters.filter((value) => typeof value === 'string')
|
|
140
|
+
: [];
|
|
141
|
+
if (!componentName || !propPath) {
|
|
142
|
+
return Response.json({ error: 'componentName and propPath are required.' }, { status: 400 });
|
|
143
|
+
}
|
|
144
|
+
const usageDocs = await findUsageDocsForComponent(payload, componentName, sourceFilters);
|
|
145
|
+
const sourceKeys = Array.from(new Set(usageDocs.map((usage) => `${usage.sourceCollection}:${usage.sourceDocumentId}`)));
|
|
146
|
+
const documents = [];
|
|
147
|
+
let occurrenceCount = 0;
|
|
148
|
+
for (const sourceKey of sourceKeys) {
|
|
149
|
+
const [collection, ...idParts] = sourceKey.split(':');
|
|
150
|
+
const documentId = idParts.join(':');
|
|
151
|
+
try {
|
|
152
|
+
const doc = (await payload.findByID({
|
|
153
|
+
collection: collection,
|
|
154
|
+
id: documentId,
|
|
155
|
+
depth: 0,
|
|
156
|
+
overrideAccess: true,
|
|
157
|
+
}));
|
|
158
|
+
const nextNitrogenData = cloneJson(doc.nitrogenData ?? []);
|
|
159
|
+
const docOccurrenceCount = updateModulesForComponent(nextNitrogenData, componentName, propPath, body.value, { apply: mode === 'apply' });
|
|
160
|
+
if (docOccurrenceCount === 0) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
occurrenceCount += docOccurrenceCount;
|
|
164
|
+
documents.push({
|
|
165
|
+
collection,
|
|
166
|
+
documentId,
|
|
167
|
+
occurrenceCount: docOccurrenceCount,
|
|
168
|
+
title: doc.title || doc.slug || undefined,
|
|
169
|
+
});
|
|
170
|
+
if (mode === 'apply') {
|
|
171
|
+
await payload.update({
|
|
172
|
+
collection: collection,
|
|
173
|
+
id: documentId,
|
|
174
|
+
data: {
|
|
175
|
+
nitrogenData: nextNitrogenData,
|
|
176
|
+
},
|
|
177
|
+
depth: 0,
|
|
178
|
+
overrideAccess: true,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
payload.logger.warn(`[@nitrogenbuilder/connector-payload] Could not update "${componentName}" in ${sourceKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
componentName,
|
|
188
|
+
documentCount: documents.length,
|
|
189
|
+
documents,
|
|
190
|
+
mode,
|
|
191
|
+
occurrenceCount,
|
|
192
|
+
propPath,
|
|
193
|
+
success: true,
|
|
194
|
+
value: body.value,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
3
197
|
export function createComponentInventoryEndpoints(args) {
|
|
4
198
|
return [
|
|
5
199
|
{
|
|
@@ -16,5 +210,84 @@ export function createComponentInventoryEndpoints(args) {
|
|
|
16
210
|
});
|
|
17
211
|
},
|
|
18
212
|
},
|
|
213
|
+
{
|
|
214
|
+
path: '/nitrogen/v1/component-inventory/preview-data',
|
|
215
|
+
method: 'get',
|
|
216
|
+
handler: async (req) => {
|
|
217
|
+
const authError = requireAuth(req);
|
|
218
|
+
if (authError)
|
|
219
|
+
return authError;
|
|
220
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
221
|
+
const componentName = url.searchParams.get('componentName')?.trim() || '';
|
|
222
|
+
const sourceCollection = url.searchParams.get('sourceCollection')?.trim() || '';
|
|
223
|
+
const sourceDocumentId = url.searchParams.get('sourceDocumentId')?.trim() || '';
|
|
224
|
+
const moduleId = url.searchParams.get('moduleId')?.trim() || undefined;
|
|
225
|
+
if (!componentName) {
|
|
226
|
+
return Response.json({ error: 'componentName is required.' }, { status: 400 });
|
|
227
|
+
}
|
|
228
|
+
if (sourceCollection && sourceDocumentId) {
|
|
229
|
+
try {
|
|
230
|
+
const doc = (await req.payload.findByID({
|
|
231
|
+
collection: sourceCollection,
|
|
232
|
+
id: sourceDocumentId,
|
|
233
|
+
depth: 0,
|
|
234
|
+
overrideAccess: true,
|
|
235
|
+
}));
|
|
236
|
+
const module = findModuleInTree(doc.nitrogenData, componentName, moduleId);
|
|
237
|
+
if (module) {
|
|
238
|
+
return Response.json({
|
|
239
|
+
componentName,
|
|
240
|
+
module: cloneJson(module),
|
|
241
|
+
source: {
|
|
242
|
+
collection: sourceCollection,
|
|
243
|
+
documentId: sourceDocumentId,
|
|
244
|
+
title: doc.title || doc.slug || '',
|
|
245
|
+
},
|
|
246
|
+
success: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Fall back to a default module below.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const catalogDoc = await getComponentCatalogDoc(req.payload, componentName);
|
|
255
|
+
return Response.json({
|
|
256
|
+
componentName,
|
|
257
|
+
definition: catalogDoc?.definition || null,
|
|
258
|
+
module: {
|
|
259
|
+
id: `nitrogen-component-preview-${componentName}`,
|
|
260
|
+
module: componentName,
|
|
261
|
+
props: {},
|
|
262
|
+
},
|
|
263
|
+
source: null,
|
|
264
|
+
success: true,
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
path: '/nitrogen/v1/component-inventory/prop-update/preview',
|
|
270
|
+
method: 'post',
|
|
271
|
+
handler: async (req) => {
|
|
272
|
+
const authError = requireAuth(req);
|
|
273
|
+
if (authError)
|
|
274
|
+
return authError;
|
|
275
|
+
const body = (await req.json?.());
|
|
276
|
+
const result = await runComponentPropUpdate(req.payload, body || {}, 'preview');
|
|
277
|
+
return result instanceof Response ? result : Response.json(result);
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
path: '/nitrogen/v1/component-inventory/prop-update/apply',
|
|
282
|
+
method: 'post',
|
|
283
|
+
handler: async (req) => {
|
|
284
|
+
const authError = requireAuth(req);
|
|
285
|
+
if (authError)
|
|
286
|
+
return authError;
|
|
287
|
+
const body = (await req.json?.());
|
|
288
|
+
const result = await runComponentPropUpdate(req.payload, body || {}, 'apply');
|
|
289
|
+
return result instanceof Response ? result : Response.json(result);
|
|
290
|
+
},
|
|
291
|
+
},
|
|
19
292
|
];
|
|
20
293
|
}
|
package/package.json
CHANGED