From 47a2f70ef788ae8e61bbbc0ac21e00d68d0029d0 Mon Sep 17 00:00:00 2001 From: Felix Gnass Date: Fri, 17 Jan 2025 13:13:58 +0100 Subject: [PATCH 01/21] feat(nested collections): allow non-index files (#7359) * feat(nested collections): allow non-index files This commit fixes #4972 to allow nested folders with additional content beyond an index file. Side effect: To keep the feature simple, this will now show index files as pages within a folder in NetlifyCMS. This enables creating additional files alongside the given index, but is a change in behavior from the current implementation. Co-authored-by: Eric Gade <105373963+eric-gade@users.noreply.github.com> * test(e2e): adapt to new way nested collections work We use regexps as otherwise .contains("Sub Directory") would also match "Another Sub Directory" --------- Co-authored-by: Andrew Dunkman Co-authored-by: Eric Gade <105373963+eric-gade@users.noreply.github.com> Co-authored-by: Anze Demsar --- .../editorial_workflow_spec_test_backend.js | 44 ++++++------ .../Collection/Entries/EntriesCollection.js | 15 ++-- .../__tests__/EntriesCollection.spec.js | 8 +-- .../EntriesCollection.spec.js.snap | 16 ++--- .../components/Collection/NestedCollection.js | 4 +- .../NestedCollection.spec.js.snap | 68 +++++++++++++++++++ 6 files changed, 108 insertions(+), 47 deletions(-) diff --git a/cypress/e2e/editorial_workflow_spec_test_backend.js b/cypress/e2e/editorial_workflow_spec_test_backend.js index 7d5fae36a968..7cd15ea7cffd 100644 --- a/cypress/e2e/editorial_workflow_spec_test_backend.js +++ b/cypress/e2e/editorial_workflow_spec_test_backend.js @@ -207,17 +207,16 @@ describe('Test Backend Editorial Workflow', () => { login(); inSidebar(() => cy.contains('a', 'Pages').click()); - inSidebar(() => cy.contains('a', 'Directory')); + inSidebar(() => cy.contains('a', /^Directory$/)); inGrid(() => cy.contains('a', 'Root Page')); - inGrid(() => cy.contains('a', 'Directory')); - inSidebar(() => cy.contains('a', 'Directory').click()); + inSidebar(() => cy.contains('a', /^Directory$/).click()); - inGrid(() => cy.contains('a', 'Sub Directory')); - inGrid(() => cy.contains('a', 'Another Sub Directory')); + inSidebar(() => cy.contains('a', /^Sub Directory$/)); + inSidebar(() => cy.contains('a', 'Another Sub Directory')); - inSidebar(() => cy.contains('a', 'Sub Directory').click()); - inGrid(() => cy.contains('a', 'Nested Directory')); + inSidebar(() => cy.contains('a', /^Sub Directory$/).click()); + inSidebar(() => cy.contains('a', 'Nested Directory')); cy.url().should( 'eq', 'http://localhost:8080/#/collections/pages/filter/directory/sub-directory', @@ -233,21 +232,17 @@ describe('Test Backend Editorial Workflow', () => { login(); inSidebar(() => cy.contains('a', 'Pages').click()); - inSidebar(() => cy.contains('a', 'Directory').click()); - inGrid(() => cy.contains('a', 'Another Sub Directory').click()); - - cy.url().should( - 'eq', - 'http://localhost:8080/#/collections/pages/entries/directory/another-sub-directory/index', - ); + inSidebar(() => cy.contains('a', /^Directory$/).click()); + inSidebar(() => cy.contains('a', 'Another Sub Directory').click()); + inGrid(() => cy.contains('a', 'Another Sub Directory')); }); it(`can create a new entry with custom path`, () => { login(); inSidebar(() => cy.contains('a', 'Pages').click()); - inSidebar(() => cy.contains('a', 'Directory').click()); - inSidebar(() => cy.contains('a', 'Sub Directory').click()); + inSidebar(() => cy.contains('a', /^Directory$/).click()); + inSidebar(() => cy.contains('a', /^Sub Directory$/).click()); cy.contains('a', 'New Page').click(); cy.get('[id^="path-field"]').should('have.value', 'directory/sub-directory'); @@ -262,9 +257,9 @@ describe('Test Backend Editorial Workflow', () => { publishEntryInEditor(publishTypes.publishNow); exitEditor(); - inGrid(() => cy.contains('a', 'New Path Title')); - inSidebar(() => cy.contains('a', 'Directory').click()); - inSidebar(() => cy.contains('a', 'Directory').click()); + inSidebar(() => cy.contains('a', 'New Path Title')); + inSidebar(() => cy.contains('a', /^Directory$/).click()); + inSidebar(() => cy.contains('a', /^Directory$/).click()); inGrid(() => cy.contains('a', 'New Path Title').should('not.exist')); }); @@ -272,8 +267,8 @@ describe('Test Backend Editorial Workflow', () => { login(); inSidebar(() => cy.contains('a', 'Pages').click()); - inSidebar(() => cy.contains('a', 'Directory').click()); - inSidebar(() => cy.contains('a', 'Sub Directory').click()); + inSidebar(() => cy.contains('a', /^Directory$/).click()); + inSidebar(() => cy.contains('a', /^Sub Directory$/).click()); cy.contains('a', 'New Page').click(); cy.get('[id^="title-field"]').type('New Path Title'); @@ -292,7 +287,8 @@ describe('Test Backend Editorial Workflow', () => { login(); inSidebar(() => cy.contains('a', 'Pages').click()); - inGrid(() => cy.contains('a', 'Directory').click()); + inSidebar(() => cy.contains('a', /^Directory$/).click()); + inGrid(() => cy.contains('a', /^Directory$/).click()); cy.get('[id^="path-field"]').should('have.value', 'directory'); cy.get('[id^="path-field"]').clear(); @@ -310,7 +306,7 @@ describe('Test Backend Editorial Workflow', () => { inSidebar(() => cy.contains('a', 'New Directory').click()); - inGrid(() => cy.contains('a', 'Sub Directory')); - inGrid(() => cy.contains('a', 'Another Sub Directory')); + inSidebar(() => cy.contains('a', /^Sub Directory$/)); + inSidebar(() => cy.contains('a', 'Another Sub Directory')); }); }); diff --git a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js index 830f7911de8c..55aa3c2bf61d 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js @@ -119,20 +119,19 @@ export class EntriesCollection extends React.Component { export function filterNestedEntries(path, collectionFolder, entries) { const filtered = entries.filter(e => { - const entryPath = e.get('path').slice(collectionFolder.length + 1); + let entryPath = e.get('path').slice(collectionFolder.length + 1); if (!entryPath.startsWith(path)) { return false; } - // only show immediate children + // for subdirectories, trim off the parent folder corresponding to + // this nested collection entry if (path) { - // non root path - const trimmed = entryPath.slice(path.length + 1); - return trimmed.split('/').length === 2; - } else { - // root path - return entryPath.split('/').length <= 2; + entryPath = entryPath.slice(path.length + 1); } + + // only show immediate children + return !entryPath.includes('/'); }); return filtered; } diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js index 5afbd19b19e2..174bc779370b 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js @@ -45,11 +45,11 @@ describe('filterNestedEntries', () => { ]; const entries = fromJS(entriesArray); expect(filterNestedEntries('dir3', 'src/pages', entries).toJS()).toEqual([ - { slug: 'dir3/dir4/index', path: 'src/pages/dir3/dir4/index.md', data: { title: 'File 4' } }, + { slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } }, ]); }); - it('should return immediate children and root for root path', () => { + it('should return only immediate children for root path', () => { const entriesArray = [ { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } }, { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } }, @@ -60,8 +60,6 @@ describe('filterNestedEntries', () => { const entries = fromJS(entriesArray); expect(filterNestedEntries('', 'src/pages', entries).toJS()).toEqual([ { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } }, - { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } }, - { slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } }, ]); }); }); @@ -126,7 +124,7 @@ describe('EntriesCollection', () => { expect(asFragment()).toMatchSnapshot(); }); - it('should render apply filter term for nested collections', () => { + it('should render with applied filter term for nested collections', () => { const entriesArray = [ { slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } }, { slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } }, diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap index ccc4d5911e89..c833607a1fff 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap @@ -1,36 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EntriesCollection should render apply filter term for nested collections 1`] = ` +exports[`EntriesCollection should render connected component 1`] = ` `; -exports[`EntriesCollection should render connected component 1`] = ` +exports[`EntriesCollection should render show only immediate children for nested collection 1`] = ` `; -exports[`EntriesCollection should render show only immediate children for nested collection 1`] = ` +exports[`EntriesCollection should render with applied filter term for nested collections 1`] = ` diff --git a/packages/decap-cms-core/src/components/Collection/NestedCollection.js b/packages/decap-cms-core/src/components/Collection/NestedCollection.js index 0cc9a068c870..d2ff36c5052e 100644 --- a/packages/decap-cms-core/src/components/Collection/NestedCollection.js +++ b/packages/decap-cms-core/src/components/Collection/NestedCollection.js @@ -80,7 +80,7 @@ function TreeNode(props) { const sortedData = sortBy(treeData, getNodeTitle); return sortedData.map(node => { - const leaf = node.children.length <= 1 && !node.children[0]?.isDir && depth > 0; + const leaf = node.children.length === 0 && depth > 0; if (leaf) { return null; } @@ -90,7 +90,7 @@ function TreeNode(props) { } const title = getNodeTitle(node); - const hasChildren = depth === 0 || node.children.some(c => c.children.some(c => c.isDir)); + const hasChildren = depth === 0 || node.children.some(c => c.isDir); return ( diff --git a/packages/decap-cms-core/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap b/packages/decap-cms-core/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap index dd3f5fcd55ac..b643246f4411 100644 --- a/packages/decap-cms-core/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +++ b/packages/decap-cms-core/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap @@ -138,6 +138,20 @@ exports[`NestedCollection should render connected component 1`] = ` margin-right: 4px; } +.emotion-6 { + position: relative; + top: 2px; + color: #fff; + width: 0; + height: 0; + border: 5px solid transparent; + border-radius: 2px; + border-left: 6px solid currentColor; + border-right: 0; + color: currentColor; + left: 2px; +} + File 1 +
.emotion-0 { @@ -207,6 +224,20 @@ exports[`NestedCollection should render connected component 1`] = ` margin-right: 4px; } +.emotion-6 { + position: relative; + top: 2px; + color: #fff; + width: 0; + height: 0; + border: 5px solid transparent; + border-radius: 2px; + border-left: 6px solid currentColor; + border-right: 0; + color: currentColor; + left: 2px; +} + File 2 +
@@ -367,6 +401,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = ` margin-right: 4px; } +.emotion-6 { + position: relative; + top: 2px; + color: #fff; + width: 0; + height: 0; + border: 5px solid transparent; + border-radius: 2px; + border-left: 6px solid currentColor; + border-right: 0; + color: currentColor; + left: 2px; +} + File 1 +
.emotion-0 { @@ -436,6 +487,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = ` margin-right: 4px; } +.emotion-6 { + position: relative; + top: 2px; + color: #fff; + width: 0; + height: 0; + border: 5px solid transparent; + border-radius: 2px; + border-left: 6px solid currentColor; + border-right: 0; + color: currentColor; + left: 2px; +} + File 2 +
From 8b8e873af9a0749720ec03cadbc4b0d391ad84e1 Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Fri, 17 Jan 2025 12:43:02 +0000 Subject: [PATCH 02/21] fix(markdown): convert inline CSS from Google Docs to Markdown (#7351) Extends the HTML to Markdown conversion to better support bold and italic formatting from Google Docs, which generates inline styles on a `span` element instead of strong/b/em/i type elements. Co-authored-by: Anze Demsar --- .../MarkdownControl/plugins/html/withHtml.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js index a7f96cbec3e6..9dc9a5d73af7 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js @@ -30,6 +30,11 @@ const TEXT_TAGS = { U: () => ({ underline: true }), }; +const INLINE_STYLES = { + 'font-style': value => (value === 'italic' ? { italic: true } : {}), + 'font-weight': value => (value === 'bold' || parseInt(value, 10) >= 600 ? { bold: true } : {}), +}; + function deserialize(el) { if (el.nodeType === 3) { return el.textContent; @@ -65,6 +70,20 @@ function deserialize(el) { return children.map(child => jsx('text', attrs, child)); } + // Convert inline CSS on span elements generated by Google Docs + if (nodeName === 'SPAN') { + const attrs = {}; + for (let i = 0; i < el.style.length; i++) { + const propertyName = el.style[i]; + if (INLINE_STYLES[propertyName]) { + const propertyValue = el.style.getPropertyValue(propertyName); + const propertyStyle = INLINE_STYLES[propertyName](propertyValue); + Object.assign(attrs, propertyStyle); + } + } + return children.map(child => jsx('text', attrs, child)); + } + return children; } From 989c2dd6ed80f69b572b8b73c4e37b5106ae04fb Mon Sep 17 00:00:00 2001 From: Felix Gnass Date: Wed, 29 Jan 2025 12:21:58 +0100 Subject: [PATCH 03/21] feat: visual editing (click-to-edit) (#7374) * refactor: clean up controlRef handling * feat: add click-to-edit * test: update snapshots --------- Co-authored-by: Anze Demsar --- dev-test/config.yml | 6 +- package-lock.json | 6 + packages/decap-cms-core/package.json | 1 + .../Editor/EditorControlPane/EditorControl.js | 3 - .../EditorControlPane/EditorControlPane.js | 29 ++-- .../Editor/EditorControlPane/Widget.js | 23 ++- .../src/components/Editor/EditorInterface.js | 7 +- .../EditorPreviewPane/EditorPreviewContent.js | 62 ++++++-- .../EditorPreviewPane/EditorPreviewPane.js | 15 +- packages/decap-cms-lib-util/src/stega.ts | 134 ++++++++++++++++++ packages/decap-cms-lib-util/src/types.ts | 9 ++ .../decap-cms-widget-list/src/ListControl.js | 107 ++++++++++---- .../__snapshots__/ListControl.spec.js.snap | 11 +- .../src/MarkdownControl/RawEditor.js | 3 +- .../src/MarkdownControl/VisualEditor.js | 3 +- .../src/MarkdownControl/index.js | 4 + .../src/ObjectControl.js | 37 ++++- 17 files changed, 397 insertions(+), 63 deletions(-) create mode 100644 packages/decap-cms-lib-util/src/stega.ts create mode 100644 packages/decap-cms-lib-util/src/types.ts diff --git a/dev-test/config.yml b/dev-test/config.yml index d9bf6182a7cc..592ffbed8b2f 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -17,6 +17,8 @@ collections: # A list of collections the CMS should be able to edit slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection + editor: + visualEditing: true view_filters: - label: Posts With Index field: title @@ -60,7 +62,9 @@ collections: # A list of collections the CMS should be able to edit folder: '_restaurants' slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' - create: true # Allow users to create new documents in this collection + create: true # Allow users to create new documents in this collection + editor: + visualEditing: true fields: # The fields each document in this collection have - { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' } - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' } diff --git a/package-lock.json b/package-lock.json index 9e1197d65412..5c1729a629cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7718,6 +7718,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vercel/stega": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@vercel/stega/-/stega-0.1.2.tgz", + "integrity": "sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "dev": true, @@ -33419,6 +33424,7 @@ "dependencies": { "@iarna/toml": "2.2.5", "@reduxjs/toolkit": "^1.9.1", + "@vercel/stega": "^0.1.2", "ajv": "8.12.0", "ajv-errors": "^3.0.0", "ajv-keywords": "^5.0.0", diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json index 59654f5a0fe4..87f3566d17cf 100644 --- a/packages/decap-cms-core/package.json +++ b/packages/decap-cms-core/package.json @@ -26,6 +26,7 @@ "dependencies": { "@iarna/toml": "2.2.5", "@reduxjs/toolkit": "^1.9.1", + "@vercel/stega": "^0.1.2", "ajv": "8.12.0", "ajv-errors": "^3.0.0", "ajv-keywords": "^5.0.0", diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 58dcd65e4241..a5467c121565 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -139,7 +139,6 @@ class EditorControl extends React.Component { removeInsertedMedia: PropTypes.func.isRequired, persistMedia: PropTypes.func.isRequired, onValidate: PropTypes.func, - processControlRef: PropTypes.func, controlRef: PropTypes.func, query: PropTypes.func.isRequired, queryHits: PropTypes.object, @@ -201,7 +200,6 @@ class EditorControl extends React.Component { removeInsertedMedia, persistMedia, onValidate, - processControlRef, controlRef, query, queryHits, @@ -329,7 +327,6 @@ class EditorControl extends React.Component { resolveWidget={resolveWidget} widget={widget} getEditorComponents={getEditorComponents} - ref={processControlRef && partial(processControlRef, field)} controlRef={controlRef} editorControl={ConnectedEditorControl} query={query} diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js index 18b2dfbf83d6..dae2d3d3a241 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js @@ -99,15 +99,17 @@ export default class ControlPane extends React.Component { selectedLocale: this.props.locale, }; - componentValidate = {}; + childRefs = {}; - controlRef(field, wrappedControl) { + controlRef = (field, wrappedControl) => { if (!wrappedControl) return; const name = field.get('name'); + this.childRefs[name] = wrappedControl; + }; - this.componentValidate[name] = - wrappedControl.innerWrappedControl?.validate || wrappedControl.validate; - } + getControlRef = field => wrappedControl => { + this.controlRef(field, wrappedControl); + }; handleLocaleChange = val => { this.setState({ selectedLocale: val }); @@ -152,7 +154,11 @@ export default class ControlPane extends React.Component { validate = async () => { this.props.fields.forEach(field => { if (field.get('widget') === 'hidden') return; - this.componentValidate[field.get('name')](); + const control = this.childRefs[field.get('name')]; + const validateFn = control?.innerWrappedControl?.validate ?? control?.validate; + if (validateFn) { + validateFn(); + } }); }; @@ -165,6 +171,14 @@ export default class ControlPane extends React.Component { } }; + focus(path) { + const [fieldName, ...remainingPath] = path.split('.'); + const control = this.childRefs[fieldName]; + if (control?.focus) { + control.focus(remainingPath.join('.')); + } + } + render() { const { collection, entry, fields, fieldsMetaData, fieldsErrors, onChange, onValidate, t } = this.props; @@ -227,8 +241,7 @@ export default class ControlPane extends React.Component { onChange(field, newValue, newMetadata, i18n); }} onValidate={onValidate} - processControlRef={this.controlRef.bind(this)} - controlRef={this.controlRef} + controlRef={this.getControlRef(field)} entry={entry} collection={collection} isDisabled={isDuplicate} diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js index 666053a42ab7..96c530d06232 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -44,6 +44,7 @@ export default class Widget extends Component { fieldsErrors: ImmutablePropTypes.map, onChange: PropTypes.func.isRequired, onValidate: PropTypes.func, + controlRef: PropTypes.func, onOpenMediaLibrary: PropTypes.func.isRequired, onClearMediaControl: PropTypes.func.isRequired, onRemoveMediaControl: PropTypes.func.isRequired, @@ -55,7 +56,6 @@ export default class Widget extends Component { widget: PropTypes.object.isRequired, getEditorComponents: PropTypes.func.isRequired, isFetching: PropTypes.bool, - controlRef: PropTypes.func, query: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired, clearFieldErrors: PropTypes.func.isRequired, @@ -112,8 +112,29 @@ export default class Widget extends Component { */ const { shouldComponentUpdate: scu } = this.innerWrappedControl; this.wrappedControlShouldComponentUpdate = scu && scu.bind(this.innerWrappedControl); + + // Call the control ref if provided, passing this Widget instance + if (this.props.controlRef) { + this.props.controlRef(this); + } }; + focus(path) { + // Try widget's custom focus method first + if (this.innerWrappedControl?.focus) { + this.innerWrappedControl.focus(path); + } else { + // Fall back to focusing by ID for simple widgets + const element = document.getElementById(this.props.uniqueFieldId); + element?.focus(); + } + // After focusing, ensure the element is visible + const label = document.querySelector(`label[for="${this.props.uniqueFieldId}"]`); + if (label) { + label.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + getValidateValue = () => { let value = this.innerWrappedControl?.getValidateValue?.() || this.props.value; // Convert list input widget value to string for validation test diff --git a/packages/decap-cms-core/src/components/Editor/EditorInterface.js b/packages/decap-cms-core/src/components/Editor/EditorInterface.js index 7fb49ae0efac..cad473866aa2 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorInterface.js +++ b/packages/decap-cms-core/src/components/Editor/EditorInterface.js @@ -162,6 +162,10 @@ class EditorInterface extends Component { i18nVisible: localStorage.getItem(I18N_VISIBLE) !== 'false', }; + handleFieldClick = path => { + this.controlPaneRef?.focus(path); + }; + handleSplitPaneDragStart = () => { this.setState({ showEventBlocker: true }); }; @@ -298,6 +302,7 @@ class EditorInterface extends Component { fields={fields} fieldsMetaData={fieldsMetaData} locale={leftPanelLocale} + onFieldClick={this.handleFieldClick} /> @@ -381,7 +386,7 @@ class EditorInterface extends Component { title={t('editor.editorInterface.togglePreview')} /> )} - {scrollSyncVisible && ( + {scrollSyncVisible && !collection.getIn(['editor', 'visualEditing']) && ( { + const { previewProps, onFieldClick } = this.props; + const visualEditing = previewProps?.collection?.getIn(['editor', 'visualEditing'], false); + + if (!visualEditing) { + return; + } + + try { + const text = e.target.textContent; + const decoded = vercelStegaDecode(text); + if (decoded?.decap) { + if (onFieldClick) { + onFieldClick(decoded.decap); + } + } + } catch (err) { + console.log('Visual editing error:', err); + } + }; + + renderPreview() { const { previewComponent, previewProps } = this.props; + return ( +
+ {isElement(previewComponent) + ? React.cloneElement(previewComponent, previewProps) + : React.createElement(previewComponent, previewProps)} +
+ ); + } + + render() { + const { previewProps } = this.props; + const visualEditing = previewProps?.collection?.getIn(['editor', 'visualEditing'], false); + const showScrollSync = !visualEditing; + return ( - {context => ( - - {isElement(previewComponent) - ? React.cloneElement(previewComponent, previewProps) - : React.createElement(previewComponent, previewProps)} - - )} + {context => { + const preview = this.renderPreview(); + if (showScrollSync) { + return ( + + {preview} + + ); + } + return preview; + }} ); } @@ -29,6 +68,7 @@ class PreviewContent extends React.Component { PreviewContent.propTypes = { previewComponent: PropTypes.func.isRequired, previewProps: PropTypes.object, + onFieldClick: PropTypes.func, }; export default PreviewContent; diff --git a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js index b9feb9887238..c35fc642fe39 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import Frame, { FrameContextConsumer } from 'react-frame-component'; import { lengths } from 'decap-cms-ui-default'; import { connect } from 'react-redux'; +import { encodeEntry } from 'decap-cms-lib-util/src/stega'; import { resolveWidget, @@ -92,6 +93,7 @@ export class PreviewPane extends React.Component { if (field.get('meta')) { value = this.props.entry.getIn(['meta', field.get('name')]); } + const nestedFields = field.get('fields'); const singleField = field.get('field'); const metadata = fieldsMetaData && fieldsMetaData.get(field.get('name'), Map()); @@ -226,9 +228,18 @@ export class PreviewPane extends React.Component { this.inferFields(); + const visualEditing = collection.getIn(['editor', 'visualEditing'], false); + + // Only encode entry data if visual editing is enabled + const previewEntry = visualEditing + ? entry.set('data', encodeEntry(entry.get('data'), this.props.fields)) + : entry; + const previewProps = { ...this.props, - widgetFor: this.widgetFor, + entry: previewEntry, + widgetFor: (name, fields, values = previewEntry.get('data'), fieldsMetaData) => + this.widgetFor(name, fields, values, fieldsMetaData), widgetsFor: this.widgetsFor, getCollection: this.getCollection, }; @@ -260,6 +271,7 @@ export class PreviewPane extends React.Component { return ( ); }} @@ -276,6 +288,7 @@ PreviewPane.propTypes = { entry: ImmutablePropTypes.map.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired, getAsset: PropTypes.func.isRequired, + onFieldClick: PropTypes.func, }; function mapStateToProps(state) { diff --git a/packages/decap-cms-lib-util/src/stega.ts b/packages/decap-cms-lib-util/src/stega.ts new file mode 100644 index 000000000000..099b86e6db03 --- /dev/null +++ b/packages/decap-cms-lib-util/src/stega.ts @@ -0,0 +1,134 @@ +import { vercelStegaEncode } from '@vercel/stega'; + +import { isImmutableMap, isImmutableList } from './types'; + +import type { Map as ImmutableMap, List } from 'immutable'; +import type { CmsField } from 'decap-cms-core'; + +/** + * Context passed to encode functions, containing the current state of the encoding process + */ +interface EncodeContext { + fields: CmsField[]; // Available CMS fields at current level + path: string; // Path to current value in object tree + visit: (value: unknown, fields: CmsField[], path: string) => unknown; // Visitor for recursive traversal +} + +/** + * Get the fields that should be used for encoding nested values + */ +function getNestedFields(f?: CmsField): CmsField[] { + if (f) { + if ('types' in f) { + return f.types ?? []; + } + if ('fields' in f) { + return f.fields ?? []; + } + if ('field' in f) { + return f.field ? [f.field] : []; + } + return [f]; + } + return []; +} + +/** + * Encode a string value by appending steganographic data + * For markdown fields, encode each paragraph separately + */ +function encodeString(value: string, { fields, path }: EncodeContext): string { + const stega = vercelStegaEncode({ decap: path }); + const isMarkdown = fields[0]?.widget === 'markdown'; + + if (isMarkdown && value.includes('\n\n')) { + const blocks = value.split(/(\n\n+)/); + return blocks.map(block => (block.trim() ? block + stega : block)).join(''); + } + return value + stega; +} + +/** + * Encode a list of values, handling both simple values and nested objects/lists + * For typed lists, use the type field to determine which fields to use + */ +function encodeList(list: List, ctx: EncodeContext): List { + let newList = list; + for (let i = 0; i < newList.size; i++) { + const item = newList.get(i); + if (isImmutableMap(item)) { + const itemType = item.get('type'); + if (typeof itemType === 'string') { + // For typed items, look up fields based on type + const field = ctx.fields.find(f => f.name === itemType); + const newItem = ctx.visit(item, getNestedFields(field), `${ctx.path}.${i}`); + newList = newList.set(i, newItem); + } else { + // For untyped items, use current fields + const newItem = ctx.visit(item, ctx.fields, `${ctx.path}.${i}`); + newList = newList.set(i, newItem); + } + } else { + // For simple values, use first field if available + const field = ctx.fields[0]; + const newItem = ctx.visit(item, field ? [field] : [], `${ctx.path}.${i}`); + if (newItem !== item) { + newList = newList.set(i, newItem); + } + } + } + return newList; +} + +/** + * Encode a map of values, looking up the appropriate field for each key + * and recursively encoding nested values + */ +function encodeMap( + map: ImmutableMap, + ctx: EncodeContext, +): ImmutableMap { + let newMap = map; + for (const [key, val] of newMap.entrySeq().toArray()) { + const field = ctx.fields.find(f => f.name === key); + if (field) { + const fields = getNestedFields(field); + const newVal = ctx.visit(val, fields, ctx.path ? `${ctx.path}.${key}` : key); + if (newVal !== val) { + newMap = newMap.set(key, newVal); + } + } + } + return newMap; +} + +/** + * Main entry point for encoding steganographic data into entry values + * Uses a visitor pattern with caching to handle recursive structures + */ +export function encodeEntry(value: unknown, fields: List>) { + const plainFields = fields.toJS() as CmsField[]; + const cache = new Map(); + + function visit(value: unknown, fields: CmsField[], path = '') { + const cached = cache.get(path); + if (cached === value) return value; + + const ctx: EncodeContext = { fields, path, visit }; + let result; + if (isImmutableList(value)) { + result = encodeList(value, ctx); + } else if (isImmutableMap(value)) { + result = encodeMap(value, ctx); + } else if (typeof value === 'string') { + result = encodeString(value, ctx); + } else { + result = value; + } + + cache.set(path, result); + return result; + } + + return visit(value, plainFields); +} diff --git a/packages/decap-cms-lib-util/src/types.ts b/packages/decap-cms-lib-util/src/types.ts new file mode 100644 index 000000000000..5caf8822565c --- /dev/null +++ b/packages/decap-cms-lib-util/src/types.ts @@ -0,0 +1,9 @@ +import { Map as ImmutableMap, List } from 'immutable'; + +export function isImmutableMap(value: unknown): value is ImmutableMap { + return ImmutableMap.isMap(value); +} + +export function isImmutableList(value: unknown): value is List { + return List.isList(value); +} diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 3db08db07e73..277e5a0d5718 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -171,7 +171,7 @@ function LabelComponent({ field, isActive, hasErrors, uniqueFieldId, isFieldOpti } export default class ListControl extends React.Component { - validations = []; + childRefs = {}; static propTypes = { metadata: ImmutablePropTypes.map, @@ -365,16 +365,18 @@ export default class ListControl extends React.Component { processControlRef = ref => { if (!ref) return; const { - validate, props: { validationKey: key }, } = ref; - this.validations.push({ key, validate }); + this.childRefs[key] = ref; + this.props.controlRef?.(this); }; validate = () => { - if (this.getValueType()) { - this.validations.forEach(item => { - item.validate(); + // First validate child widgets if this is a complex list + const hasChildWidgets = this.getValueType() && Object.keys(this.childRefs).length > 0; + if (hasChildWidgets) { + Object.values(this.childRefs).forEach(widget => { + widget?.validate?.(); }); } else { this.props.validate(); @@ -431,7 +433,17 @@ export default class ListControl extends React.Component { handleRemove = (index, event) => { event.preventDefault(); const { itemsCollapsed } = this.state; - const { value, metadata, onChange, field, clearFieldErrors } = this.props; + const { + value, + metadata, + onChange, + field, + clearFieldErrors, + onValidateObject, + forID, + fieldsErrors, + } = this.props; + const collectionName = field.get('name'); const isSingleField = this.getValueType() === valueTypes.SINGLE; @@ -441,17 +453,41 @@ export default class ListControl extends React.Component { ? { [collectionName]: metadata.removeIn(metadataRemovePath) } : metadata; + // Get the key of the item being removed + const removedKey = this.state.keys[index]; + + // Update state while preserving keys for remaining items + const newKeys = [...this.state.keys]; + newKeys.splice(index, 1); itemsCollapsed.splice(index, 1); - // clear validations - this.validations = []; this.setState({ itemsCollapsed: [...itemsCollapsed], - keys: Array.from({ length: value.size - 1 }, () => uuid()), + keys: newKeys, }); - onChange(value.remove(index), parsedMetadata); - clearFieldErrors(); + // Clear the ref for the removed item + delete this.childRefs[removedKey]; + + const newValue = value.delete(index); + + // Clear errors for the removed item and its children + if (fieldsErrors) { + Object.entries(fieldsErrors.toJS()).forEach(([fieldId, errors]) => { + if (errors.some(err => err.parentIds?.includes(removedKey))) { + clearFieldErrors(fieldId); + } + }); + } + + // If list is empty, mark it as valid + if (newValue.size === 0) { + clearFieldErrors(forID); + onValidateObject(forID, []); + } + + // Update the value last to ensure all error states are cleared + onChange(newValue, parsedMetadata); }; handleItemCollapseToggle = (index, event) => { @@ -527,7 +563,7 @@ export default class ListControl extends React.Component { } onSortEnd = ({ oldIndex, newIndex }) => { - const { value, clearFieldErrors } = this.props; + const { value } = this.props; const { itemsCollapsed, keys } = this.state; // Update value @@ -541,18 +577,13 @@ export default class ListControl extends React.Component { const updatedItemsCollapsed = [...itemsCollapsed]; updatedItemsCollapsed.splice(newIndex, 0, collapsed); - // Reset item to ensure updated state - const updatedKeys = keys.map((key, keyIndex) => { - if (keyIndex === oldIndex || keyIndex === newIndex) { - return uuid(); - } - return key; - }); - this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys }); + // Move keys to maintain relationships + const movedKey = keys[oldIndex]; + const updatedKeys = [...keys]; + updatedKeys.splice(oldIndex, 1); + updatedKeys.splice(newIndex, 0, movedKey); - //clear error fields and remove old validations - clearFieldErrors(); - this.validations = this.validations.filter(item => updatedKeys.includes(item.key)); + this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys }); }; hasError = index => { @@ -564,6 +595,34 @@ export default class ListControl extends React.Component { } }; + focus(path) { + const [index, ...remainingPath] = path.split('.'); + + if (this.state.listCollapsed || this.state.itemsCollapsed[index]) { + const newItemsCollapsed = [...this.state.itemsCollapsed]; + newItemsCollapsed[index] = false; + this.setState( + { + listCollapsed: false, + itemsCollapsed: newItemsCollapsed, + }, + () => { + const key = this.state.keys[index]; + const control = this.childRefs[key]; + if (control?.focus) { + control.focus(remainingPath.join('.')); + } + }, + ); + } else { + const key = this.state.keys[index]; + const control = this.childRefs[key]; + if (control?.focus) { + control.focus(remainingPath.join('.')); + } + } + } + // eslint-disable-next-line react/display-name renderItem = (item, index) => { const { diff --git a/packages/decap-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap b/packages/decap-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap index d165efdd4770..b5b38ac07c81 100644 --- a/packages/decap-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap +++ b/packages/decap-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap @@ -724,13 +724,12 @@ exports[`ListControl should remove from list when remove button is clicked 2`] =
diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js index d9478ddb0a66..6a99b9c8481d 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js @@ -43,8 +43,9 @@ function RawEditor(props) { useEffect(() => { if (props.pendingFocus) { ReactEditor.focus(editor); + props.pendingFocus(); } - }, []); + }, [props.pendingFocus]); function handleToggleMode() { props.onMode('rich_text'); diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index d73fbaa7ff60..860939fbb979 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -135,8 +135,9 @@ function Editor(props) { useEffect(() => { if (props.pendingFocus) { ReactEditor.focus(editor); + props.pendingFocus(); } - }, []); + }, [props.pendingFocus]); function handleMarkClick(format) { ReactEditor.focus(editor); diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/index.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/index.js index d89b199872f5..5831f88a3c2e 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/index.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/index.js @@ -67,6 +67,10 @@ export default class MarkdownControl extends React.Component { getAllowedModes = () => this.props.field.get('modes', List(['rich_text', 'raw'])).toArray(); + focus() { + this.setState({ pendingFocus: true }); + } + render() { const { onChange, diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index 2f974aabdcc9..0d717f3e287c 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -22,7 +22,14 @@ const styleStrings = { }; export default class ObjectControl extends React.Component { - componentValidate = {}; + childRefs = {}; + + processControlRef = ref => { + if (!ref) return; + const name = ref.props.field.get('name'); + this.childRefs[name] = ref; + this.props.controlRef?.(ref); + }; static propTypes = { onChangeObject: PropTypes.func.isRequired, @@ -70,7 +77,9 @@ export default class ObjectControl extends React.Component { fields = List.isList(fields) ? fields : List([fields]); fields.forEach(field => { if (field.get('widget') === 'hidden') return; - this.componentValidate[field.get('name')](); + const name = field.get('name'); + const control = this.childRefs[name]; + control?.validate?.(); }); }; @@ -83,7 +92,6 @@ export default class ObjectControl extends React.Component { metadata, fieldsErrors, editorControl: EditorControl, - controlRef, parentIds, isFieldDuplicate, isFieldHidden, @@ -111,8 +119,7 @@ export default class ObjectControl extends React.Component { fieldsMetaData={metadata} fieldsErrors={fieldsErrors} onValidate={onValidateObject} - processControlRef={controlRef && controlRef.bind(this)} - controlRef={controlRef} + controlRef={this.processControlRef} parentIds={[...parentIds, forID]} isDisabled={isDuplicate} isHidden={isHidden} @@ -128,6 +135,26 @@ export default class ObjectControl extends React.Component { this.setState({ collapsed: !this.state.collapsed }); }; + focus(path) { + if (this.state.collapsed) { + this.setState({ collapsed: false }, () => { + if (path) { + const [fieldName, ...remainingPath] = path.split('.'); + const field = this.childRefs[fieldName]; + if (field?.focus) { + field.focus(remainingPath.join('.')); + } + } + }); + } else if (path) { + const [fieldName, ...remainingPath] = path.split('.'); + const field = this.childRefs[fieldName]; + if (field?.focus) { + field.focus(remainingPath.join('.')); + } + } + } + renderFields = (multiFields, singleField) => { if (multiFields) { return multiFields.map((f, idx) => this.controlFor(f, idx)); From 9be2693d1b35bf56c7ef05bdece6c24a21ba7567 Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Wed, 29 Jan 2025 12:53:20 +0000 Subject: [PATCH 04/21] fix(widgetsFor): return widgets for variable type lists (#7296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using a Variable Type list widget and a custom preview component, the `widgetsFor` helper would only return a `data` list with each of the items in the list, not a `widgets` list, e.g. {"data" => {"markdown" => "# Title"} {"type" => "block_body"} } {"widgets" => undefined} 🚫 The `widgets` list should also be supplied, particularly for nested Markdown widgets, so a fully formatted preview can be rendered: {"data" => {"markdown" => "# Title"} {"type" => "block_body"} } {"widgets" => {"markdown" => Object} ✅ } This extends support in `widgetsFor` to detect variable type list widgets and correctly construct the `widgets` return value. As reported at https://github.com/decaporg/decap-cms/issues/2307#issuecomment-638326225 Co-authored-by: Anze Demsar --- .../EditorPreviewPane/EditorPreviewPane.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js index c35fc642fe39..39424176ac95 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js @@ -169,9 +169,28 @@ export class PreviewPane extends React.Component { const { fields, entry, fieldsMetaData } = this.props; const field = fields.find(f => f.get('name') === name); const nestedFields = field && field.get('fields'); + const variableTypes = field && field.get('types'); const value = entry.getIn(['data', field.get('name')]); const metadata = fieldsMetaData.get(field.get('name'), Map()); + // Variable Type lists + if (List.isList(value) && variableTypes) { + return value.map(val => { + const valueType = variableTypes.find(t => t.get('name') === val.get('type')); + const typeFields = valueType && valueType.get('fields'); + const widgets = + typeFields && + Map( + typeFields.map((f, i) => [ + f.get('name'), +
{this.getWidget(f, val, metadata.get(f.get('name')), this.props)}
, + ]), + ); + return Map({ data: val, widgets }); + }); + } + + // List widgets if (List.isList(value)) { return value.map(val => { const widgets = From e2165450e5963b2a8d76d4cf331319bf91f05d45 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 29 Jan 2025 14:31:47 +0100 Subject: [PATCH 05/21] chore(release): publish - decap-cms@3.6.0 - decap-cms-app@3.6.0 - decap-cms-core@3.6.0 - decap-cms-lib-util@3.2.0 - decap-cms-widget-list@3.3.0 - decap-cms-widget-markdown@3.3.0 - decap-cms-widget-object@3.3.0 - decap-server@3.2.0 --- package-lock.json | 30 +++++++++---------- packages/decap-cms-app/CHANGELOG.md | 4 +++ packages/decap-cms-app/package.json | 12 ++++---- packages/decap-cms-core/CHANGELOG.md | 11 +++++++ packages/decap-cms-core/package.json | 2 +- packages/decap-cms-lib-util/CHANGELOG.md | 6 ++++ packages/decap-cms-lib-util/package.json | 2 +- packages/decap-cms-widget-list/CHANGELOG.md | 6 ++++ packages/decap-cms-widget-list/package.json | 2 +- .../decap-cms-widget-markdown/CHANGELOG.md | 10 +++++++ .../decap-cms-widget-markdown/package.json | 2 +- packages/decap-cms-widget-object/CHANGELOG.md | 6 ++++ packages/decap-cms-widget-object/package.json | 2 +- packages/decap-cms/CHANGELOG.md | 4 +++ packages/decap-cms/package.json | 4 +-- packages/decap-server/CHANGELOG.md | 4 +++ packages/decap-server/package.json | 4 +-- 17 files changed, 81 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c1729a629cd..f9b0522802f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33162,12 +33162,12 @@ } }, "packages/decap-cms": { - "version": "3.5.0", + "version": "3.6.0", "license": "MIT", "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.5.0", + "decap-cms-app": "^3.6.0", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", @@ -33177,7 +33177,7 @@ } }, "packages/decap-cms-app": { - "version": "3.5.0", + "version": "3.6.0", "license": "MIT", "dependencies": { "@emotion/react": "^11.11.1", @@ -33193,10 +33193,10 @@ "decap-cms-backend-gitlab": "^3.2.2", "decap-cms-backend-proxy": "^3.1.4", "decap-cms-backend-test": "^3.1.3", - "decap-cms-core": "^3.5.0", + "decap-cms-core": "^3.6.0", "decap-cms-editor-component-image": "^3.1.3", "decap-cms-lib-auth": "^3.0.5", - "decap-cms-lib-util": "^3.1.0", + "decap-cms-lib-util": "^3.2.0", "decap-cms-lib-widgets": "^3.1.0", "decap-cms-locales": "^3.3.0", "decap-cms-ui-default": "^3.1.4", @@ -33206,11 +33206,11 @@ "decap-cms-widget-datetime": "^3.2.3", "decap-cms-widget-file": "^3.1.3", "decap-cms-widget-image": "^3.1.3", - "decap-cms-widget-list": "^3.2.2", + "decap-cms-widget-list": "^3.3.0", "decap-cms-widget-map": "^3.1.4", - "decap-cms-widget-markdown": "^3.2.0", + "decap-cms-widget-markdown": "^3.3.0", "decap-cms-widget-number": "^3.1.3", - "decap-cms-widget-object": "^3.2.0", + "decap-cms-widget-object": "^3.3.0", "decap-cms-widget-relation": "^3.3.2", "decap-cms-widget-select": "^3.2.2", "decap-cms-widget-string": "^3.1.3", @@ -33419,7 +33419,7 @@ } }, "packages/decap-cms-core": { - "version": "3.5.0", + "version": "3.6.0", "license": "MIT", "dependencies": { "@iarna/toml": "2.2.5", @@ -33568,7 +33568,7 @@ } }, "packages/decap-cms-lib-util": { - "version": "3.1.0", + "version": "3.2.0", "license": "MIT", "dependencies": { "js-sha256": "^0.9.0", @@ -33713,7 +33713,7 @@ } }, "packages/decap-cms-widget-list": { - "version": "3.2.2", + "version": "3.3.0", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.0.8", @@ -33749,7 +33749,7 @@ } }, "packages/decap-cms-widget-markdown": { - "version": "3.2.0", + "version": "3.3.0", "license": "MIT", "dependencies": { "dompurify": "^2.2.6", @@ -33894,7 +33894,7 @@ } }, "packages/decap-cms-widget-object": { - "version": "3.2.0", + "version": "3.3.0", "license": "MIT", "peerDependencies": { "@emotion/react": "^11.11.1", @@ -33966,7 +33966,7 @@ } }, "packages/decap-server": { - "version": "3.1.2", + "version": "3.2.0", "license": "MIT", "dependencies": { "@hapi/joi": "^17.0.2", @@ -33990,7 +33990,7 @@ "@types/morgan": "^1.7.37", "@types/node": "^16.0.0", "@types/vfile-message": "^2.0.0", - "decap-cms-lib-util": "^3.1.0", + "decap-cms-lib-util": "^3.2.0", "jest": "^27.0.0", "nodemon": "^2.0.2", "ts-jest": "^27.0.0", diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md index 181afcbb5112..d66e9e1fcaf9 100644 --- a/packages/decap-cms-app/CHANGELOG.md +++ b/packages/decap-cms-app/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.5.0...decap-cms-app@3.6.0) (2025-01-29) + +**Note:** Version bump only for package decap-cms-app + # [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.4.0...decap-cms-app@3.5.0) (2025-01-15) ### Bug Fixes diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json index 52fa65eca3c2..91d4ad502e76 100644 --- a/packages/decap-cms-app/package.json +++ b/packages/decap-cms-app/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-app", "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.", - "version": "3.5.0", + "version": "3.6.0", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -40,10 +40,10 @@ "decap-cms-backend-gitlab": "^3.2.2", "decap-cms-backend-proxy": "^3.1.4", "decap-cms-backend-test": "^3.1.3", - "decap-cms-core": "^3.5.0", + "decap-cms-core": "^3.6.0", "decap-cms-editor-component-image": "^3.1.3", "decap-cms-lib-auth": "^3.0.5", - "decap-cms-lib-util": "^3.1.0", + "decap-cms-lib-util": "^3.2.0", "decap-cms-lib-widgets": "^3.1.0", "decap-cms-locales": "^3.3.0", "decap-cms-ui-default": "^3.1.4", @@ -53,11 +53,11 @@ "decap-cms-widget-datetime": "^3.2.3", "decap-cms-widget-file": "^3.1.3", "decap-cms-widget-image": "^3.1.3", - "decap-cms-widget-list": "^3.2.2", + "decap-cms-widget-list": "^3.3.0", "decap-cms-widget-map": "^3.1.4", - "decap-cms-widget-markdown": "^3.2.0", + "decap-cms-widget-markdown": "^3.3.0", "decap-cms-widget-number": "^3.1.3", - "decap-cms-widget-object": "^3.2.0", + "decap-cms-widget-object": "^3.3.0", "decap-cms-widget-relation": "^3.3.2", "decap-cms-widget-select": "^3.2.2", "decap-cms-widget-string": "^3.1.3", diff --git a/packages/decap-cms-core/CHANGELOG.md b/packages/decap-cms-core/CHANGELOG.md index 9a4a6376dace..24ff23f1b1c5 100644 --- a/packages/decap-cms-core/CHANGELOG.md +++ b/packages/decap-cms-core/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.5.0...decap-cms-core@3.6.0) (2025-01-29) + +### Bug Fixes + +- **widgetsFor:** return widgets for variable type lists ([#7296](https://github.com/decaporg/decap-cms/issues/7296)) ([9be2693](https://github.com/decaporg/decap-cms/commit/9be2693d1b35bf56c7ef05bdece6c24a21ba7567)), closes [/github.com/decaporg/decap-cms/issues/2307#issuecomment-638326225](https://github.com//github.com/decaporg/decap-cms/issues/2307/issues/issuecomment-638326225) + +### Features + +- **nested collections:** allow non-index files ([#7359](https://github.com/decaporg/decap-cms/issues/7359)) ([47a2f70](https://github.com/decaporg/decap-cms/commit/47a2f70ef788ae8e61bbbc0ac21e00d68d0029d0)), closes [#4972](https://github.com/decaporg/decap-cms/issues/4972) +- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb)) + # [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.4.2...decap-cms-core@3.5.0) (2024-11-12) ### Bug Fixes diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json index 87f3566d17cf..68e8aff56787 100644 --- a/packages/decap-cms-core/package.json +++ b/packages/decap-cms-core/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-core", "description": "Decap CMS core application, see decap-cms package for the main distribution.", - "version": "3.5.0", + "version": "3.6.0", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core", "bugs": "https://github.com/decaporg/decap-cms/issues", "module": "dist/esm/index.js", diff --git a/packages/decap-cms-lib-util/CHANGELOG.md b/packages/decap-cms-lib-util/CHANGELOG.md index 9bc04cb12103..c29a03a2b3d2 100644 --- a/packages/decap-cms-lib-util/CHANGELOG.md +++ b/packages/decap-cms-lib-util/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-lib-util@3.1.0...decap-cms-lib-util@3.2.0) (2025-01-29) + +### Features + +- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb)) + # [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-lib-util@3.0.4...decap-cms-lib-util@3.1.0) (2024-08-07) ### Bug Fixes diff --git a/packages/decap-cms-lib-util/package.json b/packages/decap-cms-lib-util/package.json index 92ab5ecdffc6..2f9c59073b49 100644 --- a/packages/decap-cms-lib-util/package.json +++ b/packages/decap-cms-lib-util/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-lib-util", "description": "Shared utilities for Decap CMS.", - "version": "3.1.0", + "version": "3.2.0", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util", "bugs": "https://github.com/decaporg/decap-cms/issues", "module": "dist/esm/index.js", diff --git a/packages/decap-cms-widget-list/CHANGELOG.md b/packages/decap-cms-widget-list/CHANGELOG.md index a334c95bca10..c886c4c6438a 100644 --- a/packages/decap-cms-widget-list/CHANGELOG.md +++ b/packages/decap-cms-widget-list/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.2.2...decap-cms-widget-list@3.3.0) (2025-01-29) + +### Features + +- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb)) + ## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.2.1...decap-cms-widget-list@3.2.2) (2024-08-13) ### Reverts diff --git a/packages/decap-cms-widget-list/package.json b/packages/decap-cms-widget-list/package.json index 5c4860a0bb32..e312aa084726 100644 --- a/packages/decap-cms-widget-list/package.json +++ b/packages/decap-cms-widget-list/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-widget-list", "description": "Widget for editing lists in Decap CMS.", - "version": "3.2.2", + "version": "3.3.0", "homepage": "https://www.decapcms.org/docs/widgets/#list", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list", "bugs": "https://github.com/decaporg/decap-cms/issues", diff --git a/packages/decap-cms-widget-markdown/CHANGELOG.md b/packages/decap-cms-widget-markdown/CHANGELOG.md index 245d0cf027f3..56921b948e83 100644 --- a/packages/decap-cms-widget-markdown/CHANGELOG.md +++ b/packages/decap-cms-widget-markdown/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.2.0...decap-cms-widget-markdown@3.3.0) (2025-01-29) + +### Bug Fixes + +- **markdown:** convert inline CSS from Google Docs to Markdown ([#7351](https://github.com/decaporg/decap-cms/issues/7351)) ([8b8e873](https://github.com/decaporg/decap-cms/commit/8b8e873af9a0749720ec03cadbc4b0d391ad84e1)) + +### Features + +- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb)) + # [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.6...decap-cms-widget-markdown@3.2.0) (2024-11-12) ### Bug Fixes diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index b174ffdaeac5..3f382d0e03df 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-widget-markdown", "description": "Widget for editing markdown in Decap CMS.", - "version": "3.2.0", + "version": "3.3.0", "homepage": "https://www.decapcms.org/docs/widgets/#markdown", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown", "bugs": "https://github.com/decaporg/decap-cms/issues", diff --git a/packages/decap-cms-widget-object/CHANGELOG.md b/packages/decap-cms-widget-object/CHANGELOG.md index c7bfc1350685..d09dc463e6c8 100644 --- a/packages/decap-cms-widget-object/CHANGELOG.md +++ b/packages/decap-cms-widget-object/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.2.0...decap-cms-widget-object@3.3.0) (2025-01-29) + +### Features + +- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb)) + # [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.1.4...decap-cms-widget-object@3.2.0) (2025-01-15) ### Bug Fixes diff --git a/packages/decap-cms-widget-object/package.json b/packages/decap-cms-widget-object/package.json index 1bfddf240d0a..291ff0e4165f 100644 --- a/packages/decap-cms-widget-object/package.json +++ b/packages/decap-cms-widget-object/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-widget-object", "description": "Widget for displaying an object of fields for Decap CMS.", - "version": "3.2.0", + "version": "3.3.0", "homepage": "https://www.decapcms.org/docs/widgets/#object", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-object", "bugs": "https://github.com/decaporg/decap-cms/issues", diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md index b8a3511b2dc8..34d526226e88 100644 --- a/packages/decap-cms/CHANGELOG.md +++ b/packages/decap-cms/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.5.0...decap-cms@3.6.0) (2025-01-29) + +**Note:** Version bump only for package decap-cms + # [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.4.0...decap-cms@3.5.0) (2025-01-15) ### Bug Fixes diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json index 3b46f8be8dc9..fba911e2cdc6 100644 --- a/packages/decap-cms/package.json +++ b/packages/decap-cms/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms", "description": "An extensible, open source, Git-based, React CMS for static sites.", - "version": "3.5.0", + "version": "3.6.0", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -22,7 +22,7 @@ "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.5.0", + "decap-cms-app": "^3.6.0", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", diff --git a/packages/decap-server/CHANGELOG.md b/packages/decap-server/CHANGELOG.md index 3cadf6e587a8..8715efd9cbc2 100644 --- a/packages/decap-server/CHANGELOG.md +++ b/packages/decap-server/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-server@3.1.2...decap-server@3.2.0) (2025-01-29) + +**Note:** Version bump only for package decap-server + ## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-server@3.1.1...decap-server@3.1.2) (2024-08-13) ### Reverts diff --git a/packages/decap-server/package.json b/packages/decap-server/package.json index e6bb8bb8a828..61b582d03e4b 100644 --- a/packages/decap-server/package.json +++ b/packages/decap-server/package.json @@ -1,7 +1,7 @@ { "name": "decap-server", "description": "Proxy server to be used with Decap CMS proxy backend", - "version": "3.1.2", + "version": "3.2.0", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-server", "bugs": "https://github.com/decaporg/decap-cms/issues", "license": "MIT", @@ -39,7 +39,7 @@ "@types/morgan": "^1.7.37", "@types/node": "^16.0.0", "@types/vfile-message": "^2.0.0", - "decap-cms-lib-util": "^3.1.0", + "decap-cms-lib-util": "^3.2.0", "jest": "^27.0.0", "nodemon": "^2.0.2", "ts-jest": "^27.0.0", From 3f5461c4e854ce3b9a7fb08974e270d54f5fccff Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 30 Jan 2025 11:43:26 +0100 Subject: [PATCH 06/21] fix(ObjectControl): hotfix nested object validation (#7385) --- packages/decap-cms-widget-object/src/ObjectControl.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index 0d717f3e287c..fcd9c32281c0 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -79,7 +79,12 @@ export default class ObjectControl extends React.Component { if (field.get('widget') === 'hidden') return; const name = field.get('name'); const control = this.childRefs[name]; - control?.validate?.(); + + if (control?.innerWrappedControl?.validate) { + control.innerWrappedControl.validate(); + } else { + control?.validate?.(); + } }); }; From 269a14a58fc2af2afe4db5a1eb09300bb3cfe738 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 30 Jan 2025 11:45:36 +0100 Subject: [PATCH 07/21] chore(release): publish - decap-cms@3.6.1 - decap-cms-app@3.6.1 - decap-cms-widget-object@3.3.1 --- package-lock.json | 10 +++++----- packages/decap-cms-app/CHANGELOG.md | 4 ++++ packages/decap-cms-app/package.json | 4 ++-- packages/decap-cms-widget-object/CHANGELOG.md | 6 ++++++ packages/decap-cms-widget-object/package.json | 2 +- packages/decap-cms/CHANGELOG.md | 4 ++++ packages/decap-cms/package.json | 4 ++-- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9b0522802f7..9e95cd0d0586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33162,12 +33162,12 @@ } }, "packages/decap-cms": { - "version": "3.6.0", + "version": "3.6.1", "license": "MIT", "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.6.0", + "decap-cms-app": "^3.6.1", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", @@ -33177,7 +33177,7 @@ } }, "packages/decap-cms-app": { - "version": "3.6.0", + "version": "3.6.1", "license": "MIT", "dependencies": { "@emotion/react": "^11.11.1", @@ -33210,7 +33210,7 @@ "decap-cms-widget-map": "^3.1.4", "decap-cms-widget-markdown": "^3.3.0", "decap-cms-widget-number": "^3.1.3", - "decap-cms-widget-object": "^3.3.0", + "decap-cms-widget-object": "^3.3.1", "decap-cms-widget-relation": "^3.3.2", "decap-cms-widget-select": "^3.2.2", "decap-cms-widget-string": "^3.1.3", @@ -33894,7 +33894,7 @@ } }, "packages/decap-cms-widget-object": { - "version": "3.3.0", + "version": "3.3.1", "license": "MIT", "peerDependencies": { "@emotion/react": "^11.11.1", diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md index d66e9e1fcaf9..8d356fbd1a7a 100644 --- a/packages/decap-cms-app/CHANGELOG.md +++ b/packages/decap-cms-app/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.6.1](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.6.0...decap-cms-app@3.6.1) (2025-01-30) + +**Note:** Version bump only for package decap-cms-app + # [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.5.0...decap-cms-app@3.6.0) (2025-01-29) **Note:** Version bump only for package decap-cms-app diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json index 91d4ad502e76..12a9a84bf74d 100644 --- a/packages/decap-cms-app/package.json +++ b/packages/decap-cms-app/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-app", "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.", - "version": "3.6.0", + "version": "3.6.1", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -57,7 +57,7 @@ "decap-cms-widget-map": "^3.1.4", "decap-cms-widget-markdown": "^3.3.0", "decap-cms-widget-number": "^3.1.3", - "decap-cms-widget-object": "^3.3.0", + "decap-cms-widget-object": "^3.3.1", "decap-cms-widget-relation": "^3.3.2", "decap-cms-widget-select": "^3.2.2", "decap-cms-widget-string": "^3.1.3", diff --git a/packages/decap-cms-widget-object/CHANGELOG.md b/packages/decap-cms-widget-object/CHANGELOG.md index d09dc463e6c8..2ac5456251fa 100644 --- a/packages/decap-cms-widget-object/CHANGELOG.md +++ b/packages/decap-cms-widget-object/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.3.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.3.0...decap-cms-widget-object@3.3.1) (2025-01-30) + +### Bug Fixes + +- **ObjectControl:** hotfix nested object validation ([#7385](https://github.com/decaporg/decap-cms/issues/7385)) ([3f5461c](https://github.com/decaporg/decap-cms/commit/3f5461c4e854ce3b9a7fb08974e270d54f5fccff)) + # [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.2.0...decap-cms-widget-object@3.3.0) (2025-01-29) ### Features diff --git a/packages/decap-cms-widget-object/package.json b/packages/decap-cms-widget-object/package.json index 291ff0e4165f..1b5e6489913d 100644 --- a/packages/decap-cms-widget-object/package.json +++ b/packages/decap-cms-widget-object/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-widget-object", "description": "Widget for displaying an object of fields for Decap CMS.", - "version": "3.3.0", + "version": "3.3.1", "homepage": "https://www.decapcms.org/docs/widgets/#object", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-object", "bugs": "https://github.com/decaporg/decap-cms/issues", diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md index 34d526226e88..00e2dd203d73 100644 --- a/packages/decap-cms/CHANGELOG.md +++ b/packages/decap-cms/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.6.1](https://github.com/decaporg/decap-cms/compare/decap-cms@3.6.0...decap-cms@3.6.1) (2025-01-30) + +**Note:** Version bump only for package decap-cms + # [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.5.0...decap-cms@3.6.0) (2025-01-29) **Note:** Version bump only for package decap-cms diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json index fba911e2cdc6..c402b96c4d96 100644 --- a/packages/decap-cms/package.json +++ b/packages/decap-cms/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms", "description": "An extensible, open source, Git-based, React CMS for static sites.", - "version": "3.6.0", + "version": "3.6.1", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -22,7 +22,7 @@ "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.6.0", + "decap-cms-app": "^3.6.1", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", From 08f0ccb4a46c8320969af0a52b4c217da928c607 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Fri, 31 Jan 2025 14:41:56 +0100 Subject: [PATCH 08/21] chore: update to upload-artifact@v4 (#7388) --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 15d945ebcfc0..e224906029dc 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -52,7 +52,7 @@ jobs: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} NODE_OPTIONS: --max-old-space-size=4096 TZ: Europe/Amsterdam - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x' && failure() with: name: cypress-results From b85ec3d900ab56782be76875830f33d217064e8e Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 13 Feb 2025 08:26:30 +0100 Subject: [PATCH 09/21] feat(nested collections) legacy mode (#7387) * feat(nested-collections): opt-in to legacy nested folder behaviour * style: lint * feat(nested-collection): default subfolders to true * test(nested-collections): keep the tests in the subfolders: false scenario --- dev-test/config.yml | 4 ++-- packages/decap-cms-core/index.d.ts | 1 + .../Collection/Entries/EntriesCollection.js | 16 ++++++++++++++-- .../Entries/__tests__/EntriesCollection.spec.js | 6 ++++-- .../__snapshots__/EntriesCollection.spec.js.snap | 4 ++-- .../components/Collection/NestedCollection.js | 13 +++++++++++-- .../__tests__/NestedCollection.spec.js | 3 +++ .../decap-cms-core/src/constants/configSchema.js | 1 + packages/decap-cms-core/src/types/redux.ts | 2 +- 9 files changed, 39 insertions(+), 11 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index 592ffbed8b2f..6eac43f98db9 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -62,7 +62,7 @@ collections: # A list of collections the CMS should be able to edit folder: '_restaurants' slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' - create: true # Allow users to create new documents in this collection + create: true # Allow users to create new documents in this collection editor: visualEditing: true fields: # The fields each document in this collection have @@ -276,7 +276,7 @@ collections: # A list of collections the CMS should be able to edit label_singular: 'Page' folder: _pages create: true - nested: { depth: 100 } + nested: { depth: 100, subfolders: false } fields: - label: Title name: title diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts index 9eb5e7f9855e..d5efb55dd7dd 100644 --- a/packages/decap-cms-core/index.d.ts +++ b/packages/decap-cms-core/index.d.ts @@ -310,6 +310,7 @@ declare module 'decap-cms-core' { publish?: boolean; nested?: { depth: number; + subfolders?: boolean; }; meta?: { path?: { label: string; widget: string; index_file: string } }; diff --git a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js index 55aa3c2bf61d..7d7471872d4c 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js @@ -117,7 +117,7 @@ export class EntriesCollection extends React.Component { } } -export function filterNestedEntries(path, collectionFolder, entries) { +export function filterNestedEntries(path, collectionFolder, entries, subfolders) { const filtered = entries.filter(e => { let entryPath = e.get('path').slice(collectionFolder.length + 1); if (!entryPath.startsWith(path)) { @@ -130,6 +130,13 @@ export function filterNestedEntries(path, collectionFolder, entries) { entryPath = entryPath.slice(path.length + 1); } + // if subfolders legacy mode is enabled, show only immediate subfolders + // also show index file in root folder + if (subfolders) { + const depth = entryPath.split('/').length; + return path ? depth === 2 : depth <= 2; + } + // only show immediate children return !entryPath.includes('/'); }); @@ -145,7 +152,12 @@ function mapStateToProps(state, ownProps) { if (collection.has('nested')) { const collectionFolder = collection.get('folder'); - entries = filterNestedEntries(filterTerm || '', collectionFolder, entries); + entries = filterNestedEntries( + filterTerm || '', + collectionFolder, + entries, + collection.get('nested').get('subfolders') !== false, + ); } const entriesLoaded = selectEntriesLoaded(state.entries, collection.get('name')); const isFetching = selectIsFetching(state.entries, collection.get('name')); diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js index 174bc779370b..c1aedfa9c464 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js @@ -115,7 +115,9 @@ describe('EntriesCollection', () => { }); const { asFragment } = renderWithRedux( - , + , { store, }, @@ -140,7 +142,7 @@ describe('EntriesCollection', () => { const { asFragment } = renderWithRedux( , { diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap index c833607a1fff..6066c5547fc1 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap @@ -16,7 +16,7 @@ exports[`EntriesCollection should render show only immediate children for nested { - const leaf = node.children.length === 0 && depth > 0; + const leaf = + depth > 0 && + (subfolders + ? node.children.length <= 1 && !node.children[0]?.isDir + : node.children.length === 0); if (leaf) { return null; } @@ -90,7 +95,11 @@ function TreeNode(props) { } const title = getNodeTitle(node); - const hasChildren = depth === 0 || node.children.some(c => c.isDir); + const hasChildren = + depth === 0 || + (subfolders + ? node.children.some(c => c.children.some(c => c.isDir)) + : node.children.some(c => c.isDir)); return ( diff --git a/packages/decap-cms-core/src/components/Collection/__tests__/NestedCollection.spec.js b/packages/decap-cms-core/src/components/Collection/__tests__/NestedCollection.spec.js index 86380fde54ea..bdeac4040567 100644 --- a/packages/decap-cms-core/src/components/Collection/__tests__/NestedCollection.spec.js +++ b/packages/decap-cms-core/src/components/Collection/__tests__/NestedCollection.spec.js @@ -37,6 +37,9 @@ describe('NestedCollection', () => { label: 'Pages', folder: 'src/pages', fields: [{ name: 'title', widget: 'string' }], + nested: { + subfolders: false, + }, }); it('should render correctly with no entries', () => { diff --git a/packages/decap-cms-core/src/constants/configSchema.js b/packages/decap-cms-core/src/constants/configSchema.js index 5efd2cd4c172..369a4f09341f 100644 --- a/packages/decap-cms-core/src/constants/configSchema.js +++ b/packages/decap-cms-core/src/constants/configSchema.js @@ -260,6 +260,7 @@ function getConfigSchema() { type: 'object', properties: { depth: { type: 'number', minimum: 1, maximum: 1000 }, + subfolders: { type: 'boolean' }, summary: { type: 'string' }, }, required: ['depth'], diff --git a/packages/decap-cms-core/src/types/redux.ts b/packages/decap-cms-core/src/types/redux.ts index b69a82311532..f88f5becdc76 100644 --- a/packages/decap-cms-core/src/types/redux.ts +++ b/packages/decap-cms-core/src/types/redux.ts @@ -582,7 +582,7 @@ export type CollectionFile = StaticallyTypedRecord<{ export type CollectionFiles = List; -type NestedObject = { depth: number }; +type NestedObject = { depth: number; subfolders?: boolean }; type Nested = StaticallyTypedRecord; From d10b9848a31ae3a7b59c3f403d839d062b1f876f Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 13 Feb 2025 08:31:34 +0100 Subject: [PATCH 10/21] chore(release): publish - decap-cms@3.6.2 - decap-cms-app@3.6.2 - decap-cms-core@3.6.1 --- package-lock.json | 10 +++++----- packages/decap-cms-app/CHANGELOG.md | 4 ++++ packages/decap-cms-app/package.json | 4 ++-- packages/decap-cms-core/CHANGELOG.md | 4 ++++ packages/decap-cms-core/package.json | 2 +- packages/decap-cms/CHANGELOG.md | 4 ++++ packages/decap-cms/package.json | 4 ++-- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e95cd0d0586..a286b69edb9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33162,12 +33162,12 @@ } }, "packages/decap-cms": { - "version": "3.6.1", + "version": "3.6.2", "license": "MIT", "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.6.1", + "decap-cms-app": "^3.6.2", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", @@ -33177,7 +33177,7 @@ } }, "packages/decap-cms-app": { - "version": "3.6.1", + "version": "3.6.2", "license": "MIT", "dependencies": { "@emotion/react": "^11.11.1", @@ -33193,7 +33193,7 @@ "decap-cms-backend-gitlab": "^3.2.2", "decap-cms-backend-proxy": "^3.1.4", "decap-cms-backend-test": "^3.1.3", - "decap-cms-core": "^3.6.0", + "decap-cms-core": "^3.6.1", "decap-cms-editor-component-image": "^3.1.3", "decap-cms-lib-auth": "^3.0.5", "decap-cms-lib-util": "^3.2.0", @@ -33419,7 +33419,7 @@ } }, "packages/decap-cms-core": { - "version": "3.6.0", + "version": "3.6.1", "license": "MIT", "dependencies": { "@iarna/toml": "2.2.5", diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md index 8d356fbd1a7a..28fa0c6d3628 100644 --- a/packages/decap-cms-app/CHANGELOG.md +++ b/packages/decap-cms-app/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.6.2](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.6.1...decap-cms-app@3.6.2) (2025-02-13) + +**Note:** Version bump only for package decap-cms-app + ## [3.6.1](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.6.0...decap-cms-app@3.6.1) (2025-01-30) **Note:** Version bump only for package decap-cms-app diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json index 12a9a84bf74d..b65e22bcd6b3 100644 --- a/packages/decap-cms-app/package.json +++ b/packages/decap-cms-app/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-app", "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.", - "version": "3.6.1", + "version": "3.6.2", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -40,7 +40,7 @@ "decap-cms-backend-gitlab": "^3.2.2", "decap-cms-backend-proxy": "^3.1.4", "decap-cms-backend-test": "^3.1.3", - "decap-cms-core": "^3.6.0", + "decap-cms-core": "^3.6.1", "decap-cms-editor-component-image": "^3.1.3", "decap-cms-lib-auth": "^3.0.5", "decap-cms-lib-util": "^3.2.0", diff --git a/packages/decap-cms-core/CHANGELOG.md b/packages/decap-cms-core/CHANGELOG.md index 24ff23f1b1c5..5a0dc70199df 100644 --- a/packages/decap-cms-core/CHANGELOG.md +++ b/packages/decap-cms-core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.6.1](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.6.0...decap-cms-core@3.6.1) (2025-02-13) + +**Note:** Version bump only for package decap-cms-core + # [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.5.0...decap-cms-core@3.6.0) (2025-01-29) ### Bug Fixes diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json index 68e8aff56787..e65f70c8ef3a 100644 --- a/packages/decap-cms-core/package.json +++ b/packages/decap-cms-core/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-core", "description": "Decap CMS core application, see decap-cms package for the main distribution.", - "version": "3.6.0", + "version": "3.6.1", "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core", "bugs": "https://github.com/decaporg/decap-cms/issues", "module": "dist/esm/index.js", diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md index 00e2dd203d73..747fe60417fe 100644 --- a/packages/decap-cms/CHANGELOG.md +++ b/packages/decap-cms/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.6.2](https://github.com/decaporg/decap-cms/compare/decap-cms@3.6.1...decap-cms@3.6.2) (2025-02-13) + +**Note:** Version bump only for package decap-cms + ## [3.6.1](https://github.com/decaporg/decap-cms/compare/decap-cms@3.6.0...decap-cms@3.6.1) (2025-01-30) **Note:** Version bump only for package decap-cms diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json index c402b96c4d96..20c8efdef43c 100644 --- a/packages/decap-cms/package.json +++ b/packages/decap-cms/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms", "description": "An extensible, open source, Git-based, React CMS for static sites.", - "version": "3.6.1", + "version": "3.6.2", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -22,7 +22,7 @@ "dependencies": { "codemirror": "^5.46.0", "create-react-class": "^15.7.0", - "decap-cms-app": "^3.6.1", + "decap-cms-app": "^3.6.2", "decap-cms-media-library-cloudinary": "^3.0.3", "decap-cms-media-library-uploadcare": "^3.0.2", "file-loader": "^6.2.0", From 86d41d79b20583041570bce30b46835a36bcecf2 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Fri, 7 Mar 2025 09:54:33 +0100 Subject: [PATCH 11/21] fix(nested-i18n): pass newPath only when customPath differs from current path (#7418) --- packages/decap-cms-core/src/backend.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index bcb8881de045..52dfb538a0f5 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -1128,12 +1128,13 @@ export class Backend { updateAssetProxies(assetProxies, config, collection, entryDraft, path); } else { const slug = entryDraft.getIn(['entry', 'slug']); + const path = entryDraft.getIn(['entry', 'path']); dataFile = { - path: entryDraft.getIn(['entry', 'path']), + path, // for workflow entries we refresh the slug on publish slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug, raw: this.entryToRaw(collection, entryDraft.get('entry')), - newPath: customPath, + newPath: customPath === path ? undefined : customPath, }; } From 1e6768a096237ae029044310898ca5c35bbf4de1 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Thu, 20 Mar 2025 13:40:46 +0100 Subject: [PATCH 12/21] feat: processing wip --- .../src/actions/editorialWorkflow.ts | 12 +++++-- .../decap-cms-core/src/actions/entries.ts | 6 ++-- packages/decap-cms-core/src/backend.ts | 32 ++++++++++++------- .../src/components/App/StackToolbar.js | 8 +++++ .../src/components/Editor/Editor.js | 29 +++++++++++++++-- .../src/components/Editor/EditorToolbar.js | 8 +++++ .../src/components/Workflow/WorkflowList.js | 23 +++++++++++-- .../src/constants/publishModes.ts | 1 + packages/decap-cms-locales/src/en/index.js | 6 ++++ .../src/ListItemTopBar.js | 23 +++++++++---- packages/decap-cms-ui-default/src/styles.js | 3 ++ .../decap-cms-widget-list/src/ListControl.js | 22 +++++++++++++ 12 files changed, 143 insertions(+), 30 deletions(-) diff --git a/packages/decap-cms-core/src/actions/editorialWorkflow.ts b/packages/decap-cms-core/src/actions/editorialWorkflow.ts index b19914c98937..20b996d2efe2 100644 --- a/packages/decap-cms-core/src/actions/editorialWorkflow.ts +++ b/packages/decap-cms-core/src/actions/editorialWorkflow.ts @@ -39,6 +39,7 @@ import type { AnyAction } from 'redux'; import type { EntryValue } from '../valueObjects/Entry'; import type { Status } from '../constants/publishModes'; import type { ThunkDispatch } from 'redux-thunk'; +import type { HookContext } from '../backend'; /* * Constant Declarations @@ -67,6 +68,8 @@ export const UNPUBLISHED_ENTRY_DELETE_REQUEST = 'UNPUBLISHED_ENTRY_DELETE_REQUES export const UNPUBLISHED_ENTRY_DELETE_SUCCESS = 'UNPUBLISHED_ENTRY_DELETE_SUCCESS'; export const UNPUBLISHED_ENTRY_DELETE_FAILURE = 'UNPUBLISHED_ENTRY_DELETE_FAILURE'; +export const EDITORIAL_WORKFLOW_DISMISS_ERROR = 'EDITORIAL_WORKFLOW_DISMISS_ERROR'; + /* * Simple Action Creators (Internal) */ @@ -271,6 +274,7 @@ export function loadUnpublishedEntry(collection: Collection, slug: string) { dispatch(unpublishedEntryLoaded(collection, entry)); dispatch(createDraftFromEntry(entry)); } catch (error) { + if (error.name === EDITORIAL_WORKFLOW_DISMISS_ERROR) return if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) { dispatch(unpublishedEntryRedirected(collection, slug)); dispatch(loadEntry(collection, slug)); @@ -392,6 +396,7 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis navigateToEntry(collection.get('name'), newSlug); } } catch (error) { + if (error.name === EDITORIAL_WORKFLOW_DISMISS_ERROR) return; dispatch( addNotification({ message: { @@ -483,7 +488,7 @@ export function deleteUnpublishedEntry(collection: string, slug: string) { export function publishUnpublishedEntry( collectionName: string, slug: string, - publishStack: boolean, + context: HookContext, ) { return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); @@ -493,7 +498,7 @@ export function publishUnpublishedEntry( const isDeleteWorkflow = entry.get('isDeleteWorkflow'); dispatch(unpublishedEntryPublishRequest(collectionName, slug)); try { - if (!publishStack && state.stack.status.status) { + if (!context.publishStack && state.stack.status.status) { dispatch( addNotification({ message: { @@ -507,7 +512,7 @@ export function publishUnpublishedEntry( return dispatch(unpublishedEntryPublishError(collectionName, slug)); } - await backend.publishUnpublishedEntry(entry, publishStack); + await backend.publishUnpublishedEntry(entry, context); await dispatch(checkStackStatus()); @@ -537,6 +542,7 @@ export function publishUnpublishedEntry( return dispatch(loadEntry(collection, slug)); } } catch (error) { + if (error.name === EDITORIAL_WORKFLOW_DISMISS_ERROR) return; dispatch( addNotification({ message: { key: 'ui.toast.onFailToPublishEntry', details: error }, diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index d3fa33cf845b..8800383f3873 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -37,7 +37,7 @@ import type { Entry, } from '../types/redux'; import type { EntryValue } from '../valueObjects/Entry'; -import type { Backend } from '../backend'; +import type { Backend, HookContext } from '../backend'; import type AssetProxy from '../valueObjects/AssetProxy'; import type { Set } from 'immutable'; @@ -885,7 +885,7 @@ export function getSerializedEntry(collection: Collection, entry: Entry) { return serializedEntry; } -export function persistEntry(collection: Collection, publishStack?: boolean) { +export function persistEntry(collection: Collection, context: HookContext) { return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const entryDraft = state.entryDraft; @@ -929,7 +929,7 @@ export function persistEntry(collection: Collection, publishStack?: boolean) { entryDraft: serializedEntryDraft, assetProxies, usedSlugs, - publishStack, + context, }) .then(async (newSlug: string) => { dispatch( diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index 62e30340943c..6c1cd5ccb9af 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -276,9 +276,9 @@ interface PersistArgs { entryDraft: EntryDraft; assetProxies: AssetProxy[]; usedSlugs: List; - publishStack?: boolean; unpublished?: boolean; status?: string; + context?: HookContext; } interface ImplementationInitOptions { @@ -291,6 +291,11 @@ type Implementation = BackendImplementation & { init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation; }; +export interface HookContext { + publishStack?: boolean; + actions?: Record; +} + function prepareMetaPath(path: string, collection: Collection) { if (!selectHasMetaPath(collection)) { return path; @@ -703,7 +708,7 @@ export class Backend { } const merged = mergeExpandedEntries(hits); - return { query: searchTerm, hits: merged }; + return { query: searchTerm, hits: merged, collection }; } traverseCursor(cursor: Cursor, action: string) { @@ -1103,9 +1108,9 @@ export class Backend { entryDraft: draft, assetProxies, usedSlugs, - publishStack = false, unpublished = false, status, + context, }: PersistArgs) { const updatedEntity = await this.invokePreSaveEvent(draft.get('entry')); @@ -1191,12 +1196,12 @@ export class Backend { commitMessage, collectionName, useWorkflow, - publishStack, + publishStack: context?.publishStack || false, ...updatedOptions, }; if (!useWorkflow) { - await this.invokePrePublishEvent(entryDraft.get('entry')); + await this.invokePrePublishEvent(entryDraft.get('entry'), context); } await this.implementation.persistEntry( @@ -1216,13 +1221,16 @@ export class Backend { return slug; } - async invokeEventWithEntry(event: string, entry: EntryMap) { + async invokeEventWithEntry(event: string, entry: EntryMap, context: HookContext = {}) { const { login, name } = (await this.currentUser()) as User; - return await invokeEvent({ name: event, data: { entry, author: { login, name } } }); + return await invokeEvent({ + name: event, + data: { entry, author: { login, name }, context }, + }); } - async invokePrePublishEvent(entry: EntryMap) { - await this.invokeEventWithEntry('prePublish', entry); + async invokePrePublishEvent(entry: EntryMap, context?: HookContext) { + await this.invokeEventWithEntry('prePublish', entry, context); } async invokePostPublishEvent(entry: EntryMap) { @@ -1329,11 +1337,11 @@ export class Backend { return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus); } - async publishUnpublishedEntry(entry: EntryMap, publishStack?: boolean) { + async publishUnpublishedEntry(entry: EntryMap, context: HookContext) { const collection = entry.get('collection'); const slug = entry.get('slug'); - await this.invokePrePublishEvent(entry); + await this.invokePrePublishEvent(entry, context); const config = this.config; if (config.backend.stack) { @@ -1350,7 +1358,7 @@ export class Backend { ); await this.implementation.publishUnpublishedEntryStack!(collection, slug, { stackCommitMessage, - publishStack, + publishStack: context.publishStack, }); } else { await this.implementation.publishUnpublishedEntry!(collection, slug); diff --git a/packages/decap-cms-core/src/components/App/StackToolbar.js b/packages/decap-cms-core/src/components/App/StackToolbar.js index 7ff2ce385889..f7653fe2bc1f 100644 --- a/packages/decap-cms-core/src/components/App/StackToolbar.js +++ b/packages/decap-cms-core/src/components/App/StackToolbar.js @@ -90,6 +90,13 @@ const StatusButton = styled(DropdownButton)` background-color: ${colorsRaw.tealLight}; color: ${colorsRaw.teal}; + ${props => + props.label === 'processing' && + css` + background-color: ${colors.processingBackground}; + color: ${colors.processingText}; + `} + ${props => props.label === 'stale' && css` @@ -164,6 +171,7 @@ export class EditorToolbar extends React.Component { [status.get('DRAFT')]: t('editor.editorToolbar.draft'), [status.get('PENDING_REVIEW')]: t('editor.editorToolbar.inReview'), [status.get('PENDING_PUBLISH')]: t('editor.editorToolbar.ready'), + [status.get('PROCESSING')]: t('editor.editorToolbar.inProcessing'), [status.get('STALE')]: t('editor.editorToolbar.inStale'), }; diff --git a/packages/decap-cms-core/src/components/Editor/Editor.js b/packages/decap-cms-core/src/components/Editor/Editor.js index 385c95733505..619571b13311 100644 --- a/packages/decap-cms-core/src/components/Editor/Editor.js +++ b/packages/decap-cms-core/src/components/Editor/Editor.js @@ -210,6 +210,10 @@ export class Editor extends React.Component { handleChangeStatus = newStatusName => { const { entryDraft, updateUnpublishedEntryStatus, collection, slug, currentStatus, t } = this.props; + if (currentStatus === status.get('PROCESSING')) { + window.alert(t('editor.editor.onProcessingUpdate')); + return; + } if (entryDraft.get('hasChanged')) { window.alert(t('editor.editor.onUpdatingWithUnsavedChanges')); return; @@ -224,6 +228,15 @@ export class Editor extends React.Component { // deleteLocalBackup(collection, !newEntry && slug); // } + createHookContext = (context) => { + const defaultContext = { + handleChangeStatus: this.handleChangeStatus + } + if (!context) return defaultContext + + return Object.assign(defaultContext, context) + } + handlePersistEntry = async (opts = {}) => { const { createNew = false, duplicate = false, publishStack = false } = opts; const { @@ -237,7 +250,7 @@ export class Editor extends React.Component { entryDraft, } = this.props; - await persistEntry(collection, publishStack); + await persistEntry(collection, this.createHookContext({ publishStack })); // this.deleteBackup(); @@ -279,7 +292,7 @@ export class Editor extends React.Component { return; } - await publishUnpublishedEntry(collection.get('name'), slug, publishStack); + await publishUnpublishedEntry(collection.get('name'), slug, this.createHookContext({ publishStack })); // this.deleteBackup(); @@ -307,7 +320,11 @@ export class Editor extends React.Component { }; handleDeleteEntry = () => { - const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props; + const { entryDraft, newEntry, collection, deleteEntry, slug, currentStatus, t } = this.props; + if (currentStatus === status.get('PROCESSING')) { + window.alert(t('editor.editor.onProcessingUpdate')); + return; + } if (entryDraft.get('hasChanged')) { if (!window.confirm(t('editor.editor.onDeleteWithUnsavedChanges'))) { return; @@ -331,6 +348,7 @@ export class Editor extends React.Component { entryDraft, collection, slug, + currentStatus, removeAssets, removeDraftEntryMediaFiles, deleteUnpublishedEntry, @@ -339,6 +357,11 @@ export class Editor extends React.Component { isDeleteWorkflow, t, } = this.props; + if (currentStatus === status.get('PROCESSING')) { + window.alert(t('editor.editor.onProcessingUpdate')); + return; + } + if ( entryDraft.get('hasChanged') && !window.confirm( diff --git a/packages/decap-cms-core/src/components/Editor/EditorToolbar.js b/packages/decap-cms-core/src/components/Editor/EditorToolbar.js index c139a31737d1..911f857d44db 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorToolbar.js +++ b/packages/decap-cms-core/src/components/Editor/EditorToolbar.js @@ -219,6 +219,13 @@ const StatusButton = styled(DropdownButton)` background-color: ${colorsRaw.tealLight}; color: ${colorsRaw.teal}; + ${props => + props.label === 'processing' && + css` + background-color: ${colors.processingBackground}; + color: ${colors.processingText}; + `} + ${props => props.label === 'stale' && css` @@ -390,6 +397,7 @@ export class EditorToolbar extends React.Component { [status.get('DRAFT')]: t('editor.editorToolbar.draft'), [status.get('PENDING_REVIEW')]: t('editor.editorToolbar.inReview'), [status.get('PENDING_PUBLISH')]: t('editor.editorToolbar.ready'), + [status.get('PROCESSING')]: t('editor.editorToolbar.inProcessing'), [status.get('STALE')]: t('editor.editorToolbar.inStale'), }; diff --git a/packages/decap-cms-core/src/components/Workflow/WorkflowList.js b/packages/decap-cms-core/src/components/Workflow/WorkflowList.js index 855a79a2a211..87b6b8a9266f 100644 --- a/packages/decap-cms-core/src/components/Workflow/WorkflowList.js +++ b/packages/decap-cms-core/src/components/Workflow/WorkflowList.js @@ -15,7 +15,7 @@ import { selectEntryCollectionTitle } from '../../reducers/collections'; const WorkflowListContainer = styled.div` min-height: 60%; display: grid; - grid-template-columns: 25% 25% 25% 25%; + grid-template-columns: 20% 20% 20% 20% 20%; `; const WorkflowListContainerOpenAuthoring = styled.div` @@ -105,6 +105,13 @@ const ColumnHeader = styled.h2` color: ${colors.statusReadyText}; `} + ${props => + props.name === 'processing' && + css` + background-color: ${colors.processingBackground}; + color: ${colors.processingText}; + `} + ${props => props.name === 'stale' && css` @@ -128,12 +135,14 @@ function getColumnHeaderText(columnName, t) { switch (columnName) { case 'draft': return t('workflow.workflowList.draftHeader'); - case 'stale': - return t('workflow.workflowList.inStaleHeader'); case 'pending_review': return t('workflow.workflowList.inReviewHeader'); case 'pending_publish': return t('workflow.workflowList.readyHeader'); + case 'processing': + return t('workflow.workflowList.inProcessingHeader'); + case 'stale': + return t('workflow.workflowList.inStaleHeader'); } } @@ -152,6 +161,10 @@ class WorkflowList extends React.Component { const slug = dragProps.slug; const collection = dragProps.collection; const oldStatus = dragProps.ownStatus; + if (oldStatus === 'processing') { + window.alert(this.props.t('workflow.workflowList.onProcessingUpdate')) + return; + } if (newStatus === 'stale') { window.alert(this.props.t('workflow.workflowList.onStaleUpdate')); return; @@ -160,6 +173,10 @@ class WorkflowList extends React.Component { }; requestDelete = (collection, slug, ownStatus) => { + if (ownStatus === 'processing') { + window.alert(this.props.t('workflow.workflowList.onProcessingUpdate')) + return; + } if (window.confirm(this.props.t('workflow.workflowList.onDeleteEntry'))) { this.props.handleDelete(collection, slug, ownStatus); } diff --git a/packages/decap-cms-core/src/constants/publishModes.ts b/packages/decap-cms-core/src/constants/publishModes.ts index 62be9769ff49..a64fd344ee66 100644 --- a/packages/decap-cms-core/src/constants/publishModes.ts +++ b/packages/decap-cms-core/src/constants/publishModes.ts @@ -8,6 +8,7 @@ export const Statues = { DRAFT: 'draft', PENDING_REVIEW: 'pending_review', PENDING_PUBLISH: 'pending_publish', + PROCESSING: 'processing', STALE: 'stale', }; diff --git a/packages/decap-cms-locales/src/en/index.js b/packages/decap-cms-locales/src/en/index.js index ede9afcb9294..901c56a5daf3 100644 --- a/packages/decap-cms-locales/src/en/index.js +++ b/packages/decap-cms-locales/src/en/index.js @@ -114,6 +114,8 @@ const en = { 'All changes to this entry will be deleted.\n\n Do you still want to delete?', loadingEntry: 'Loading entry...', confirmLoadBackup: 'A local backup was recovered for this entry, would you like to use it?', + onProcessingUpdate: + "Entry can't change the status while processing!\n\n Please wait until the process end.", onStackPublishing: 'Are you sure you want to publish all changes?', onStackClosing: 'Are you sure you want to discard all changes?', }, @@ -152,6 +154,7 @@ const en = { draft: 'Draft', inReview: 'In review', ready: 'Ready', + inProcessing: 'Processing', inStale: 'Stale', publishNow: 'Publish now', stackChange: 'Stack change', @@ -322,6 +325,9 @@ const en = { draftHeader: 'Drafts', inReviewHeader: 'In Review', readyHeader: 'Ready', + inProcessingHeader: 'Processing', + onProcessingUpdate: + "Entry can't change the status while processing!\n\n Please wait until the process end.", inStaleHeader: 'Stale', onStaleUpdate: "Entry can't be manually updated to stale status! Please discard changes insted of using stale status.", diff --git a/packages/decap-cms-ui-default/src/ListItemTopBar.js b/packages/decap-cms-ui-default/src/ListItemTopBar.js index cc09183b6652..dfa1d2d0b346 100644 --- a/packages/decap-cms-ui-default/src/ListItemTopBar.js +++ b/packages/decap-cms-ui-default/src/ListItemTopBar.js @@ -28,6 +28,10 @@ const TopBarButton = styled.button` align-items: center; `; +const TopBarButtonGroups = styled.div` + display: flex; +`; + const TopBarButtonSpan = TopBarButton.withComponent('span'); const DragIconContainer = styled(TopBarButtonSpan)` @@ -46,7 +50,7 @@ function DragHandle({ Wrapper, id }) { } function ListItemTopBar(props) { - const { className, collapsed, onCollapseToggle, onRemove, dragHandle, id } = props; + const { className, collapsed, onCollapseToggle, onDuplicate, onRemove, dragHandle, id } = props; return ( {onCollapseToggle ? ( @@ -55,11 +59,18 @@ function ListItemTopBar(props) { ) : null} {dragHandle ? : null} - {onRemove ? ( - - - - ) : null} + + {onDuplicate ? ( + + + + ) : null} + {onRemove ? ( + + + + ) : null} + ); } diff --git a/packages/decap-cms-ui-default/src/styles.js b/packages/decap-cms-ui-default/src/styles.js index 684eb78019ea..ae9266248acc 100644 --- a/packages/decap-cms-ui-default/src/styles.js +++ b/packages/decap-cms-ui-default/src/styles.js @@ -42,6 +42,7 @@ const colorsRaw = { greenLight: '#caef6f', brown: '#754e00', yellow: '#ffee9c', + yellowDark: '#ffe150', red: '#ff003b', redDark: '#D60032', redLight: '#fcefea', @@ -59,6 +60,8 @@ const colors = { statusReviewBackground: colorsRaw.yellow, statusReadyText: colorsRaw.green, statusReadyBackground: colorsRaw.greenLight, + processingBackground: colorsRaw.yellowDark, + processingText: colorsRaw.grayDark, staleBackground: colorsRaw.redDark, staleText: colorsRaw.redLight, text: colorsRaw.gray, diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 497dcbdd6483..73c52af8e78c 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -433,6 +433,26 @@ export default class ListControl extends React.Component { }; } + handleDuplicate = (index, event) => { + event.preventDefault(); + const { value, onChange } = this.props; + const { itemsCollapsed } = this.state; + + // Create new arrays with the item inserted at index + 1 + const newItemsCollapsed = [...itemsCollapsed]; + const newKeys = [...this.state.keys]; + + newItemsCollapsed.splice(index + 1, 0, false); // Insert expanded state + newKeys.splice(index + 1, 0, uuid()); // Insert new key + + this.setState({ + itemsCollapsed: newItemsCollapsed, + keys: newKeys + }); + + onChange(value.insert(index + 1, value.get(index))); + }; + handleRemove = (index, event) => { event.preventDefault(); const { itemsCollapsed } = this.state; @@ -623,6 +643,7 @@ export default class ListControl extends React.Component { onCollapseToggle={partial(this.handleItemCollapseToggle, index)} dragHandle={DragHandle} id={key} + onDuplicate={partial(this.handleDuplicate, index)} onRemove={partial(this.handleRemove, index)} data-testid={`styled-list-item-top-bar-${key}`} /> @@ -675,6 +696,7 @@ export default class ListControl extends React.Component { > Date: Thu, 20 Mar 2025 13:41:00 +0100 Subject: [PATCH 13/21] feat: processing wip --- .../Editor/EditorControlPane/EditorControl.js | 3 +++ .../components/Editor/EditorControlPane/Widget.js | 2 ++ packages/decap-cms-core/src/lib/registry.js | 12 ++++++++++++ packages/decap-cms-ui-default/src/Toggle.js | 6 +++++- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index ba79e235cf8b..304abc8febc5 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -205,6 +205,7 @@ class EditorControl extends React.Component { value, entry, collection, + collections, config, field, fieldsMetaData, @@ -337,6 +338,7 @@ class EditorControl extends React.Component { controlComponent={widget.control} entry={entry} collection={collection} + collections={collections} config={config} field={field} uniqueFieldId={this.uniqueFieldId} @@ -439,6 +441,7 @@ function mapStateToProps(state) { config: state.config, entry, collection, + collections: state.collections, isLoadingAsset, loadEntry, validateMetaField: (field, value, t) => validateMetaField(state, collection, field, value, t), diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js index 6d0bcac29050..b831a7725a64 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -265,6 +265,7 @@ export default class Widget extends Component { controlComponent, entry, collection, + collections, config, field, value, @@ -315,6 +316,7 @@ export default class Widget extends Component { return React.createElement(controlComponent, { entry, collection, + collections, config, field, value, diff --git a/packages/decap-cms-core/src/lib/registry.js b/packages/decap-cms-core/src/lib/registry.js index 573d8867762c..ef094fe37a57 100644 --- a/packages/decap-cms-core/src/lib/registry.js +++ b/packages/decap-cms-core/src/lib/registry.js @@ -1,6 +1,7 @@ import { Map } from 'immutable'; import { produce } from 'immer'; import { oneLine } from 'common-tags'; +import * as immutable from 'immutable'; import EditorComponent from '../valueObjects/EditorComponent'; @@ -17,10 +18,15 @@ allowedEvents.forEach(e => { eventHandlers[e] = []; }); +const lib = { + immutable, +} + /** * Global Registry Object */ const registry = { + lib, backends: {}, templates: {}, previewStyles: [], @@ -35,6 +41,7 @@ const registry = { }; export default { + getLib, registerPreviewStyle, getPreviewStyles, registerPreviewTemplate, @@ -65,6 +72,10 @@ export default { getCustomFormatsFormatters, }; +export function getLib() { + return registry.lib; +} + /** * Preview Styles * @@ -310,3 +321,4 @@ export function getCustomFormatsFormatters() { export function getFormatter(name) { return registry.formats[name]?.formatter; } + diff --git a/packages/decap-cms-ui-default/src/Toggle.js b/packages/decap-cms-ui-default/src/Toggle.js index 46a4d25b9c39..50923f5cd6d7 100644 --- a/packages/decap-cms-ui-default/src/Toggle.js +++ b/packages/decap-cms-ui-default/src/Toggle.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; @@ -57,6 +57,10 @@ function Toggle({ }) { const [isActive, setIsActive] = useState(active); + useEffect(() => { + setIsActive(active); + }, [active]); + function handleToggle() { setIsActive(prevIsActive => !prevIsActive); if (onChange) { From 1be29e4c25ccdf034933fdfb26acf3ae34ee1064 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Mon, 24 Mar 2025 00:04:15 +0100 Subject: [PATCH 14/21] feat: added validation nested fix --- packages/decap-cms-widget-object/src/ObjectControl.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index 536b52615257..a975bbb1d1c6 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -26,9 +26,13 @@ export default class ObjectControl extends React.Component { processControlRef = ref => { if (!ref) return; - const name = ref.props.field.get('name'); - this.childRefs[name] = ref; - this.props.controlRef?.(ref); + const parentId = ref.props.parentIds[ref.props.parentIds.length - 1]; + const belongsToDifferentParent = parentId && this.props.forID && parentId !== this.props.forID; + if (!belongsToDifferentParent) { + const name = ref.props.field.get('name'); + this.childRefs[name] = ref; + } + this.props.controlRef?.(this); }; static propTypes = { From 38f86915309b8dbfb1210206217e9c5c47cc22c1 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Fri, 11 Apr 2025 19:18:57 +0200 Subject: [PATCH 15/21] wip --- .../Editor/EditorControlPane/EditorControl.js | 7 +++++- .../Editor/EditorControlPane/Widget.js | 3 +++ .../decap-cms-widget-list/src/ListControl.js | 25 +++++++++++++++---- .../src/ObjectControl.js | 4 ++- .../src/RelationControl.js | 22 ++++++++++------ .../src/StringControl.js | 6 +++-- 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index b17352ff77b1..a577cc1ccad9 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -12,7 +12,7 @@ import ReactMarkdown from 'react-markdown'; import gfm from 'remark-gfm'; import { List, Map } from 'immutable'; -import { resolveWidget, getEditorComponents } from '../../../lib/registry'; +import { resolveWidget, getEditorComponents, getWidget } from '../../../lib/registry'; import { clearFieldErrors, tryLoadEntry, validateMetaField } from '../../../actions/entries'; import { addAsset, boundGetAsset } from '../../../actions/media'; import { selectIsLoadingAsset } from '../../../reducers/medias'; @@ -70,6 +70,9 @@ const styleStrings = { unused: ` opacity: 0.5; `, + flat: ` + margin-top: 0 !important; + ` }; const ControlContainer = styled.div` @@ -269,6 +272,7 @@ class EditorControl extends React.Component { css={css` ${!this.state.use && unused && styleStrings.unused} ${isHidden && styleStrings.hidden}; + ${isFlat && styleStrings.flat} `} > {widgetTitle &&

{widgetTitle}

} @@ -362,6 +366,7 @@ class EditorControl extends React.Component { resolveWidget={resolveWidget} widget={widget} getEditorComponents={getEditorComponents} + getWidget={getWidget} controlRef={controlRef} editorControl={ConnectedEditorControl} query={query} diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js index 080f1553b205..7b0c353f7674 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -55,6 +55,7 @@ export default class Widget extends Component { resolveWidget: PropTypes.func.isRequired, widget: PropTypes.object.isRequired, getEditorComponents: PropTypes.func.isRequired, + getWidget: PropTypes.func.isRequired, isFetching: PropTypes.bool, query: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired, @@ -313,6 +314,7 @@ export default class Widget extends Component { resolveWidget, widget, getEditorComponents, + getWidget, query, queryHits, clearSearch, @@ -368,6 +370,7 @@ export default class Widget extends Component { resolveWidget, widget, getEditorComponents, + getWidget, getRemarkPlugins, query, queryHits, diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index be66cce73c92..7b718147de92 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -6,7 +6,6 @@ import { css, ClassNames } from '@emotion/react'; import { List, Map, fromJS } from 'immutable'; import { partial, isEmpty, uniqueId } from 'lodash'; import { v4 as uuid } from 'uuid'; -import DecapCmsWidgetObject from 'decap-cms-widget-object'; import { DndContext, MouseSensor, @@ -34,8 +33,6 @@ import { getErrorMessageForTypedFieldAndValue, } from './typedListHelpers'; -const ObjectControl = DecapCmsWidgetObject.controlComponent; - const ListItem = styled.div(); const StyledListItemTopBar = styled(ListItemTopBar)` @@ -412,7 +409,7 @@ export default class ListControl extends React.Component { */ getObjectValue = idx => this.props.value.get(idx) || Map(); - handleChangeFor(index) { + handleFieldChangeFor(index) { return (f, newValue, newMetadata) => { const { value, metadata, onChange, field } = this.props; const collectionName = field.get('name'); @@ -433,6 +430,17 @@ export default class ListControl extends React.Component { }; } + handleChangeFor(index) { + return (newValue, newMetadata) => { + const { value, metadata, onChange, field } = this.props; + const collectionName = field.get('name'); + const parsedMetadata = { + [collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata || {}), + }; + onChange(value.set(index, newValue), parsedMetadata); + }; + } + handleDuplicate = (index, event) => { event.preventDefault(); const { value, onChange } = this.props; @@ -664,6 +672,8 @@ export default class ListControl extends React.Component { parentIds, forID, t, + collection, + collections, } = this.props; const { itemsCollapsed, keys } = this.state; @@ -679,6 +689,8 @@ export default class ListControl extends React.Component { } } + const ObjectControl = (this.props.getWidget('object')).control; + return ( field.set('parentName', fieldParentName)); const singleField = f.get('field')?.set('parentName', fieldParentName); - return mappedMultiFields.push(...this.renderFields(multiFields, singleField, f)); + const renderedFields = this.renderFields(multiFields, singleField, f); + if (Array.isArray(renderedFields)) return mappedMultiFields.push(...renderedFields); + return mappedMultiFields.push(renderedFields); } return mappedMultiFields.push(this.controlFor(f, idx)); }); diff --git a/packages/decap-cms-widget-relation/src/RelationControl.js b/packages/decap-cms-widget-relation/src/RelationControl.js index c17deca0a536..c9e5be261451 100644 --- a/packages/decap-cms-widget-relation/src/RelationControl.js +++ b/packages/decap-cms-widget-relation/src/RelationControl.js @@ -227,15 +227,12 @@ export default class RelationControl extends React.Component { return ( this.props.value !== nextProps.value || this.props.hasActiveStyle !== nextProps.hasActiveStyle || - this.props.queryHits !== nextProps.queryHits + this.props.queryHits !== nextProps.queryHits || + this.props.field.get('collection') !== nextProps.field.get('collection') ); } - async componentDidMount() { - this.mounted = true; - // if the field has a previous value perform an initial search based on the value field - // this is required since each search is limited by optionsLength so the selected value - // might not show up on the search + async loadInitialOptions() { const { forID, field, value, query, onChange } = this.props; const collection = field.get('collection'); const file = field.get('file'); @@ -272,6 +269,17 @@ export default class RelationControl extends React.Component { } } + async componentDidMount() { + this.mounted = true; + await this.loadInitialOptions(); + } + + async componentDidUpdate(prevProps) { + if (this.props.field.get('collection') !== prevProps.field.get('collection')) { + await this.loadInitialOptions(); + } + } + componentWillUnmount() { this.mounted = false; } @@ -424,7 +432,7 @@ export default class RelationControl extends React.Component { value={selectedValue} inputId={forID} cacheOptions - defaultOptions + defaultOptions={options?.length ? options : true} loadOptions={this.loadOptions} onChange={this.handleChange} className={classNameWrapper} diff --git a/packages/decap-cms-widget-string/src/StringControl.js b/packages/decap-cms-widget-string/src/StringControl.js index 339e9338058f..01910fc36174 100644 --- a/packages/decap-cms-widget-string/src/StringControl.js +++ b/packages/decap-cms-widget-string/src/StringControl.js @@ -29,9 +29,11 @@ export default class StringControl extends React.Component { // The input element ref _el = null; - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState = {}) { return Boolean( - nextState && (this.state.value !== nextState.value || nextProps.value !== nextState.value), + this.props.classNameWrapper !== nextProps.classNameWrapper || + this.state.value !== nextState.value || + nextProps.value !== nextState.value ); } From 9f7d76e80d22fc9e847c05883192f054b7c4174f Mon Sep 17 00:00:00 2001 From: Dfdez Date: Tue, 15 Apr 2025 17:29:35 +0200 Subject: [PATCH 16/21] feat: custom context wip --- packages/decap-cms-core/src/backend.ts | 13 +++++++- .../src/components/Editor/Editor.js | 33 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index 35d14481e9cf..43485f6b1239 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -622,6 +622,17 @@ export class Backend { // Perform a local search by requesting all entries. For each // collection, load it, search, and call onCollectionResults with // its results. + + if (searchTerm === null) { + const allEntries = await Promise.all( + collections.map(async collection => { + const entries = await this.listAllEntries(collection); + return entries; + }) + ); + return { entries: flatten(allEntries) }; + } + const errors: Error[] = []; const collectionEntriesRequests = collections .map(async collection => { @@ -954,7 +965,7 @@ export class Backend { data, dataFile.path, dataFile.newFile, - dataFile.deletedFile, + dataFile.deleteFile, ); return entryWithFormat; }; diff --git a/packages/decap-cms-core/src/components/Editor/Editor.js b/packages/decap-cms-core/src/components/Editor/Editor.js index 619571b13311..59972fe75d0b 100644 --- a/packages/decap-cms-core/src/components/Editor/Editor.js +++ b/packages/decap-cms-core/src/components/Editor/Editor.js @@ -32,6 +32,7 @@ import { } from '../../actions/editorialWorkflow'; import { removeAssets } from '../../actions/media'; import { loadDeployPreview } from '../../actions/deploys'; +import { searchEntries } from '../../actions/search'; import { selectEntry, selectUnpublishedEntry, selectDeployPreview } from '../../reducers'; import { selectFields } from '../../reducers/collections'; import { status, EDITORIAL_WORKFLOW } from '../../constants/publishModes'; @@ -71,6 +72,7 @@ export class Editor extends React.Component { loadDeployPreview: PropTypes.func.isRequired, currentStatus: PropTypes.string, user: PropTypes.object, + searchEntries: PropTypes.func.isRequired, location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string, @@ -230,7 +232,35 @@ export class Editor extends React.Component { createHookContext = (context) => { const defaultContext = { - handleChangeStatus: this.handleChangeStatus + actions: { + navigateToCollection, + searchEntries: this.props.searchEntries, + handleChangeStatus: this.handleChangeStatus, + // loadEntry: (collection, slug) => this.props.loadEntry(collection, slug), + getCollection: (name) => { + return this.props.collections.get(name); + }, + persistEntry: async (collection, entry, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.persistEntry(collection, context); + }, + deleteEntry: async (collection, slug) => { + return this.props.deleteEntry(collection, slug); + }, + publishEntry: async (collection, slug, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.publishUnpublishedEntry(collection.get('name'), slug, context); + }, + unpublishEntry: async (collection, slug) => { + return this.props.unpublishPublishedEntry(collection, slug); + }, + createDraft: (collection, searchParams) => { + return this.props.createEmptyDraft(collection, searchParams); + }, + duplicateEntry: (collection, entry) => { + return this.props.createDraftDuplicateFromEntry(entry); + }, + } } if (!context) return defaultContext @@ -554,6 +584,7 @@ const mapDispatchToProps = { removeAssets, removeDraftEntryMediaFiles, logoutUser, + searchEntries, }; export default connect(mapStateToProps, mapDispatchToProps)(withWorkflow(translate()(Editor))); From 54a6e57fe0eedad5e4d5c0f3210039fe4fa4f7b7 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Tue, 15 Apr 2025 19:52:56 +0200 Subject: [PATCH 17/21] feat: custom context wip --- packages/decap-cms-core/index.d.ts | 8 +++ .../decap-cms-core/src/actions/entries.ts | 68 ++++++++++++------- packages/decap-cms-core/src/backend.ts | 6 +- .../src/components/Editor/Editor.js | 41 +++++------ 4 files changed, 77 insertions(+), 46 deletions(-) diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts index c132a8054d51..ffb9ed93d8ae 100644 --- a/packages/decap-cms-core/index.d.ts +++ b/packages/decap-cms-core/index.d.ts @@ -516,12 +516,20 @@ declare module 'decap-cms-core' { handler: ({ entry, author, + context, }: { entry: Map; author: { login: string; name: string }; + context?: HookContext; }) => any; } + export interface HookContext { + publishStack?: boolean; + actions?: Record; + [key: string]: any; + } + export type CmsEventListenerOptions = any; // TODO: type properly export type CmsLocalePhrases = any; // TODO: type properly diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index 8800383f3873..cd10f505baee 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -35,6 +35,8 @@ import type { ViewFilter, ViewGroup, Entry, + EntryDraft, + Entries, } from '../types/redux'; import type { EntryValue } from '../valueObjects/Entry'; import type { Backend, HookContext } from '../backend'; @@ -885,33 +887,19 @@ export function getSerializedEntry(collection: Collection, entry: Entry) { return serializedEntry; } -export function persistEntry(collection: Collection, context: HookContext) { +export function persistCustomEntry( + collection: Collection, + context: HookContext, + entryDraft: EntryDraft, + entries?: Entries, +) { return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); - const entryDraft = state.entryDraft; - const fieldsErrors = entryDraft.get('fieldsErrors'); - const usedSlugs = selectPublishedSlugs(state, collection.get('name')); - - // Early return if draft contains validation errors - if (!fieldsErrors.isEmpty()) { - const hasPresenceErrors = fieldsErrors.some(errors => - errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), - ); - - if (hasPresenceErrors) { - dispatch( - addNotification({ - message: { - key: 'ui.toast.missingRequiredField', - }, - type: 'error', - dismissAfter: 8000, - }), - ); - } - return Promise.reject(); - } + const usedSlugs = selectPublishedSlugs( + entries ? { ...state, entries: fromJS(entries) } : state, + collection.get('name'), + ); const backend = currentBackend(state.config); const entry = entryDraft.get('entry'); @@ -972,6 +960,38 @@ export function persistEntry(collection: Collection, context: HookContext) { }; } +export function persistEntry(collection: Collection, context: HookContext) { + return async (dispatch: ThunkDispatch, getState: () => State) => { + const state = getState(); + const entryDraft = state.entryDraft; + + const fieldsErrors = entryDraft.get('fieldsErrors'); + + // Early return if draft contains validation errors + if (!fieldsErrors.isEmpty()) { + const hasPresenceErrors = fieldsErrors.some(errors => + errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), + ); + + if (hasPresenceErrors) { + dispatch( + addNotification({ + message: { + key: 'ui.toast.missingRequiredField', + }, + type: 'error', + dismissAfter: 8000, + }), + ); + } + + return Promise.reject(); + } + const persistFunc = persistCustomEntry(collection, context, entryDraft); + return persistFunc(dispatch, getState); + }; +} + export function deleteEntry(collection: Collection, slug: string) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index 43485f6b1239..2fb0ff5f3bd6 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -1123,7 +1123,7 @@ export class Backend { status, context, }: PersistArgs) { - const updatedEntity = await this.invokePreSaveEvent(draft.get('entry')); + const updatedEntity = await this.invokePreSaveEvent(draft.get('entry'), context); let entryDraft; if (updatedEntity.get('data') === undefined) { @@ -1257,8 +1257,8 @@ export class Backend { await this.invokeEventWithEntry('postUnpublish', entry); } - async invokePreSaveEvent(entry: EntryMap) { - return await this.invokeEventWithEntry('preSave', entry); + async invokePreSaveEvent(entry: EntryMap, context?: HookContext) { + return await this.invokeEventWithEntry('preSave', entry, context); } async invokePostSaveEvent(entry: EntryMap) { diff --git a/packages/decap-cms-core/src/components/Editor/Editor.js b/packages/decap-cms-core/src/components/Editor/Editor.js index 59972fe75d0b..f792e9f1682e 100644 --- a/packages/decap-cms-core/src/components/Editor/Editor.js +++ b/packages/decap-cms-core/src/components/Editor/Editor.js @@ -16,6 +16,7 @@ import { discardDraft, changeDraftField, changeDraftFieldValidation, + persistCustomEntry, persistEntry, deleteEntry, // persistLocalBackup, @@ -50,6 +51,7 @@ export class Editor extends React.Component { entry: ImmutablePropTypes.map, entryDraft: ImmutablePropTypes.map.isRequired, loadEntry: PropTypes.func.isRequired, + persistCustomEntry: PropTypes.func.isRequired, persistEntry: PropTypes.func.isRequired, deleteEntry: PropTypes.func.isRequired, showDelete: PropTypes.bool.isRequired, @@ -236,30 +238,30 @@ export class Editor extends React.Component { navigateToCollection, searchEntries: this.props.searchEntries, handleChangeStatus: this.handleChangeStatus, - // loadEntry: (collection, slug) => this.props.loadEntry(collection, slug), getCollection: (name) => { return this.props.collections.get(name); }, - persistEntry: async (collection, entry, opts = {}) => { + persistCustomEntry: async (collection, entry, opts = {}) => { const context = this.createHookContext(opts); - return this.props.persistEntry(collection, context); - }, - deleteEntry: async (collection, slug) => { - return this.props.deleteEntry(collection, slug); - }, - publishEntry: async (collection, slug, opts = {}) => { - const context = this.createHookContext(opts); - return this.props.publishUnpublishedEntry(collection.get('name'), slug, context); - }, - unpublishEntry: async (collection, slug) => { - return this.props.unpublishPublishedEntry(collection, slug); - }, - createDraft: (collection, searchParams) => { - return this.props.createEmptyDraft(collection, searchParams); - }, - duplicateEntry: (collection, entry) => { - return this.props.createDraftDuplicateFromEntry(entry); + const { entries } = (await this.props.searchEntries(null, [collection.get('name')])).payload; + return this.props.persistCustomEntry(collection, context, entry, entries); }, + // deleteEntry: async (collection, slug) => { + // return this.props.deleteEntry(collection, slug); + // }, + // publishEntry: async (collection, slug, opts = {}) => { + // const context = this.createHookContext(opts); + // return this.props.publishUnpublishedEntry(collection.get('name'), slug, context); + // }, + // unpublishEntry: async (collection, slug) => { + // return this.props.unpublishPublishedEntry(collection, slug); + // }, + // createDraft: (collection, searchParams) => { + // return this.props.createEmptyDraft(collection, searchParams); + // }, + // duplicateEntry: (collection, entry) => { + // return this.props.createDraftDuplicateFromEntry(entry); + // }, } } if (!context) return defaultContext @@ -575,6 +577,7 @@ const mapDispatchToProps = { createDraftDuplicateFromEntry, createEmptyDraft, discardDraft, + persistCustomEntry, persistEntry, deleteEntry, updateUnpublishedEntryStatus, From 30c6ec6afa1e4436530e5257d1f5569f4497c061 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Tue, 15 Apr 2025 23:00:20 +0200 Subject: [PATCH 18/21] feat: custom context wip --- .../src/actions/editorialWorkflow.ts | 8 +++-- .../decap-cms-core/src/actions/entries.ts | 4 +-- packages/decap-cms-core/src/backend.ts | 28 ++++++++-------- .../src/components/Editor/Editor.js | 32 ++++++++----------- .../src/components/Editor/withWorkflow.js | 4 +-- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/decap-cms-core/src/actions/editorialWorkflow.ts b/packages/decap-cms-core/src/actions/editorialWorkflow.ts index 20b996d2efe2..c48e39150ae3 100644 --- a/packages/decap-cms-core/src/actions/editorialWorkflow.ts +++ b/packages/decap-cms-core/src/actions/editorialWorkflow.ts @@ -325,7 +325,7 @@ export function loadUnpublishedEntries(collections: Collections) { }; } -export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean) { +export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean, context: HookContext) { return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const entryDraft = state.entryDraft; @@ -379,6 +379,7 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis entryDraft: serializedEntryDraft, assetProxies, usedSlugs, + context, }); dispatch( addNotification({ @@ -555,7 +556,7 @@ export function publishUnpublishedEntry( }; } -export function unpublishPublishedEntry(collection: Collection, slug: string) { +export function unpublishPublishedEntry(collection: Collection, slug: string, context: HookContext) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); @@ -563,7 +564,7 @@ export function unpublishPublishedEntry(collection: Collection, slug: string) { const entryDraft = Map().set('entry', entry) as unknown as EntryDraft; dispatch(unpublishedEntryPersisting(collection, slug)); return backend - .deleteEntry(state, collection, slug) + .deleteEntry(state, collection, slug, context) .then(() => { if (!backend.implementation.deleteCollectionFiles) { backend.persistEntry({ @@ -573,6 +574,7 @@ export function unpublishPublishedEntry(collection: Collection, slug: string) { assetProxies: [], usedSlugs: List(), status: status.get('PENDING_PUBLISH'), + context, }); } }) diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index cd10f505baee..b3b7bb5c9b63 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -992,14 +992,14 @@ export function persistEntry(collection: Collection, context: HookContext) { }; } -export function deleteEntry(collection: Collection, slug: string) { +export function deleteEntry(collection: Collection, slug: string, context: HookContext) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); dispatch(entryDeleting(collection, slug)); return backend - .deleteEntry(state, collection, slug) + .deleteEntry(state, collection, slug, context) .then(async () => { dispatch(entryDeleted(collection, slug)); dispatch( diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index 2fb0ff5f3bd6..f489dd4e060f 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -628,7 +628,7 @@ export class Backend { collections.map(async collection => { const entries = await this.listAllEntries(collection); return entries; - }) + }), ); return { entries: flatten(allEntries) }; } @@ -1224,7 +1224,7 @@ export class Backend { opts, ); - await this.invokePostSaveEvent(entryDraft.get('entry')); + await this.invokePostSaveEvent(entryDraft.get('entry'), context); if (!useWorkflow) { await this.invokePostPublishEvent(entryDraft.get('entry')); @@ -1245,24 +1245,24 @@ export class Backend { await this.invokeEventWithEntry('prePublish', entry, context); } - async invokePostPublishEvent(entry: EntryMap) { - await this.invokeEventWithEntry('postPublish', entry); + async invokePostPublishEvent(entry: EntryMap, context?: HookContext) { + await this.invokeEventWithEntry('postPublish', entry, context); } - async invokePreUnpublishEvent(entry: EntryMap) { - await this.invokeEventWithEntry('preUnpublish', entry); + async invokePreUnpublishEvent(entry: EntryMap, context?: HookContext) { + await this.invokeEventWithEntry('preUnpublish', entry, context); } - async invokePostUnpublishEvent(entry: EntryMap) { - await this.invokeEventWithEntry('postUnpublish', entry); + async invokePostUnpublishEvent(entry: EntryMap, context?: HookContext) { + await this.invokeEventWithEntry('postUnpublish', entry, context); } async invokePreSaveEvent(entry: EntryMap, context?: HookContext) { return await this.invokeEventWithEntry('preSave', entry, context); } - async invokePostSaveEvent(entry: EntryMap) { - await this.invokeEventWithEntry('postSave', entry); + async invokePostSaveEvent(entry: EntryMap, context?: HookContext) { + await this.invokeEventWithEntry('postSave', entry, context); } async persistMedia(config: CmsConfig, file: AssetProxy) { @@ -1282,7 +1282,7 @@ export class Backend { return this.implementation.persistMedia(file, options); } - async deleteEntry(state: State, collection: Collection, slug: string) { + async deleteEntry(state: State, collection: Collection, slug: string, context?: HookContext) { const config = state.config; const path = selectEntryPath(collection, slug) as string; const extension = selectFolderEntryExtension(collection) as string; @@ -1306,7 +1306,7 @@ export class Backend { ); const entry = selectEntry(state.entries, collection.get('name'), slug); - await this.invokePreUnpublishEvent(entry); + await this.invokePreUnpublishEvent(entry, context); let paths = [path]; if (hasI18n(collection)) { paths = getFilePaths(collection, extension, path, slug); @@ -1323,7 +1323,7 @@ export class Backend { await this.implementation.deleteFiles(paths, commitMessage); } - await this.invokePostUnpublishEvent(entry); + await this.invokePostUnpublishEvent(entry, context); } async deleteMedia(config: CmsConfig, path: string) { @@ -1376,7 +1376,7 @@ export class Backend { await this.implementation.publishUnpublishedEntry!(collection, slug); } - await this.invokePostPublishEvent(entry); + await this.invokePostPublishEvent(entry, context); } deleteUnpublishedEntry(collection: string, slug: string) { diff --git a/packages/decap-cms-core/src/components/Editor/Editor.js b/packages/decap-cms-core/src/components/Editor/Editor.js index f792e9f1682e..79bc501085fe 100644 --- a/packages/decap-cms-core/src/components/Editor/Editor.js +++ b/packages/decap-cms-core/src/components/Editor/Editor.js @@ -246,22 +246,18 @@ export class Editor extends React.Component { const { entries } = (await this.props.searchEntries(null, [collection.get('name')])).payload; return this.props.persistCustomEntry(collection, context, entry, entries); }, - // deleteEntry: async (collection, slug) => { - // return this.props.deleteEntry(collection, slug); - // }, - // publishEntry: async (collection, slug, opts = {}) => { - // const context = this.createHookContext(opts); - // return this.props.publishUnpublishedEntry(collection.get('name'), slug, context); - // }, - // unpublishEntry: async (collection, slug) => { - // return this.props.unpublishPublishedEntry(collection, slug); - // }, - // createDraft: (collection, searchParams) => { - // return this.props.createEmptyDraft(collection, searchParams); - // }, - // duplicateEntry: (collection, entry) => { - // return this.props.createDraftDuplicateFromEntry(entry); - // }, + deleteEntry: async (collection, slug, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.deleteEntry(collection, slug, context); + }, + publishEntry: async (collection, slug, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.publishUnpublishedEntry(collection.get('name'), slug, context); + }, + unpublishEntry: async (collection, slug, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.unpublishPublishedEntry(collection, slug, context); + }, } } if (!context) return defaultContext @@ -339,7 +335,7 @@ export class Editor extends React.Component { const { unpublishPublishedEntry, collection, slug, t } = this.props; if (!window.confirm(t('editor.editor.onUnpublishing'))) return; - await unpublishPublishedEntry(collection, slug); + await unpublishPublishedEntry(collection, slug, this.createHookContext()); // return navigateToCollection(collection.get('name')); }; @@ -369,7 +365,7 @@ export class Editor extends React.Component { } setTimeout(async () => { - await deleteEntry(collection, slug); + await deleteEntry(collection, slug, this.createHookContext()); // this.deleteBackup(); // return navigateToCollection(collection.get('name')); }, 0); diff --git a/packages/decap-cms-core/src/components/Editor/withWorkflow.js b/packages/decap-cms-core/src/components/Editor/withWorkflow.js index 47fb52c0cfb8..6869f83c6c86 100644 --- a/packages/decap-cms-core/src/components/Editor/withWorkflow.js +++ b/packages/decap-cms-core/src/components/Editor/withWorkflow.js @@ -35,8 +35,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) { returnObj.loadEntry = (collection, slug) => dispatch(loadUnpublishedEntry(collection, slug)); // Overwrite persistEntry to persistUnpublishedEntry - returnObj.persistEntry = collection => - dispatch(persistUnpublishedEntry(collection, unpublishedEntry)); + returnObj.persistEntry = (collection, context) => + dispatch(persistUnpublishedEntry(collection, unpublishedEntry, context)); } return { From a28db60b98ed5ef5a2acb78c16f0f7134c4685f3 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Wed, 16 Apr 2025 00:24:53 +0200 Subject: [PATCH 19/21] feat: custom context final --- .../src/actions/editorialWorkflow.ts | 78 ++++++++++++------- .../decap-cms-core/src/actions/entries.ts | 4 +- .../src/components/Editor/Editor.js | 14 +++- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/packages/decap-cms-core/src/actions/editorialWorkflow.ts b/packages/decap-cms-core/src/actions/editorialWorkflow.ts index c48e39150ae3..4841b72c9873 100644 --- a/packages/decap-cms-core/src/actions/editorialWorkflow.ts +++ b/packages/decap-cms-core/src/actions/editorialWorkflow.ts @@ -325,39 +325,21 @@ export function loadUnpublishedEntries(collections: Collections) { }; } -export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean, context: HookContext) { +export function persistCustomUnpublishedEntry( + collection: Collection, + existingUnpublishedEntry: boolean, + entryDraft: EntryDraft, + context: HookContext, +) { return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); - const entryDraft = state.entryDraft; - const fieldsErrors = entryDraft.get('fieldsErrors'); const unpublishedSlugs = selectUnpublishedSlugs(state, collection.get('name')); const publishedSlugs = selectPublishedSlugs(state, collection.get('name')); const usedSlugs = publishedSlugs.concat(unpublishedSlugs) as List; const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false); - //load unpublishedEntries !entriesLoaded && dispatch(loadUnpublishedEntries(state.collections)); - // Early return if draft contains validation errors - if (!fieldsErrors.isEmpty()) { - const hasPresenceErrors = fieldsErrors.some(errors => - errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), - ); - - if (hasPresenceErrors) { - dispatch( - addNotification({ - message: { - key: 'ui.toast.missingRequiredField', - }, - type: 'error', - dismissAfter: 8000, - }), - ); - } - return Promise.reject(); - } - const backend = currentBackend(state.config); const entry = entryDraft.get('entry'); const assetProxies = getMediaAssets({ @@ -415,6 +397,37 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis }; } +export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean, context: HookContext) { + return async (dispatch: ThunkDispatch, getState: () => State) => { + const state = getState(); + const entryDraft = state.entryDraft; + const fieldsErrors = entryDraft.get('fieldsErrors'); + + // Early return if draft contains validation errors + if (!fieldsErrors.isEmpty()) { + const hasPresenceErrors = fieldsErrors.some(errors => + errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), + ); + + if (hasPresenceErrors) { + dispatch( + addNotification({ + message: { + key: 'ui.toast.missingRequiredField', + }, + type: 'error', + dismissAfter: 8000, + }), + ); + } + return Promise.reject(); + } + + const persistFunc = persistCustomUnpublishedEntry(collection, existingUnpublishedEntry, entryDraft, context); + return persistFunc(dispatch, getState); + }; +} + export function updateUnpublishedEntryStatus( collection: string, slug: string, @@ -556,11 +569,15 @@ export function publishUnpublishedEntry( }; } -export function unpublishPublishedEntry(collection: Collection, slug: string, context: HookContext) { +export function unpublishCustomPublishedEntry( + collection: Collection, + slug: string, + entry: EntryMap, + context: HookContext, +) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); - const entry = selectEntry(state, collection.get('name'), slug); const entryDraft = Map().set('entry', entry) as unknown as EntryDraft; dispatch(unpublishedEntryPersisting(collection, slug)); return backend @@ -606,3 +623,12 @@ export function unpublishPublishedEntry(collection: Collection, slug: string, co }); }; } + +export function unpublishPublishedEntry(collection: Collection, slug: string, context: HookContext) { + return (dispatch: ThunkDispatch, getState: () => State) => { + const state = getState(); + const entry = selectEntry(state, collection.get('name'), slug); + const unpublishFunc = unpublishCustomPublishedEntry(collection, slug, entry, context); + return unpublishFunc(dispatch, getState); + }; +} diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index b3b7bb5c9b63..4ba19f10e660 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -889,8 +889,8 @@ export function getSerializedEntry(collection: Collection, entry: Entry) { export function persistCustomEntry( collection: Collection, - context: HookContext, entryDraft: EntryDraft, + context: HookContext, entries?: Entries, ) { return async (dispatch: ThunkDispatch, getState: () => State) => { @@ -987,7 +987,7 @@ export function persistEntry(collection: Collection, context: HookContext) { return Promise.reject(); } - const persistFunc = persistCustomEntry(collection, context, entryDraft); + const persistFunc = persistCustomEntry(collection, entryDraft, context); return persistFunc(dispatch, getState); }; } diff --git a/packages/decap-cms-core/src/components/Editor/Editor.js b/packages/decap-cms-core/src/components/Editor/Editor.js index 79bc501085fe..c4f16c5743eb 100644 --- a/packages/decap-cms-core/src/components/Editor/Editor.js +++ b/packages/decap-cms-core/src/components/Editor/Editor.js @@ -241,14 +241,14 @@ export class Editor extends React.Component { getCollection: (name) => { return this.props.collections.get(name); }, - persistCustomEntry: async (collection, entry, opts = {}) => { + persistEntry: async (collection, entry, opts = {}) => { const context = this.createHookContext(opts); const { entries } = (await this.props.searchEntries(null, [collection.get('name')])).payload; return this.props.persistCustomEntry(collection, context, entry, entries); }, - deleteEntry: async (collection, slug, opts = {}) => { + persistUnpublishedEntry: async (collection, existingUnpublishedEntry, entry, opts = {}) => { const context = this.createHookContext(opts); - return this.props.deleteEntry(collection, slug, context); + return this.props.persistCustomUnpublishedEntry(collection, existingUnpublishedEntry, entry, context); }, publishEntry: async (collection, slug, opts = {}) => { const context = this.createHookContext(opts); @@ -258,6 +258,14 @@ export class Editor extends React.Component { const context = this.createHookContext(opts); return this.props.unpublishPublishedEntry(collection, slug, context); }, + unpublishPublishedEntry: async (collection, slug, entry, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.unpublishCustomPublishedEntry(collection, slug, entry, context); + }, + deleteEntry: async (collection, slug, opts = {}) => { + const context = this.createHookContext(opts); + return this.props.deleteEntry(collection, slug, context); + }, } } if (!context) return defaultContext From 89842fc1f0e1fcf062ee1d0e663d8a463abad45c Mon Sep 17 00:00:00 2001 From: Dfdez Date: Thu, 17 Apr 2025 13:44:16 +0200 Subject: [PATCH 20/21] fix: fixed Listcontrol ref --- packages/decap-cms-widget-list/src/ListControl.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 7b718147de92..6237fe8a0cf5 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -667,7 +667,6 @@ export default class ListControl extends React.Component { metadata, clearFieldErrors, fieldsErrors, - controlRef, resolveWidget, parentIds, forID, @@ -742,8 +741,7 @@ export default class ListControl extends React.Component { onValidateObject={onValidateObject} clearFieldErrors={clearFieldErrors} fieldsErrors={fieldsErrors} - ref={this.processControlRef} - controlRef={controlRef} + controlRef={this.processControlRef} validationKey={key} collapsed={collapsed} data-testid={`object-control-${key}`} From 10518b9d47271b3f1fb32b93840520c753a5e873 Mon Sep 17 00:00:00 2001 From: Dfdez Date: Thu, 17 Apr 2025 13:44:33 +0200 Subject: [PATCH 21/21] fix: fixed search fzf --- packages/decap-cms-core/src/backend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index f489dd4e060f..3c1f3f831703 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -146,7 +146,7 @@ export function extractSearchFields(searchFields: string[]) { searchFields.reduce((acc, field) => { const value = getEntryField(field, entry); if (value) { - return `${acc} ${value}`; + return acc ? `${acc} ${value}` : value; } else { return acc; }