@nitrogenbuilder/connector-payload 0.1.38 → 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.
@@ -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 buildRelationshipSummaries(usages, getRelationshipName) {
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 usedInside = buildRelationshipSummaries(usages, (usage) => usage.parentComponentName);
271
- const contains = containsByComponent.get(componentName) ?? [];
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'] }), contains.length ? (_jsxs("span", { style: { color: '#6b7280', fontSize: 13, fontWeight: 500 }, children: ["Contains ", contains.length, " component", contains.length === 1 ? '' : 's'] })) : null, 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: {
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: 'repeat(auto-fit, minmax(320px, 1fr))',
297
- }, children: [_jsxs("div", { style: { borderRight: '1px solid #e5e7eb', padding: 16 }, 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({
298
- adminRoute,
299
- path: `/collections/${usage.sourceCollection}/${usage.sourceDocumentId}`,
300
- }), style: {
301
- border: '1px solid #e5e7eb',
302
- borderRadius: 6,
303
- color: 'inherit',
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
- padding: 12,
306
- textDecoration: 'none',
307
- }, 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." })), renderRelationshipList('Used Inside', usedInside, 'This component is not indexed inside another component.')] }), _jsxs("div", { style: { padding: 16 }, children: [renderRelationshipList('Contains', contains, 'No nested child components were indexed for this component.'), _jsx("h3", { style: { margin: '16px 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." }))] })] })) : null] }, componentName));
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
  }
@@ -0,0 +1,3 @@
1
+ import type { UIFieldServerComponent } from 'payload';
2
+ declare const NitrogenPackageVersionsField: UIFieldServerComponent;
3
+ export default NitrogenPackageVersionsField;
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { NITROGEN_PACKAGE_NAMES, } from '../constants.js';
6
+ async function readJsonFile(filePath) {
7
+ try {
8
+ const contents = await fs.readFile(filePath, 'utf8');
9
+ return JSON.parse(contents);
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ async function fileExists(filePath) {
16
+ try {
17
+ await fs.access(filePath);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function getDeclaredSpec(packageJson, packageName) {
25
+ return (packageJson.dependencies?.[packageName] ||
26
+ packageJson.devDependencies?.[packageName] ||
27
+ packageJson.optionalDependencies?.[packageName] ||
28
+ packageJson.peerDependencies?.[packageName]);
29
+ }
30
+ async function findNearestPackageJson(packageName, startPath) {
31
+ let directory = path.dirname(startPath);
32
+ while (directory !== path.dirname(directory)) {
33
+ const packageJsonPath = path.join(directory, 'package.json');
34
+ const packageJson = await readJsonFile(packageJsonPath);
35
+ if (packageJson?.name === packageName) {
36
+ return { packageJson, packageJsonPath };
37
+ }
38
+ directory = path.dirname(directory);
39
+ }
40
+ return undefined;
41
+ }
42
+ async function resolveInstalledPackageVersion(appRoot, packageName) {
43
+ try {
44
+ const requireFromApp = createRequire(path.join(appRoot, 'package.json'));
45
+ const entryPath = requireFromApp.resolve(packageName);
46
+ const resolvedPackage = await findNearestPackageJson(packageName, entryPath);
47
+ return {
48
+ installedVersion: resolvedPackage?.packageJson.version,
49
+ resolvedPackageJsonPath: resolvedPackage?.packageJsonPath,
50
+ };
51
+ }
52
+ catch {
53
+ return {};
54
+ }
55
+ }
56
+ async function findPackageJsonPath(candidates) {
57
+ for (const candidate of candidates) {
58
+ if (!candidate)
59
+ continue;
60
+ const packageJsonPath = candidate.endsWith('package.json')
61
+ ? candidate
62
+ : path.join(candidate, 'package.json');
63
+ if (await fileExists(packageJsonPath)) {
64
+ return packageJsonPath;
65
+ }
66
+ }
67
+ return undefined;
68
+ }
69
+ function buildFrontendRootCandidates(cwd) {
70
+ return [
71
+ process.env.NITROGEN_FRONTEND_PACKAGE_JSON,
72
+ process.env.NITROGEN_FRONTEND_ROOT,
73
+ path.resolve(cwd, '../frontend'),
74
+ path.resolve(cwd, '../../apps/frontend'),
75
+ path.resolve(cwd, 'apps/frontend'),
76
+ path.resolve(cwd, '../apps/frontend'),
77
+ ];
78
+ }
79
+ async function getAppVersionSummary(app, label, packageJsonCandidates) {
80
+ const packageJsonPath = await findPackageJsonPath(packageJsonCandidates);
81
+ const packageJson = packageJsonPath
82
+ ? await readJsonFile(packageJsonPath)
83
+ : undefined;
84
+ const rootPath = packageJsonPath ? path.dirname(packageJsonPath) : undefined;
85
+ const packages = rootPath && packageJson
86
+ ? await Promise.all(NITROGEN_PACKAGE_NAMES.map(async (packageName) => {
87
+ const installed = await resolveInstalledPackageVersion(rootPath, packageName);
88
+ return {
89
+ declaredSpec: getDeclaredSpec(packageJson, packageName),
90
+ packageName,
91
+ ...installed,
92
+ };
93
+ }))
94
+ : [];
95
+ return {
96
+ app,
97
+ label,
98
+ packageJsonPath,
99
+ packages: packages.filter((pkg) => pkg.declaredSpec || pkg.installedVersion),
100
+ rootPath,
101
+ };
102
+ }
103
+ function versionCell(version) {
104
+ return version ? (_jsx("code", { style: { color: '#111827', fontSize: 12 }, children: version })) : (_jsx("span", { style: { color: '#9ca3af' }, children: "Not resolved" }));
105
+ }
106
+ function renderAppSummary(summary) {
107
+ return (_jsxs("section", { style: {
108
+ border: '1px solid #e5e7eb',
109
+ borderRadius: 8,
110
+ overflow: 'hidden',
111
+ }, children: [_jsxs("div", { style: { background: '#f9fafb', padding: '12px 14px' }, children: [_jsx("h3", { style: { fontSize: 15, margin: 0 }, children: summary.label }), summary.packageJsonPath ? (_jsx("p", { style: { color: '#6b7280', fontSize: 12, margin: '4px 0 0' }, children: summary.packageJsonPath })) : (_jsx("p", { style: { color: '#991b1b', fontSize: 12, margin: '4px 0 0' }, children: "Could not find package.json." }))] }), summary.packages.length ? (_jsx("div", { style: { overflowX: 'auto' }, children: _jsxs("table", { style: { borderCollapse: 'collapse', fontSize: 13, width: '100%' }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableHeaderStyle, children: "Package" }), _jsx("th", { style: tableHeaderStyle, children: "Declared spec" }), _jsx("th", { style: tableHeaderStyle, children: "Installed version" })] }) }), _jsx("tbody", { children: summary.packages.map((pkg) => (_jsxs("tr", { children: [_jsx("td", { style: tableCellStyle, children: _jsx("code", { style: { fontSize: 12 }, children: pkg.packageName }) }), _jsx("td", { style: tableCellStyle, children: pkg.declaredSpec ? (_jsx("code", { style: { color: '#374151', fontSize: 12 }, children: pkg.declaredSpec })) : (_jsx("span", { style: { color: '#9ca3af' }, children: "Not declared" })) }), _jsx("td", { style: tableCellStyle, children: versionCell(pkg.installedVersion) })] }, pkg.packageName))) })] }) })) : (_jsx("p", { style: { color: '#6b7280', fontSize: 13, margin: 0, padding: 14 }, children: "No @nitrogenbuilder packages found for this app." }))] }, summary.app));
112
+ }
113
+ const tableHeaderStyle = {
114
+ borderTop: '1px solid #e5e7eb',
115
+ color: '#4b5563',
116
+ fontSize: 12,
117
+ fontWeight: 700,
118
+ padding: '10px 12px',
119
+ textAlign: 'left',
120
+ };
121
+ const tableCellStyle = {
122
+ borderTop: '1px solid #e5e7eb',
123
+ padding: '10px 12px',
124
+ verticalAlign: 'top',
125
+ };
126
+ const NitrogenPackageVersionsField = async () => {
127
+ const cwd = process.cwd();
128
+ const [backend, frontend] = await Promise.all([
129
+ getAppVersionSummary('backend', 'Backend', [
130
+ process.env.NITROGEN_BACKEND_PACKAGE_JSON,
131
+ process.env.NITROGEN_BACKEND_ROOT,
132
+ cwd,
133
+ ]),
134
+ getAppVersionSummary('frontend', 'Frontend', buildFrontendRootCandidates(cwd)),
135
+ ]);
136
+ return (_jsxs("div", { style: { display: 'grid', gap: 14, marginBottom: 24 }, children: [renderAppSummary(backend), renderAppSummary(frontend)] }));
137
+ };
138
+ export default NitrogenPackageVersionsField;
@@ -0,0 +1,3 @@
1
+ import type { UIFieldServerComponent } from 'payload';
2
+ declare const NitrogenPackageVersionsHeaderField: UIFieldServerComponent;
3
+ export default NitrogenPackageVersionsHeaderField;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const NitrogenPackageVersionsHeaderField = () => {
3
+ return (_jsxs("div", { style: { marginBottom: 16 }, children: [_jsx("h2", { style: { fontSize: 18, margin: '0 0 6px' }, children: "Nitrogen Versions" }), _jsx("p", { style: { color: '#6b7280', fontSize: 13, margin: 0, maxWidth: 920 }, children: "Declared spec is read from package.json. Installed version is resolved from node_modules, so dev link/file dependencies and production installs are shown separately." })] }));
4
+ };
5
+ export default NitrogenPackageVersionsHeaderField;
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_EDITOR_CDN_VERSION = "0.5.30";
2
+ export declare const NITROGEN_PACKAGE_NAMES: readonly ["@nitrogenbuilder/client", "@nitrogenbuilder/client-core", "@nitrogenbuilder/client-preact", "@nitrogenbuilder/client-react", "@nitrogenbuilder/connector-payload", "@nitrogenbuilder/editor", "@nitrogenbuilder/types"];
@@ -0,0 +1,10 @@
1
+ export const DEFAULT_EDITOR_CDN_VERSION = '0.5.30';
2
+ export const NITROGEN_PACKAGE_NAMES = [
3
+ '@nitrogenbuilder/client',
4
+ '@nitrogenbuilder/client-core',
5
+ '@nitrogenbuilder/client-preact',
6
+ '@nitrogenbuilder/client-react',
7
+ '@nitrogenbuilder/connector-payload',
8
+ '@nitrogenbuilder/editor',
9
+ '@nitrogenbuilder/types',
10
+ ];
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { getPayload } from "payload";
3
3
  import { headers } from "next/headers";
4
4
  import { redirect } from "next/navigation";
5
- const EDITOR_CDN_VERSION = "0.5.30";
5
+ import { DEFAULT_EDITOR_CDN_VERSION } from "../constants.js";
6
6
  function getNitrogenApiUrl(isDevelopment) {
7
7
  if (isDevelopment) {
8
8
  return "/api/nitrogen/v1";
@@ -111,7 +111,11 @@ export function createNitrogenEditorPage(config) {
111
111
  const editorAssetsBase = isEditorDev
112
112
  ? editorOrigin
113
113
  : "/nitrogen-editor/assets";
114
- const cdnBase = `https://cdn.jsdelivr.net/npm/@nitrogenbuilder/editor@${EDITOR_CDN_VERSION}`;
114
+ const editorCdnVersion = typeof settings.editorCdnVersion === "string" &&
115
+ settings.editorCdnVersion.trim()
116
+ ? settings.editorCdnVersion.trim()
117
+ : DEFAULT_EDITOR_CDN_VERSION;
118
+ const cdnBase = `https://cdn.jsdelivr.net/npm/@nitrogenbuilder/editor@${editorCdnVersion}`;
115
119
  return (_jsxs(_Fragment, { children: [!isEditorDev && (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://cdn.jsdelivr.net" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" })] })), isEditorDev ? (_jsxs(_Fragment, { children: [_jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${editorAssetsBase}/fa620pro/css/all.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${editorAssetsBase}/fa620pro/css/sharp-solid.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${editorOrigin}/src/index.scss` })] })) : (_jsxs(_Fragment, { children: [_jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${editorAssetsBase}/fa620pro/css/all.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${editorAssetsBase}/fa620pro/css/sharp-solid.css` }), _jsx("link", { rel: "stylesheet", crossOrigin: "", href: `${cdnBase}/index.css` })] })), _jsx("script", { dangerouslySetInnerHTML: {
116
120
  __html: [
117
121
  `window.nitrogenConfig = ${JSON.stringify(builderConfig)};`,
@@ -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
  }
@@ -1,3 +1,12 @@
1
+ import { DEFAULT_EDITOR_CDN_VERSION } from '../constants.js';
2
+ function validateEditorCdnVersion(value) {
3
+ if (typeof value !== 'string' || value.trim() === '') {
4
+ return true;
5
+ }
6
+ return /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(value.trim())
7
+ ? true
8
+ : 'Use an npm version or dist tag, such as 0.5.30, latest, or beta.';
9
+ }
1
10
  export const NitrogenSettings = {
2
11
  slug: 'nitrogen-settings',
3
12
  admin: {
@@ -43,5 +52,33 @@ export const NitrogenSettings = {
43
52
  },
44
53
  },
45
54
  },
55
+ {
56
+ name: 'packageVersionsHeader',
57
+ type: 'ui',
58
+ admin: {
59
+ components: {
60
+ Field: '@nitrogenbuilder/connector-payload/components/NitrogenPackageVersionsHeaderField',
61
+ },
62
+ },
63
+ },
64
+ {
65
+ name: 'editorCdnVersion',
66
+ label: 'Editor CDN Version',
67
+ type: 'text',
68
+ defaultValue: DEFAULT_EDITOR_CDN_VERSION,
69
+ validate: validateEditorCdnVersion,
70
+ admin: {
71
+ description: 'Version or npm dist tag used for @nitrogenbuilder/editor on jsDelivr when not using editor development mode.',
72
+ },
73
+ },
74
+ {
75
+ name: 'packageVersions',
76
+ type: 'ui',
77
+ admin: {
78
+ components: {
79
+ Field: '@nitrogenbuilder/connector-payload/components/NitrogenPackageVersionsField',
80
+ },
81
+ },
82
+ },
46
83
  ],
47
84
  };
package/dist/types.d.ts CHANGED
@@ -36,6 +36,7 @@ export interface NitrogenSettingsGlobal {
36
36
  instanceUrl?: string;
37
37
  frontendUrl?: string;
38
38
  developmentUrl?: string;
39
+ editorCdnVersion?: string;
39
40
  nitrogenConfig?: NitrogenConfig;
40
41
  }
41
42
  export interface NitrogenConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitrogenbuilder/connector-payload",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Nitrogen page builder connector plugin for Payload CMS 3.x",
5
5
  "author": "Leonardo Dentzien <leo@torchmedia.ca>",
6
6
  "type": "module",