decap-cms-core 3.5.0 → 3.6.1

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.
Files changed (141) hide show
  1. package/dist/decap-cms-core.js +10 -10
  2. package/dist/decap-cms-core.js.map +1 -1
  3. package/dist/esm/actions/auth.js +22 -37
  4. package/dist/esm/actions/collections.js +9 -17
  5. package/dist/esm/actions/config.js +58 -74
  6. package/dist/esm/actions/deploys.js +10 -17
  7. package/dist/esm/actions/editorialWorkflow.js +87 -101
  8. package/dist/esm/actions/entries.js +161 -211
  9. package/dist/esm/actions/media.js +31 -46
  10. package/dist/esm/actions/mediaLibrary.js +94 -126
  11. package/dist/esm/actions/notifications.js +5 -13
  12. package/dist/esm/actions/search.js +30 -47
  13. package/dist/esm/actions/status.js +13 -23
  14. package/dist/esm/actions/waitUntil.js +4 -11
  15. package/dist/esm/backend.js +132 -148
  16. package/dist/esm/bootstrap.js +37 -44
  17. package/dist/esm/components/App/App.js +82 -89
  18. package/dist/esm/components/App/Header.js +46 -52
  19. package/dist/esm/components/App/NotFoundPage.js +11 -18
  20. package/dist/esm/components/Collection/Collection.js +55 -63
  21. package/dist/esm/components/Collection/CollectionControls.js +15 -22
  22. package/dist/esm/components/Collection/CollectionSearch.js +35 -42
  23. package/dist/esm/components/Collection/CollectionTop.js +23 -30
  24. package/dist/esm/components/Collection/ControlButton.js +10 -16
  25. package/dist/esm/components/Collection/Entries/Entries.js +24 -31
  26. package/dist/esm/components/Collection/Entries/EntriesCollection.js +60 -63
  27. package/dist/esm/components/Collection/Entries/EntriesSearch.js +26 -33
  28. package/dist/esm/components/Collection/Entries/EntryCard.js +38 -45
  29. package/dist/esm/components/Collection/Entries/EntryListing.js +24 -32
  30. package/dist/esm/components/Collection/FilterControl.js +9 -16
  31. package/dist/esm/components/Collection/GroupControl.js +9 -16
  32. package/dist/esm/components/Collection/NestedCollection.js +56 -64
  33. package/dist/esm/components/Collection/Sidebar.js +36 -43
  34. package/dist/esm/components/Collection/SortControl.js +19 -26
  35. package/dist/esm/components/Collection/ViewStyleControl.js +17 -24
  36. package/dist/esm/components/Editor/Editor.js +100 -108
  37. package/dist/esm/components/Editor/EditorControlPane/EditorControl.js +105 -112
  38. package/dist/esm/components/Editor/EditorControlPane/EditorControlPane.js +68 -62
  39. package/dist/esm/components/Editor/EditorControlPane/Widget.js +87 -73
  40. package/dist/esm/components/Editor/EditorInterface.js +95 -98
  41. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreview.js +13 -21
  42. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewContent.js +64 -23
  43. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +94 -78
  44. package/dist/esm/components/Editor/EditorPreviewPane/PreviewHOC.js +9 -16
  45. package/dist/esm/components/Editor/EditorToolbar.js +133 -140
  46. package/dist/esm/components/Editor/withWorkflow.js +15 -22
  47. package/dist/esm/components/EditorWidgets/Unknown/UnknownControl.js +9 -16
  48. package/dist/esm/components/EditorWidgets/Unknown/UnknownPreview.js +9 -16
  49. package/dist/esm/components/EditorWidgets/index.js +4 -7
  50. package/dist/esm/components/MediaLibrary/EmptyMessage.js +12 -19
  51. package/dist/esm/components/MediaLibrary/MediaLibrary.js +55 -62
  52. package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +28 -35
  53. package/dist/esm/components/MediaLibrary/MediaLibraryCard.js +36 -43
  54. package/dist/esm/components/MediaLibrary/MediaLibraryCardGrid.js +50 -57
  55. package/dist/esm/components/MediaLibrary/MediaLibraryHeader.js +16 -23
  56. package/dist/esm/components/MediaLibrary/MediaLibraryModal.js +59 -64
  57. package/dist/esm/components/MediaLibrary/MediaLibrarySearch.js +18 -25
  58. package/dist/esm/components/MediaLibrary/MediaLibraryTop.js +39 -46
  59. package/dist/esm/components/UI/DragDrop.js +21 -30
  60. package/dist/esm/components/UI/ErrorBoundary.js +35 -43
  61. package/dist/esm/components/UI/FileUploadButton.js +11 -18
  62. package/dist/esm/components/UI/Modal.js +19 -26
  63. package/dist/esm/components/UI/Notifications.js +21 -28
  64. package/dist/esm/components/UI/SettingsDropdown.js +28 -34
  65. package/dist/esm/components/UI/index.js +6 -60
  66. package/dist/esm/components/Workflow/Workflow.js +52 -61
  67. package/dist/esm/components/Workflow/WorkflowCard.js +45 -51
  68. package/dist/esm/components/Workflow/WorkflowList.js +43 -49
  69. package/dist/esm/constants/collectionTypes.js +2 -8
  70. package/dist/esm/constants/collectionViews.js +2 -8
  71. package/dist/esm/constants/commitProps.js +2 -8
  72. package/dist/esm/constants/configSchema.js +23 -27
  73. package/dist/esm/constants/fieldInference.js +8 -15
  74. package/dist/esm/constants/publishModes.js +6 -11
  75. package/dist/esm/constants/validationErrorTypes.js +1 -7
  76. package/dist/esm/formats/formats.js +32 -41
  77. package/dist/esm/formats/frontmatter.js +18 -30
  78. package/dist/esm/formats/helpers.js +1 -7
  79. package/dist/esm/formats/json.js +1 -7
  80. package/dist/esm/formats/toml.js +11 -18
  81. package/dist/esm/formats/yaml.js +7 -14
  82. package/dist/esm/index.js +5 -12
  83. package/dist/esm/integrations/index.js +8 -16
  84. package/dist/esm/integrations/providers/algolia/implementation.js +14 -22
  85. package/dist/esm/integrations/providers/assetStore/implementation.js +10 -18
  86. package/dist/esm/lib/consoleError.js +1 -7
  87. package/dist/esm/lib/formatters.js +34 -47
  88. package/dist/esm/lib/i18n.js +37 -66
  89. package/dist/esm/lib/phrases.js +4 -11
  90. package/dist/esm/lib/registry.js +40 -75
  91. package/dist/esm/lib/serializeEntryValues.js +11 -18
  92. package/dist/esm/lib/textHelper.js +1 -7
  93. package/dist/esm/lib/urlHelper.js +28 -43
  94. package/dist/esm/mediaLibrary.js +12 -16
  95. package/dist/esm/reducers/auth.js +10 -16
  96. package/dist/esm/reducers/collections.js +70 -102
  97. package/dist/esm/reducers/combinedReducer.js +4 -11
  98. package/dist/esm/reducers/config.js +11 -19
  99. package/dist/esm/reducers/cursors.js +12 -18
  100. package/dist/esm/reducers/deploys.js +8 -15
  101. package/dist/esm/reducers/editorialWorkflow.js +37 -47
  102. package/dist/esm/reducers/entries.js +107 -132
  103. package/dist/esm/reducers/entryDraft.js +64 -72
  104. package/dist/esm/reducers/globalUI.js +5 -11
  105. package/dist/esm/reducers/index.js +43 -64
  106. package/dist/esm/reducers/integrations.js +8 -16
  107. package/dist/esm/reducers/mediaLibrary.js +43 -52
  108. package/dist/esm/reducers/medias.js +11 -18
  109. package/dist/esm/reducers/notifications.js +9 -15
  110. package/dist/esm/reducers/search.js +12 -18
  111. package/dist/esm/reducers/status.js +7 -13
  112. package/dist/esm/redux/index.js +7 -13
  113. package/dist/esm/redux/middleware/waitUntilAction.js +3 -10
  114. package/dist/esm/routing/history.js +7 -15
  115. package/dist/esm/types/diacritics.d.js +0 -1
  116. package/dist/esm/types/global.d.js +1 -5
  117. package/dist/esm/types/immutable.js +1 -5
  118. package/dist/esm/types/redux.js +7 -8
  119. package/dist/esm/types/tomlify-j0.4.d.js +0 -1
  120. package/dist/esm/valueObjects/AssetProxy.js +2 -10
  121. package/dist/esm/valueObjects/EditorComponent.js +5 -12
  122. package/dist/esm/valueObjects/Entry.js +3 -10
  123. package/index.d.ts +1 -0
  124. package/package.json +3 -2
  125. package/src/components/Collection/Entries/EntriesCollection.js +21 -10
  126. package/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +7 -7
  127. package/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +9 -9
  128. package/src/components/Collection/NestedCollection.js +11 -2
  129. package/src/components/Collection/__tests__/NestedCollection.spec.js +3 -0
  130. package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +68 -0
  131. package/src/components/Editor/EditorControlPane/EditorControl.js +0 -3
  132. package/src/components/Editor/EditorControlPane/EditorControlPane.js +21 -8
  133. package/src/components/Editor/EditorControlPane/Widget.js +22 -1
  134. package/src/components/Editor/EditorInterface.js +6 -1
  135. package/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +51 -11
  136. package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +33 -1
  137. package/src/constants/configSchema.js +1 -0
  138. package/src/types/redux.ts +1 -1
  139. package/dist/esm/actions/editorControl.js +0 -14
  140. package/dist/esm/reducers/editorComponent.js +0 -1
  141. package/dist/esm/reducers/editorControl.js +0 -17
@@ -1,12 +1,5 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.createEntry = createEntry;
7
- var _isBoolean2 = _interopRequireDefault(require("lodash/isBoolean"));
8
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
9
- function createEntry(collection, slug = '', path = '', options = {}) {
1
+ import _isBoolean from "lodash/isBoolean";
2
+ export function createEntry(collection, slug = '', path = '', options = {}) {
10
3
  const returnObj = {
11
4
  collection,
12
5
  slug,
@@ -15,7 +8,7 @@ function createEntry(collection, slug = '', path = '', options = {}) {
15
8
  raw: options.raw || '',
16
9
  data: options.data || {},
17
10
  label: options.label || null,
18
- isModification: (0, _isBoolean2.default)(options.isModification) ? options.isModification : null,
11
+ isModification: _isBoolean(options.isModification) ? options.isModification : null,
19
12
  mediaFiles: options.mediaFiles || [],
20
13
  author: options.author || '',
21
14
  updatedOn: options.updatedOn || '',
package/index.d.ts CHANGED
@@ -310,6 +310,7 @@ declare module 'decap-cms-core' {
310
310
  publish?: boolean;
311
311
  nested?: {
312
312
  depth: number;
313
+ subfolders?: boolean;
313
314
  };
314
315
  meta?: { path?: { label: string; widget: string; index_file: string } };
315
316
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "decap-cms-core",
3
3
  "description": "Decap CMS core application, see decap-cms package for the main distribution.",
4
- "version": "3.5.0",
4
+ "version": "3.6.1",
5
5
  "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core",
6
6
  "bugs": "https://github.com/decaporg/decap-cms/issues",
7
7
  "module": "dist/esm/index.js",
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "@iarna/toml": "2.2.5",
28
28
  "@reduxjs/toolkit": "^1.9.1",
29
+ "@vercel/stega": "^0.1.2",
29
30
  "ajv": "8.12.0",
30
31
  "ajv-errors": "^3.0.0",
31
32
  "ajv-keywords": "^5.0.0",
@@ -96,5 +97,5 @@
96
97
  "@types/url-join": "^4.0.0",
97
98
  "redux-mock-store": "^1.5.3"
98
99
  },
99
- "gitHead": "022dbe523d07bb8bc74970460cc82b259fde4041"
100
+ "gitHead": "d10b9848a31ae3a7b59c3f403d839d062b1f876f"
100
101
  }
@@ -117,22 +117,28 @@ export class EntriesCollection extends React.Component {
117
117
  }
118
118
  }
119
119
 
120
- export function filterNestedEntries(path, collectionFolder, entries) {
120
+ export function filterNestedEntries(path, collectionFolder, entries, subfolders) {
121
121
  const filtered = entries.filter(e => {
122
- const entryPath = e.get('path').slice(collectionFolder.length + 1);
122
+ let entryPath = e.get('path').slice(collectionFolder.length + 1);
123
123
  if (!entryPath.startsWith(path)) {
124
124
  return false;
125
125
  }
126
126
 
127
- // only show immediate children
127
+ // for subdirectories, trim off the parent folder corresponding to
128
+ // this nested collection entry
128
129
  if (path) {
129
- // non root path
130
- const trimmed = entryPath.slice(path.length + 1);
131
- return trimmed.split('/').length === 2;
132
- } else {
133
- // root path
134
- return entryPath.split('/').length <= 2;
130
+ entryPath = entryPath.slice(path.length + 1);
131
+ }
132
+
133
+ // if subfolders legacy mode is enabled, show only immediate subfolders
134
+ // also show index file in root folder
135
+ if (subfolders) {
136
+ const depth = entryPath.split('/').length;
137
+ return path ? depth === 2 : depth <= 2;
135
138
  }
139
+
140
+ // only show immediate children
141
+ return !entryPath.includes('/');
136
142
  });
137
143
  return filtered;
138
144
  }
@@ -146,7 +152,12 @@ function mapStateToProps(state, ownProps) {
146
152
 
147
153
  if (collection.has('nested')) {
148
154
  const collectionFolder = collection.get('folder');
149
- entries = filterNestedEntries(filterTerm || '', collectionFolder, entries);
155
+ entries = filterNestedEntries(
156
+ filterTerm || '',
157
+ collectionFolder,
158
+ entries,
159
+ collection.get('nested').get('subfolders') !== false,
160
+ );
150
161
  }
151
162
  const entriesLoaded = selectEntriesLoaded(state.entries, collection.get('name'));
152
163
  const isFetching = selectIsFetching(state.entries, collection.get('name'));
@@ -45,11 +45,11 @@ describe('filterNestedEntries', () => {
45
45
  ];
46
46
  const entries = fromJS(entriesArray);
47
47
  expect(filterNestedEntries('dir3', 'src/pages', entries).toJS()).toEqual([
48
- { slug: 'dir3/dir4/index', path: 'src/pages/dir3/dir4/index.md', data: { title: 'File 4' } },
48
+ { slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } },
49
49
  ]);
50
50
  });
51
51
 
52
- it('should return immediate children and root for root path', () => {
52
+ it('should return only immediate children for root path', () => {
53
53
  const entriesArray = [
54
54
  { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
55
55
  { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },
@@ -60,8 +60,6 @@ describe('filterNestedEntries', () => {
60
60
  const entries = fromJS(entriesArray);
61
61
  expect(filterNestedEntries('', 'src/pages', entries).toJS()).toEqual([
62
62
  { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
63
- { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },
64
- { slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } },
65
63
  ]);
66
64
  });
67
65
  });
@@ -117,7 +115,9 @@ describe('EntriesCollection', () => {
117
115
  });
118
116
 
119
117
  const { asFragment } = renderWithRedux(
120
- <ConnectedEntriesCollection collection={collection.set('nested', fromJS({ depth: 10 }))} />,
118
+ <ConnectedEntriesCollection
119
+ collection={collection.set('nested', fromJS({ depth: 10, subfolders: false }))}
120
+ />,
121
121
  {
122
122
  store,
123
123
  },
@@ -126,7 +126,7 @@ describe('EntriesCollection', () => {
126
126
  expect(asFragment()).toMatchSnapshot();
127
127
  });
128
128
 
129
- it('should render apply filter term for nested collections', () => {
129
+ it('should render with applied filter term for nested collections', () => {
130
130
  const entriesArray = [
131
131
  { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
132
132
  { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },
@@ -142,7 +142,7 @@ describe('EntriesCollection', () => {
142
142
 
143
143
  const { asFragment } = renderWithRedux(
144
144
  <ConnectedEntriesCollection
145
- collection={collection.set('nested', fromJS({ depth: 10 }))}
145
+ collection={collection.set('nested', fromJS({ depth: 10, subfolders: false }))}
146
146
  filterTerm="dir3/dir4"
147
147
  />,
148
148
  {
@@ -1,36 +1,36 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`EntriesCollection should render apply filter term for nested collections 1`] = `
3
+ exports[`EntriesCollection should render connected component 1`] = `
4
4
  <DocumentFragment>
5
5
  <mock-entries
6
6
  collectionname="Pages"
7
- collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10 } }"
7
+ collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\" }"
8
8
  cursor="[object Object]"
9
- entries="List []"
9
+ entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir2/index\\", \\"path\\": \\"src/pages/dir2/index.md\\", \\"data\\": Map { \\"title\\": \\"File 2\\" } } ]"
10
10
  isfetching="false"
11
11
  />
12
12
  </DocumentFragment>
13
13
  `;
14
14
 
15
- exports[`EntriesCollection should render connected component 1`] = `
15
+ exports[`EntriesCollection should render show only immediate children for nested collection 1`] = `
16
16
  <DocumentFragment>
17
17
  <mock-entries
18
18
  collectionname="Pages"
19
- collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\" }"
19
+ collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10, \\"subfolders\\": false } }"
20
20
  cursor="[object Object]"
21
- entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir2/index\\", \\"path\\": \\"src/pages/dir2/index.md\\", \\"data\\": Map { \\"title\\": \\"File 2\\" } } ]"
21
+ entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } } ]"
22
22
  isfetching="false"
23
23
  />
24
24
  </DocumentFragment>
25
25
  `;
26
26
 
27
- exports[`EntriesCollection should render show only immediate children for nested collection 1`] = `
27
+ exports[`EntriesCollection should render with applied filter term for nested collections 1`] = `
28
28
  <DocumentFragment>
29
29
  <mock-entries
30
30
  collectionname="Pages"
31
- collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10 } }"
31
+ collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10, \\"subfolders\\": false } }"
32
32
  cursor="[object Object]"
33
- entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir3/index\\", \\"path\\": \\"src/pages/dir3/index.md\\", \\"data\\": Map { \\"title\\": \\"File 3\\" } } ]"
33
+ entries="List [ Map { \\"slug\\": \\"dir3/dir4/index\\", \\"path\\": \\"src/pages/dir3/dir4/index.md\\", \\"data\\": Map { \\"title\\": \\"File 4\\" } } ]"
34
34
  isfetching="false"
35
35
  />
36
36
  </DocumentFragment>
@@ -79,8 +79,13 @@ function TreeNode(props) {
79
79
  const collectionName = collection.get('name');
80
80
 
81
81
  const sortedData = sortBy(treeData, getNodeTitle);
82
+ const subfolders = collection.get('nested')?.get('subfolders') !== false;
82
83
  return sortedData.map(node => {
83
- const leaf = node.children.length <= 1 && !node.children[0]?.isDir && depth > 0;
84
+ const leaf =
85
+ depth > 0 &&
86
+ (subfolders
87
+ ? node.children.length <= 1 && !node.children[0]?.isDir
88
+ : node.children.length === 0);
84
89
  if (leaf) {
85
90
  return null;
86
91
  }
@@ -90,7 +95,11 @@ function TreeNode(props) {
90
95
  }
91
96
  const title = getNodeTitle(node);
92
97
 
93
- const hasChildren = depth === 0 || node.children.some(c => c.children.some(c => c.isDir));
98
+ const hasChildren =
99
+ depth === 0 ||
100
+ (subfolders
101
+ ? node.children.some(c => c.children.some(c => c.isDir))
102
+ : node.children.some(c => c.isDir));
94
103
 
95
104
  return (
96
105
  <React.Fragment key={node.path}>
@@ -37,6 +37,9 @@ describe('NestedCollection', () => {
37
37
  label: 'Pages',
38
38
  folder: 'src/pages',
39
39
  fields: [{ name: 'title', widget: 'string' }],
40
+ nested: {
41
+ subfolders: false,
42
+ },
40
43
  });
41
44
 
42
45
  it('should render correctly with no entries', () => {
@@ -138,6 +138,20 @@ exports[`NestedCollection should render connected component 1`] = `
138
138
  margin-right: 4px;
139
139
  }
140
140
 
141
+ .emotion-6 {
142
+ position: relative;
143
+ top: 2px;
144
+ color: #fff;
145
+ width: 0;
146
+ height: 0;
147
+ border: 5px solid transparent;
148
+ border-radius: 2px;
149
+ border-left: 6px solid currentColor;
150
+ border-right: 0;
151
+ color: currentColor;
152
+ left: 2px;
153
+ }
154
+
141
155
  <a
142
156
  class="emotion-0 emotion-1"
143
157
  data-testid="/a"
@@ -155,6 +169,9 @@ exports[`NestedCollection should render connected component 1`] = `
155
169
  >
156
170
  File 1
157
171
  </div>
172
+ <div
173
+ class="emotion-6 emotion-7"
174
+ />
158
175
  </div>
159
176
  </a>
160
177
  .emotion-0 {
@@ -207,6 +224,20 @@ exports[`NestedCollection should render connected component 1`] = `
207
224
  margin-right: 4px;
208
225
  }
209
226
 
227
+ .emotion-6 {
228
+ position: relative;
229
+ top: 2px;
230
+ color: #fff;
231
+ width: 0;
232
+ height: 0;
233
+ border: 5px solid transparent;
234
+ border-radius: 2px;
235
+ border-left: 6px solid currentColor;
236
+ border-right: 0;
237
+ color: currentColor;
238
+ left: 2px;
239
+ }
240
+
210
241
  <a
211
242
  class="emotion-0 emotion-1"
212
243
  data-testid="/b"
@@ -224,6 +255,9 @@ exports[`NestedCollection should render connected component 1`] = `
224
255
  >
225
256
  File 2
226
257
  </div>
258
+ <div
259
+ class="emotion-6 emotion-7"
260
+ />
227
261
  </div>
228
262
  </a>
229
263
  </DocumentFragment>
@@ -367,6 +401,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
367
401
  margin-right: 4px;
368
402
  }
369
403
 
404
+ .emotion-6 {
405
+ position: relative;
406
+ top: 2px;
407
+ color: #fff;
408
+ width: 0;
409
+ height: 0;
410
+ border: 5px solid transparent;
411
+ border-radius: 2px;
412
+ border-left: 6px solid currentColor;
413
+ border-right: 0;
414
+ color: currentColor;
415
+ left: 2px;
416
+ }
417
+
370
418
  <a
371
419
  class="emotion-0 emotion-1"
372
420
  data-testid="/a"
@@ -384,6 +432,9 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
384
432
  >
385
433
  File 1
386
434
  </div>
435
+ <div
436
+ class="emotion-6 emotion-7"
437
+ />
387
438
  </div>
388
439
  </a>
389
440
  .emotion-0 {
@@ -436,6 +487,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
436
487
  margin-right: 4px;
437
488
  }
438
489
 
490
+ .emotion-6 {
491
+ position: relative;
492
+ top: 2px;
493
+ color: #fff;
494
+ width: 0;
495
+ height: 0;
496
+ border: 5px solid transparent;
497
+ border-radius: 2px;
498
+ border-left: 6px solid currentColor;
499
+ border-right: 0;
500
+ color: currentColor;
501
+ left: 2px;
502
+ }
503
+
439
504
  <a
440
505
  class="emotion-0 emotion-1"
441
506
  data-testid="/b"
@@ -453,6 +518,9 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
453
518
  >
454
519
  File 2
455
520
  </div>
521
+ <div
522
+ class="emotion-6 emotion-7"
523
+ />
456
524
  </div>
457
525
  </a>
458
526
  </DocumentFragment>
@@ -139,7 +139,6 @@ class EditorControl extends React.Component {
139
139
  removeInsertedMedia: PropTypes.func.isRequired,
140
140
  persistMedia: PropTypes.func.isRequired,
141
141
  onValidate: PropTypes.func,
142
- processControlRef: PropTypes.func,
143
142
  controlRef: PropTypes.func,
144
143
  query: PropTypes.func.isRequired,
145
144
  queryHits: PropTypes.object,
@@ -201,7 +200,6 @@ class EditorControl extends React.Component {
201
200
  removeInsertedMedia,
202
201
  persistMedia,
203
202
  onValidate,
204
- processControlRef,
205
203
  controlRef,
206
204
  query,
207
205
  queryHits,
@@ -329,7 +327,6 @@ class EditorControl extends React.Component {
329
327
  resolveWidget={resolveWidget}
330
328
  widget={widget}
331
329
  getEditorComponents={getEditorComponents}
332
- ref={processControlRef && partial(processControlRef, field)}
333
330
  controlRef={controlRef}
334
331
  editorControl={ConnectedEditorControl}
335
332
  query={query}
@@ -99,15 +99,17 @@ export default class ControlPane extends React.Component {
99
99
  selectedLocale: this.props.locale,
100
100
  };
101
101
 
102
- componentValidate = {};
102
+ childRefs = {};
103
103
 
104
- controlRef(field, wrappedControl) {
104
+ controlRef = (field, wrappedControl) => {
105
105
  if (!wrappedControl) return;
106
106
  const name = field.get('name');
107
+ this.childRefs[name] = wrappedControl;
108
+ };
107
109
 
108
- this.componentValidate[name] =
109
- wrappedControl.innerWrappedControl?.validate || wrappedControl.validate;
110
- }
110
+ getControlRef = field => wrappedControl => {
111
+ this.controlRef(field, wrappedControl);
112
+ };
111
113
 
112
114
  handleLocaleChange = val => {
113
115
  this.setState({ selectedLocale: val });
@@ -152,7 +154,11 @@ export default class ControlPane extends React.Component {
152
154
  validate = async () => {
153
155
  this.props.fields.forEach(field => {
154
156
  if (field.get('widget') === 'hidden') return;
155
- this.componentValidate[field.get('name')]();
157
+ const control = this.childRefs[field.get('name')];
158
+ const validateFn = control?.innerWrappedControl?.validate ?? control?.validate;
159
+ if (validateFn) {
160
+ validateFn();
161
+ }
156
162
  });
157
163
  };
158
164
 
@@ -165,6 +171,14 @@ export default class ControlPane extends React.Component {
165
171
  }
166
172
  };
167
173
 
174
+ focus(path) {
175
+ const [fieldName, ...remainingPath] = path.split('.');
176
+ const control = this.childRefs[fieldName];
177
+ if (control?.focus) {
178
+ control.focus(remainingPath.join('.'));
179
+ }
180
+ }
181
+
168
182
  render() {
169
183
  const { collection, entry, fields, fieldsMetaData, fieldsErrors, onChange, onValidate, t } =
170
184
  this.props;
@@ -227,8 +241,7 @@ export default class ControlPane extends React.Component {
227
241
  onChange(field, newValue, newMetadata, i18n);
228
242
  }}
229
243
  onValidate={onValidate}
230
- processControlRef={this.controlRef.bind(this)}
231
- controlRef={this.controlRef}
244
+ controlRef={this.getControlRef(field)}
232
245
  entry={entry}
233
246
  collection={collection}
234
247
  isDisabled={isDuplicate}
@@ -44,6 +44,7 @@ export default class Widget extends Component {
44
44
  fieldsErrors: ImmutablePropTypes.map,
45
45
  onChange: PropTypes.func.isRequired,
46
46
  onValidate: PropTypes.func,
47
+ controlRef: PropTypes.func,
47
48
  onOpenMediaLibrary: PropTypes.func.isRequired,
48
49
  onClearMediaControl: PropTypes.func.isRequired,
49
50
  onRemoveMediaControl: PropTypes.func.isRequired,
@@ -55,7 +56,6 @@ export default class Widget extends Component {
55
56
  widget: PropTypes.object.isRequired,
56
57
  getEditorComponents: PropTypes.func.isRequired,
57
58
  isFetching: PropTypes.bool,
58
- controlRef: PropTypes.func,
59
59
  query: PropTypes.func.isRequired,
60
60
  clearSearch: PropTypes.func.isRequired,
61
61
  clearFieldErrors: PropTypes.func.isRequired,
@@ -112,8 +112,29 @@ export default class Widget extends Component {
112
112
  */
113
113
  const { shouldComponentUpdate: scu } = this.innerWrappedControl;
114
114
  this.wrappedControlShouldComponentUpdate = scu && scu.bind(this.innerWrappedControl);
115
+
116
+ // Call the control ref if provided, passing this Widget instance
117
+ if (this.props.controlRef) {
118
+ this.props.controlRef(this);
119
+ }
115
120
  };
116
121
 
122
+ focus(path) {
123
+ // Try widget's custom focus method first
124
+ if (this.innerWrappedControl?.focus) {
125
+ this.innerWrappedControl.focus(path);
126
+ } else {
127
+ // Fall back to focusing by ID for simple widgets
128
+ const element = document.getElementById(this.props.uniqueFieldId);
129
+ element?.focus();
130
+ }
131
+ // After focusing, ensure the element is visible
132
+ const label = document.querySelector(`label[for="${this.props.uniqueFieldId}"]`);
133
+ if (label) {
134
+ label.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
135
+ }
136
+ }
137
+
117
138
  getValidateValue = () => {
118
139
  let value = this.innerWrappedControl?.getValidateValue?.() || this.props.value;
119
140
  // Convert list input widget value to string for validation test
@@ -162,6 +162,10 @@ class EditorInterface extends Component {
162
162
  i18nVisible: localStorage.getItem(I18N_VISIBLE) !== 'false',
163
163
  };
164
164
 
165
+ handleFieldClick = path => {
166
+ this.controlPaneRef?.focus(path);
167
+ };
168
+
165
169
  handleSplitPaneDragStart = () => {
166
170
  this.setState({ showEventBlocker: true });
167
171
  };
@@ -298,6 +302,7 @@ class EditorInterface extends Component {
298
302
  fields={fields}
299
303
  fieldsMetaData={fieldsMetaData}
300
304
  locale={leftPanelLocale}
305
+ onFieldClick={this.handleFieldClick}
301
306
  />
302
307
  </PreviewPaneContainer>
303
308
  </StyledSplitPane>
@@ -381,7 +386,7 @@ class EditorInterface extends Component {
381
386
  title={t('editor.editorInterface.togglePreview')}
382
387
  />
383
388
  )}
384
- {scrollSyncVisible && (
389
+ {scrollSyncVisible && !collection.getIn(['editor', 'visualEditing']) && (
385
390
  <EditorToggle
386
391
  isActive={scrollSyncEnabled}
387
392
  onClick={this.handleToggleScrollSync}
@@ -3,24 +3,63 @@ import React from 'react';
3
3
  import { isElement } from 'react-is';
4
4
  import { ScrollSyncPane } from 'react-scroll-sync';
5
5
  import { FrameContextConsumer } from 'react-frame-component';
6
+ import { vercelStegaDecode } from '@vercel/stega';
6
7
 
7
8
  /**
8
- * We need to create a lightweight component here so that we can access the
9
- * context within the Frame. This allows us to attach the ScrollSyncPane to the
10
- * body.
9
+ * PreviewContent renders the preview component and optionally handles visual editing interactions.
10
+ * By default it uses scroll sync, but can be configured to use visual editing instead.
11
11
  */
12
12
  class PreviewContent extends React.Component {
13
- render() {
13
+ handleClick = e => {
14
+ const { previewProps, onFieldClick } = this.props;
15
+ const visualEditing = previewProps?.collection?.getIn(['editor', 'visualEditing'], false);
16
+
17
+ if (!visualEditing) {
18
+ return;
19
+ }
20
+
21
+ try {
22
+ const text = e.target.textContent;
23
+ const decoded = vercelStegaDecode(text);
24
+ if (decoded?.decap) {
25
+ if (onFieldClick) {
26
+ onFieldClick(decoded.decap);
27
+ }
28
+ }
29
+ } catch (err) {
30
+ console.log('Visual editing error:', err);
31
+ }
32
+ };
33
+
34
+ renderPreview() {
14
35
  const { previewComponent, previewProps } = this.props;
36
+ return (
37
+ <div onClick={this.handleClick}>
38
+ {isElement(previewComponent)
39
+ ? React.cloneElement(previewComponent, previewProps)
40
+ : React.createElement(previewComponent, previewProps)}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ render() {
46
+ const { previewProps } = this.props;
47
+ const visualEditing = previewProps?.collection?.getIn(['editor', 'visualEditing'], false);
48
+ const showScrollSync = !visualEditing;
49
+
15
50
  return (
16
51
  <FrameContextConsumer>
17
- {context => (
18
- <ScrollSyncPane attachTo={context.document.scrollingElement}>
19
- {isElement(previewComponent)
20
- ? React.cloneElement(previewComponent, previewProps)
21
- : React.createElement(previewComponent, previewProps)}
22
- </ScrollSyncPane>
23
- )}
52
+ {context => {
53
+ const preview = this.renderPreview();
54
+ if (showScrollSync) {
55
+ return (
56
+ <ScrollSyncPane attachTo={context.document.scrollingElement}>
57
+ {preview}
58
+ </ScrollSyncPane>
59
+ );
60
+ }
61
+ return preview;
62
+ }}
24
63
  </FrameContextConsumer>
25
64
  );
26
65
  }
@@ -29,6 +68,7 @@ class PreviewContent extends React.Component {
29
68
  PreviewContent.propTypes = {
30
69
  previewComponent: PropTypes.func.isRequired,
31
70
  previewProps: PropTypes.object,
71
+ onFieldClick: PropTypes.func,
32
72
  };
33
73
 
34
74
  export default PreviewContent;