diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c8d246e5..8b7aed4fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,65 @@ +## 17.0.0-alpha.14 (2023-06-23) + +### Feature + +- Added slug-based linked headings in `volto-slate`. @tiberiuichim, @nileshgulia1 [#4287](https://github.com/plone/volto/issues/4287) +- Refactored Anontools components. @Tishasoumya-02 [#4845](https://github.com/plone/volto/issues/4845) + +### Bugfix + +- Update to version 6.0.5 of Plone backend. @davisagli [#4897](https://github.com/plone/volto/issues/4897) + + +## 17.0.0-alpha.13 (2023-06-15) + +### Feature + +- Add and enforce a new config setting, `maxFileUploadSize`. @davisagli [#4868](https://github.com/plone/volto/issues/4868) + +### Bugfix + +- Fix and improve the `addStyling` helper @sneridagh [#4880](https://github.com/plone/volto/issues/4880) + + +## 17.0.0-alpha.12 (2023-06-14) + +### Feature + +- Allow to deselect color in ColorPickerWidget. @ksuess [#4838](https://github.com/plone/volto/issues/4838) +- Configurable Container component from registry for some key route views. @sneridagh [#4871](https://github.com/plone/volto/issues/4871) + +### Bugfix + +- Fix regression in horizontal scroll in contents view, add it back @sneridagh [#4872](https://github.com/plone/volto/issues/4872) + + +## 17.0.0-alpha.11 (2023-06-09) + +### Bugfix + +- Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159) + + +## 17.0.0-alpha.10 (2023-06-09) + +### Feature + +- Search Block: Add support for advanced facets that are only displayed on demand. + [pbauer, razvanMiu, claudiaifrim] [#4783](https://github.com/plone/volto/issues/4783) +- Display PAS validation errors. [tschorr] [#4801](https://github.com/plone/volto/issues/4801) +- Added a CSS identifier to the Slate style menu options. @razvanMiu [#4846](https://github.com/plone/volto/issues/4846) +- Use a Container from the registry in the Form component and fallback to the Semantic UI one. @sneridagh [#4849](https://github.com/plone/volto/issues/4849) +- Update Brazilian Portuguese translations @ericof [#4853](https://github.com/plone/volto/issues/4853) + +### Bugfix + +- Convert header class to function. @gomez [#4767](https://github.com/plone/volto/issues/4767) +- Do not break validation on required number field with value 0 @cekk [#4841](https://github.com/plone/volto/issues/4841) + + ## 17.0.0-alpha.9 (2023-06-01) ### Bugfix @@ -238,6 +297,44 @@ - Use a universal static path for both documentation and volto repos. @stevepiercy [#4376](https://github.com/plone/volto/issues/4376) +## 16.21.1 (2023-06-23) + +### Bugfix + +- Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159) + + +## 16.21.0 (2023-06-16) + +### Feature + +- Display PAS validation errors. [tschorr] [#4801](https://github.com/plone/volto/issues/4801) +- Allow to deselect color in ColorPickerWidget. @ksuess [#4838](https://github.com/plone/volto/issues/4838) +- Added a CSS identifier to the Slate style menu options. @razvanMiu [#4846](https://github.com/plone/volto/issues/4846) +- Use a Container from the registry in the Form component and fallback to the Semantic UI one. @sneridagh [#4849](https://github.com/plone/volto/issues/4849) +- Add and enforce a new config setting, `maxFileUploadSize`. @davisagli [#4868](https://github.com/plone/volto/issues/4868) +- Configurable Container component from registry for some key route views. @sneridagh [#4871](https://github.com/plone/volto/issues/4871) + +### Bugfix + +- Do not break validation on required number field with value 0 @cekk [#4841](https://github.com/plone/volto/issues/4841) +- Fix regression in horizontal scroll in contents view, add it back @sneridagh [#4872](https://github.com/plone/volto/issues/4872) +- Fix and improve the `addStyling` helper @sneridagh [#4880](https://github.com/plone/volto/issues/4880) + + +## 16.20.8 (2023-06-01) + +### Bugfix + +- Fix special characters in request urls @pnicolli @mamico @luca-bellenghi @cekk [#4826](https://github.com/plone/volto/issues/4826) +- Fix block is undefined in StyleWrapper helper when building classnames @sneridagh [#4827](https://github.com/plone/volto/issues/4827) +- Fix navigation sections in 404 pages @sneridagh [#4836](https://github.com/plone/volto/issues/4836) + +### Documentation + +- Fix glossary warning due to lack of empty line before a term. @stevepiercy [#4820](https://github.com/plone/volto/issues/4820) + + ## 16.20.7 (2023-05-24) ### Bugfix @@ -1209,6 +1306,7 @@ See https://6.docs.plone.org/volto/upgrade-guide/index.html for more information - Add support for loading core add-ons from the `packages` folder defined in Volto's `package.json` @sneridagh - Implement the Upgrade Control Panel @ericof - Allow addons to customize modules from the project root, via the `@root` namespace and folder @tiberiuichim +- Updated Spanish translation @macagua ### Bugfix diff --git a/Makefile b/Makefile index 17d412f4aa..a094073869 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ MAKEFLAGS+=--no-builtin-rules # Project settings INSTANCE_PORT=8080 -DOCKER_IMAGE=plone/server-dev:6.0.4 -DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:6.0.4 +DOCKER_IMAGE=plone/server-dev:6.0.5 +DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:6.0.5 KGS= NODEBIN = ./node_modules/.bin SCRIPTSPACKAGE = ./packages/scripts diff --git a/README.md b/README.md index 7c32093b7f..c86d75746b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ cd myvoltoproject You can bootstrap a ready Docker Plone container with all the dependencies and ready for Volto use. We recommend to use the Plone docker builds based in `pip` [plone/plone-backend](https://github.com/plone/plone-backend) image: ```shell -docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.1 +docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.5 ``` or as an alternative if you have experience with Plone and you have all the @@ -266,7 +266,7 @@ yarn Either using a Docker command: ```shell -docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.1 +docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.5 ``` or using the convenience makefile command: diff --git a/api/buildout.cfg b/api/buildout.cfg index ead796cfcb..f3d12ae947 100644 --- a/api/buildout.cfg +++ b/api/buildout.cfg @@ -1,7 +1,7 @@ [buildout] index = https://pypi.org/simple/ extends = - http://dist.plone.org/release/6.0.4/versions.cfg + http://dist.plone.org/release/6.0.5/versions.cfg version-constraints.cfg versions.cfg parts = instance plonesite site-packages test robot-server diff --git a/cypress/support/commands.js b/cypress/support/commands.js index cd2e6f23d4..70bd7683a8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -849,3 +849,23 @@ Cypress.Commands.add('getTableSlate', (header = false) => { ); return slate; }); + +Cypress.Commands.add('configureListingWith', (contentType) => { + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + cy.get('.querystring-widget .fields').contains('Add criteria').click(); + cy.get( + '.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option', + ) + .contains('Type') + .click(); + + //insert Page + cy.get('.querystring-widget .fields:first-of-type > .field').click(); + cy.get( + '.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option', + ) + .contains(contentType) + .click(); +}); diff --git a/cypress/tests/core/blocks/block-anchors.js b/cypress/tests/core/blocks/block-anchors.js new file mode 100644 index 0000000000..1166b0ce26 --- /dev/null +++ b/cypress/tests/core/blocks/block-anchors.js @@ -0,0 +1,71 @@ +import { slateBeforeEach } from '../../../support/volto-slate'; + +describe('Block Tests: Anchors', () => { + beforeEach(slateBeforeEach); + + it('Add Block: Links', () => { + // Change page title + cy.clearSlateTitle(); + cy.getSlateTitle().type('Slate Heading Anchors'); + cy.getSlate().click(); + + // Add TOC block + cy.get('.ui.basic.icon.button.block-add-button').first().click(); + cy.get(".blocks-chooser .ui.form .field.searchbox input[type='text']").type( + 'table of contents', + ); + cy.get('.button.toc').click(); + + // Save page + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/my-page'); + cy.get('h1.documentFirstHeading') + .trigger('mouseover', { eventConstructor: 'MouseEvent' }) + .children() + .should('have.length', 1); + }); + + it('Add Block: add content to TOC', () => { + // Change page title + cy.clearSlateTitle(); + cy.getSlateTitle().type('Slate Heading Anchors'); + cy.getSlate().click(); + + // Add TOC block + cy.get('.ui.basic.icon.button.block-add-button').first().click(); + cy.get(".blocks-chooser .ui.form .field.searchbox input[type='text']").type( + 'table of contents', + ); + cy.get('.button.toc').click(); + + // Add headings + cy.get('.ui.drag.block.inner.slate').click().type('Title 1').click(); + cy.get('.ui.drag.block.inner.slate span span span').setSelection('Title 1'); + cy.get('.slate-inline-toolbar .button-wrapper a[title="Title"]').click({ + force: true, + }); + cy.get('.ui.drag.block.inner.slate').click().type('{enter}'); + + cy.get('.ui.drag.block.inner.slate').eq(1).click().type('Title 2').click(); + cy.get('.ui.drag.block.inner.slate span span span') + .eq(1) + .setSelection('Title 2'); + cy.get('.slate-inline-toolbar .button-wrapper a[title="Title"]').click({ + force: true, + }); + cy.get('.ui.drag.block.inner.slate').eq(1).click().type('{enter}'); + + // Save page + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/my-page'); + + // Check if the page contains the TOC and scrolls to each entry on click + cy.contains('Slate Heading Anchors'); + cy.get('h2[id="title-1"]').contains('Title 1'); + cy.get('h2[id="title-2"]').contains('Title 2'); + cy.get('a[href="#title-1"]').click(); + cy.get('h2[id="title-1"]').scrollIntoView().should('be.visible'); + cy.get('a[href="#title-2"]').click(); + cy.get('h2[id="title-2"]').scrollIntoView().should('be.visible'); + }); +}); diff --git a/cypress/tests/core/blocks/blocks-listing.js b/cypress/tests/core/blocks/blocks-listing.js index 583738bed4..2e08c03053 100644 --- a/cypress/tests/core/blocks/blocks-listing.js +++ b/cypress/tests/core/blocks/blocks-listing.js @@ -21,6 +21,42 @@ describe('Listing Block Tests', () => { cy.waitForResourceToLoad(''); }); + it('Add Listing block with no results', () => { + cy.intercept('PATCH', '/**/my-page').as('save'); + cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page-test', + contentTitle: 'My Page Test', + path: 'my-page', + }); + + cy.navigate('/my-page'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type('My title'); + + //add listing block + cy.addNewBlock('listing'); + + cy.configureListingWith('News Item'); + + //save + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + + //test after save + cy.get('#page-document .block.listing.default .emptyListing').contains( + 'No results found.', + ); + }); + it('Add Listing block', () => { cy.intercept('PATCH', '/**/my-page').as('save'); cy.intercept('GET', '/**/my-page').as('content'); @@ -841,6 +877,354 @@ describe('Listing Block Tests', () => { cy.get('.listing-item h2').first().contains('My Folder 3'); }); + it('Navigates in listing block pagination and url clears on logo click', () => { + cy.intercept('PATCH', '/**/my-page').as('save'); + cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder', + contentTitle: 'My Folder', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder2', + contentTitle: 'My Folder 2', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder3', + contentTitle: 'My Folder 3', + path: 'my-page', + }); + + cy.navigate('/my-page'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type('Listing block - navigate to second page'); + + //add listing block + cy.addNewBlock('listing'); + + //verify before save + cy.get(`.block.listing .listing-body:first-of-type`).contains('My Folder'); + + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + cy.get('.querystring-widget .fields').contains('Add criteria').click(); + cy.get( + '.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option', + ) + .contains('Type') + .click(); + + //insert Page + cy.get('.querystring-widget .fields:first-of-type > .field').click(); + cy.get( + '.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option', + ) + .contains('Page') + .click(); + + cy.get('#field-limit-3-querystring').click().type('2'); + + cy.get('#field-limit-3-querystring').click().clear().type('0'); + cy.get('#field-b_size-4-querystring').click().type('2'); + cy.get('.ui.pagination.menu a[value="2"]').first().click(); + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + //test second pagination click + cy.get('.ui.pagination.menu a[value="2"]').first().click({ force: true }); + cy.url().should('include', '?page=2'); + //on logo click go to home page and remove ?page=2 from path + cy.get('.logo').first().click(); + cy.url().should('not.include', '?page=2'); + }); + + // reload url when ?page=2 + it('Reload path when listing page = 2', () => { + cy.intercept('PATCH', '/**/my-page').as('save'); + cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder', + contentTitle: 'My Folder', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder2', + contentTitle: 'My Folder 2', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder3', + contentTitle: 'My Folder 3', + path: 'my-page', + }); + + cy.navigate('/my-page'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type( + 'Listing block - navigate to second page and reload path', + ); + + //add listing block + cy.addNewBlock('listing'); + + //verify before save + cy.get(`.block.listing .listing-body:first-of-type`).contains('My Folder'); + + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + cy.get('.querystring-widget .fields').contains('Add criteria').click(); + cy.get( + '.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option', + ) + .contains('Type') + .click(); + + //insert Page + cy.get('.querystring-widget .fields:first-of-type > .field').click(); + cy.get( + '.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option', + ) + .contains('Page') + .click(); + + cy.get('#field-limit-3-querystring').click().type('2'); + + //save + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + + //test after save + cy.get('#page-document .listing-item:first-of-type a').should( + 'have.attr', + 'href', + '/my-page/my-folder', + ); + cy.get('.listing-item').should(($els) => { + expect($els).to.have.length(2); + }); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.get('.block-editor-listing').click(); + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + + cy.get('#field-limit-3-querystring').click().clear().type('0'); + cy.get('#field-b_size-4-querystring').click().type('2'); + cy.get('.ui.pagination.menu a[value="2"]').first().click(); + + cy.get('.listing-item h4').first().contains('My Folder 3'); + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + //test second pagination click + cy.get('.ui.pagination.menu a[value="2"]').first().click(); + + //test f5 + cy.reload(); + cy.url().should('include', '?page=2'); + }); + + // different page in two listings on the same page + it('Navigate to different pages on two different listings', () => { + cy.intercept('PATCH', '/**/my-page').as('save'); + cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder', + contentTitle: 'My Folder', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder2', + contentTitle: 'My Folder 2', + path: 'my-page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder3', + contentTitle: 'My Folder 3', + path: 'my-page', + }); + + cy.navigate('/my-page'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type( + 'Listing block - navigate to different pages on two different listings', + ); + + //add listing block + cy.addNewBlock('listing'); + + //verify before save + cy.get(`.block.listing .listing-body:first-of-type`).contains('My Folder'); + + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + cy.get('.querystring-widget .fields').contains('Add criteria').click(); + cy.get( + '.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option', + ) + .contains('Type') + .click(); + + //insert Page + cy.get('.querystring-widget .fields:first-of-type > .field').click(); + cy.get( + '.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option', + ) + .contains('Page') + .click(); + + cy.get('#field-limit-3-querystring').click().type('0'); + cy.get('#field-b_size-4-querystring').click().type('2'); + + cy.addNewBlock('listing'); + + //verify before save + cy.get(`.block.listing .listing-body:first-of-type`).contains('My Folder'); + + cy.get('.sidebar-container .tabs-wrapper .menu .item') + .contains('Block') + .click(); + cy.get('.querystring-widget .fields').contains('Add criteria').click(); + cy.get( + '.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option', + ) + .contains('Type') + .click(); + + //insert Page + cy.get('.querystring-widget .fields:first-of-type > .field').click(); + cy.get( + '.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option', + ) + .contains('Page') + .click(); + + cy.get('#field-limit-3-querystring').click().type('0'); + cy.get('#field-b_size-4-querystring').click().type('1'); + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + + // const listing1 = cy.get('.ui.pagination.menu').first(); + // cy.log('listing1', listing1); + //test second pagination click + cy.get('.ui.pagination.menu a[value="2"]').first().click(); + //test f5 + cy.reload(); + cy.url().should('include', '=2'); + // const listing2 = cy.get('.ui.pagination.menu').last(); + //test third pagination click on second listing + cy.get('.ui.pagination.menu a[value="3"]').first().click(); + //test f5 + cy.reload(); + cy.url().should('include', '=2'); + cy.url().should('include', '=3'); + //on logo click go to home page and remove ?page=2 from path + cy.get('.logo').first().click(); + cy.url().should('not.include', '=2'); + cy.url().should('not.include', '=3'); + //test back button + cy.navigate('/my-page'); + cy.wait('@content'); + cy.get('.ui.pagination.menu a[value="2"]').first().click({ force: true }); + cy.get('.ui.pagination.menu a[value="3"]').first().click({ force: true }); + cy.go(-1); + cy.url().should('not.include', '=3'); + cy.go(-1); + cy.url().should('not.include', '=2'); + cy.url().should('not.include', '=3'); + }); + + it('Add Listing block with no results, navigate to home, add a News Item, go to the listing', () => { + cy.intercept('PATCH', '/**/my-page').as('save'); + cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page-test', + contentTitle: 'My Page Test', + path: 'my-page', + }); + + cy.navigate('/my-page'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type('My title'); + + //add listing block + cy.addNewBlock('listing'); + + cy.configureListingWith('News Item'); + + //save + cy.get('#toolbar-save').click(); + cy.wait('@save'); + cy.wait('@content'); + + //test after save + cy.get('#page-document .block.listing.default .emptyListing').contains( + 'No results found.', + ); + + cy.get('.logo').first().click(); + + cy.createContent({ + contentType: 'News Item', + contentId: 'my-news-item-test', + contentTitle: 'My News Item', + path: 'my-page', + }); + cy.navigate('/my-page'); + + cy.get('#page-document .listing-body:first-of-type').contains( + 'My News Item', + ); + cy.get('#page-document .listing-item:first-of-type a').should( + 'have.attr', + 'href', + '/my-page/my-news-item-test', + ); + }); + // it('Listing block - Test Criteria: Location Navigation', () => { // /*not implemented because Navigation ui is not yet developed in Listing Block sidebar*/ // }); diff --git a/cypress/tests/core/guillotina/blocks-text.js b/cypress/tests/core/guillotina/blocks-text.js index 8ca7b815b8..334c3b3c32 100644 --- a/cypress/tests/core/guillotina/blocks-text.js +++ b/cypress/tests/core/guillotina/blocks-text.js @@ -53,7 +53,7 @@ describe('Text Block Tests', () => { // then the page view should contain a link cy.contains('Colorless green ideas sleep furiously.'); - cy.get('#page-document a') + cy.get('#page-document p a') .should('have.attr', 'href') .and('include', 'https://google.com'); }); @@ -81,7 +81,7 @@ describe('Text Block Tests', () => { // then the page view should contain a mailto link cy.contains('Colorless green ideas sleep furiously.'); - cy.get('#page-document a') + cy.get('#page-document p a') .should('have.attr', 'href') .and('include', 'mailto:hello@example.com'); }); diff --git a/cypress/tests/core/volto-slate/05-block-slate-format-basics.js b/cypress/tests/core/volto-slate/05-block-slate-format-basics.js index 413d43a21b..3d32b822e4 100644 --- a/cypress/tests/core/volto-slate/05-block-slate-format-basics.js +++ b/cypress/tests/core/volto-slate/05-block-slate-format-basics.js @@ -135,7 +135,7 @@ describe('Block Tests: Basic text format', () => { cy.toolbarSave(); // then the page view should contain our changes - cy.get('[id="page-document"] h2').children().should('have.length', 0); + cy.get('[id="page-document"] h2').children().should('have.length', 1); cy.get('[id="page-document"] h2').contains('Colorless'); }); @@ -177,7 +177,7 @@ describe('Block Tests: Basic text format', () => { cy.toolbarSave(); // then the page view should contain our changes - cy.get('[id="page-document"] h3').children().should('have.length', 0); + cy.get('[id="page-document"] h3').children().should('have.length', 1); cy.get('[id="page-document"] h3').contains('Colorless'); }); diff --git a/cypress/tests/guillotina/blocks-text.js b/cypress/tests/guillotina/blocks-text.js index 8ca7b815b8..334c3b3c32 100644 --- a/cypress/tests/guillotina/blocks-text.js +++ b/cypress/tests/guillotina/blocks-text.js @@ -53,7 +53,7 @@ describe('Text Block Tests', () => { // then the page view should contain a link cy.contains('Colorless green ideas sleep furiously.'); - cy.get('#page-document a') + cy.get('#page-document p a') .should('have.attr', 'href') .and('include', 'https://google.com'); }); @@ -81,7 +81,7 @@ describe('Text Block Tests', () => { // then the page view should contain a mailto link cy.contains('Colorless green ideas sleep furiously.'); - cy.get('#page-document a') + cy.get('#page-document p a') .should('have.attr', 'href') .and('include', 'mailto:hello@example.com'); }); diff --git a/docker-compose.yml b/docker-compose.yml index 82a49830b7..ba7948726a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.3' services: backend: - image: plone/plone-backend:6.0.1 + image: plone/plone-backend:6.0.5 # Plone 5.2 series can be used too # image: plone/plone-backend:5.2.10 ports: diff --git a/docs/source/configuration/settings-reference.md b/docs/source/configuration/settings-reference.md index 779d7f1417..c201810b22 100644 --- a/docs/source/configuration/settings-reference.md +++ b/docs/source/configuration/settings-reference.md @@ -83,6 +83,10 @@ maxResponseSize You can edit this limit in the `settings` object setting a new value in bytes (for example, to set 500 mb you need to write 5000000000). +maxFileUploadSize + The maximum allowed size of file uploads (in bytes). + Default: `null` (no limit enforced by Volto). + initialReducersBlacklist The initial state passed from server to browser needs to be minimal in order to optimize the resultant html generated. This state gets stored in `window.__data` and received in client. diff --git a/docs/source/configuration/volto-slate/configuration-settings.md b/docs/source/configuration/volto-slate/configuration-settings.md index a6da06c3b2..97276b804f 100644 --- a/docs/source/configuration/volto-slate/configuration-settings.md +++ b/docs/source/configuration/volto-slate/configuration-settings.md @@ -239,6 +239,19 @@ They are not persisted in the final value, so they are useful, for example, to h slate.runtimeDecorators = [([node, path], ranges) => ranges]; ``` +(editor-configuration-slate-useLinkedHeadings-label)= + +## `slate.useLinkedHeadings` + +The setting `slate.useLinkedHeadings` controls whether `volto-slate` creates anchors for headings, such as `h1` and `h2`, in the editor. + +The default setting is `true`. + +You can opt out of this feature by setting its value to `false`. + +```js +slate.useLinkedHeadings = false +``` (editor-configuration-blocks-initialBlocksFocus-label)= diff --git a/locales/ca/LC_MESSAGES/volto.po b/locales/ca/LC_MESSAGES/volto.po index e01b268d76..db73f21323 100644 --- a/locales/ca/LC_MESSAGES/volto.po +++ b/locales/ca/LC_MESSAGES/volto.po @@ -1903,6 +1903,11 @@ msgstr "" msgid "Link" msgstr "Enllaç" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4303,6 +4308,11 @@ msgstr "quan" msgid "event_where" msgstr "On?" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/de/LC_MESSAGES/volto.po b/locales/de/LC_MESSAGES/volto.po index 22c83cb8a6..4d37f47225 100644 --- a/locales/de/LC_MESSAGES/volto.po +++ b/locales/de/LC_MESSAGES/volto.po @@ -1900,6 +1900,11 @@ msgstr "" msgid "Link" msgstr "Link" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4300,10 +4305,15 @@ msgstr "Datum" msgid "event_where" msgstr "Ort" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "Dateien, die größer sind als {limit}, dürfen nicht hochgeladen werden." + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" -msgstr "TODO Translation. IntIds neu generieren und Relationen neu katalogisieren" +msgstr "IntIds neu generieren und Relationen neu katalogisieren" #: components/manage/Blocks/Teaser/schema # defaultMessage: Head title diff --git a/locales/en/LC_MESSAGES/volto.po b/locales/en/LC_MESSAGES/volto.po index c85490b44b..7783ecbea7 100644 --- a/locales/en/LC_MESSAGES/volto.po +++ b/locales/en/LC_MESSAGES/volto.po @@ -1894,6 +1894,11 @@ msgstr "" msgid "Link" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4294,6 +4299,11 @@ msgstr "" msgid "event_where" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/es/LC_MESSAGES/volto.po b/locales/es/LC_MESSAGES/volto.po index 696ad123e6..e532261061 100644 --- a/locales/es/LC_MESSAGES/volto.po +++ b/locales/es/LC_MESSAGES/volto.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: Plone\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-12-03 03:20-0400\n" -"PO-Revision-Date: 2022-12-02 11:45+0100\n" +"PO-Revision-Date: 2023-06-20 14:02-0400\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language: es\n" "Language-Team: ES \n" @@ -15,7 +15,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" -"X-Generator: Poedit 2.3\n" +"X-Generator: Poedit 3.3.1\n" "Language-Code: es\n" "Language-Name: Español\n" "Preferred-Encodings: utf-8\n" @@ -282,12 +282,12 @@ msgstr "Complemento actualizado con éxito" #: components/manage/Blocks/Search/schema # defaultMessage: Advanced facet? msgid "Advanced facet?" -msgstr "" +msgstr "¿Faceta avanzada?" #: components/manage/Blocks/Search/schema # defaultMessage: Advanced facets are initially hidden and displayed on demand msgid "Advanced facets are initially hidden and displayed on demand" -msgstr "" +msgstr "Las facetas avanzadas se ocultan inicialmente y se muestran cuando se solicitan" #: config/Views # defaultMessage: Album view @@ -479,7 +479,7 @@ msgstr "Barra de ruta" #: components/manage/Controlpanels/Relations/BrokenRelations # defaultMessage: Broken relations msgid "Broken relations" -msgstr "" +msgstr "Relaciones rotas" #: components/manage/Blocks/HeroImageLeft/Edit #: components/manage/Contents/ContentsUploadModal @@ -802,7 +802,7 @@ msgstr "Información del copyright o otros derechos en este elemento." #: helpers/MessageLabels/MessageLabels # defaultMessage: Create or delete relations to target msgid "Create or delete relations to target" -msgstr "" +msgstr "Crear o eliminar relaciones con el destino" #: components/manage/Toolbar/More # defaultMessage: Create working copy @@ -1426,7 +1426,7 @@ msgstr "Nombre del archivo" #: helpers/MessageLabels/MessageLabels # defaultMessage: Filter msgid "Filter" -msgstr "" +msgstr "Filtrar" #: components/manage/Controlpanels/Rules/Rules # defaultMessage: Filter Rules: @@ -1456,7 +1456,7 @@ msgstr "Primero" #: helpers/MessageLabels/MessageLabels # defaultMessage: Fix relations msgid "Fix relations" -msgstr "" +msgstr "Corregir relaciones" #: components/manage/Blocks/Table/Edit # defaultMessage: Fixed width columns @@ -1597,7 +1597,7 @@ msgstr "¿Ocultar la faceta?" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Hide filters msgid "Hide filters" -msgstr "" +msgstr "Ocultar filtros" #: components/manage/History/History #: components/manage/Toolbar/More @@ -1670,7 +1670,7 @@ msgstr "Galería de imágenes" #: components/manage/Blocks/Teaser/schema # defaultMessage: Image override msgid "Image override" -msgstr "" +msgstr "Reemplazar imagen" #: components/manage/Blocks/Image/schema # defaultMessage: Image size @@ -1733,7 +1733,7 @@ msgstr "Insertar fila antes" #: helpers/MessageLabels/MessageLabels # defaultMessage: Inspect relations msgid "Inspect relations" -msgstr "" +msgstr "Inspeccionar relaciones" #: components/manage/Controlpanels/AddonsControlpanel # defaultMessage: Install @@ -1779,7 +1779,7 @@ msgstr "Intervalo anual" #: components/theme/View/RenderBlocks # defaultMessage: Invalid block - Will be removed on saving msgid "Invalid Block" -msgstr "Bloque no válido" +msgstr "Bloque no válido: se eliminará al guardar" #: components/manage/Widgets/QuerystringWidget # defaultMessage: Item batch size @@ -1898,13 +1898,18 @@ msgstr "Izquierda" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Less filters msgid "Less filters" -msgstr "" +msgstr "Menos filtros" #: components/manage/Toolbar/Toolbar # defaultMessage: Link msgid "Link" msgstr "Enlace" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -2030,7 +2035,7 @@ msgstr "¿Añadidos de forma manual o automática?" #: helpers/MessageLabels/MessageLabels # defaultMessage: Many relations found. Please search. msgid "Many relations found. Please search." -msgstr "" +msgstr "Muchas relaciones encontradas. Por favor, busque." #: components/manage/Blocks/Maps/MapsSidebar #: components/manage/Blocks/Maps/schema @@ -2112,7 +2117,7 @@ msgstr "Más" #: components/manage/Blocks/Search/components/Facets # defaultMessage: More filters msgid "More filters" -msgstr "" +msgstr "Más filtros" #: components/manage/Controlpanels/UpgradeControlPanel # defaultMessage: More information about the upgrade procedure can be found in the documentation section of plone.org in the Upgrade Guide. @@ -2231,7 +2236,7 @@ msgstr "No se han encontrado complementos" #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: No broken relations found. msgid "No broken relations found." -msgstr "" +msgstr "No se encontraron relaciones rotas." #: components/theme/RequestTimeout/RequestTimeout # defaultMessage: There is no connection to the server, due to a timeout o no network connection. @@ -2289,7 +2294,7 @@ msgstr "No hay opciones" #: helpers/MessageLabels/MessageLabels # defaultMessage: No relation found msgid "No relation found" -msgstr "" +msgstr "No se encontró relación" #: components/manage/BlockChooser/BlockChooser #: components/theme/Search/Search @@ -2474,7 +2479,7 @@ msgstr "Las personas responsables de la creación del contenido de este elemento #: components/manage/Blocks/Teaser/DefaultBody # defaultMessage: Please choose an existing content as source for this element msgid "Please choose an existing content as source for this element" -msgstr "" +msgstr "Elija un contenido existente como origen para este elemento" #: components/manage/Controlpanels/Controlpanels # defaultMessage: Please continue with the upgrade. @@ -2514,7 +2519,7 @@ msgstr "Actualice plone.restapi a la versión 8.24.0 o superior." #: components/manage/Controlpanels/Relations/Relations # defaultMessage: Please upgrade to plone.restapi >= 8.35.3. msgid "Please upgrade to plone.restapi >= 8.35.3." -msgstr "" +msgstr "Por favor, actualice a plone.restapi >= 8.35.3." #: components/theme/Footer/Footer # defaultMessage: Plone Foundation @@ -2613,7 +2618,7 @@ msgstr "Leer Más..." #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Read only for this type of relation. msgid "Read only for this type of relation." -msgstr "" +msgstr "Solo lectura para este tipo de relación." #: components/manage/Contents/Contents # defaultMessage: Rearrange items by… @@ -2661,24 +2666,24 @@ msgstr "Formulario de registro" #: helpers/MessageLabels/MessageLabels # defaultMessage: Relation msgid "Relation name" -msgstr "" +msgstr "Relación" #: components/manage/Controlpanels/Controlpanels #: components/manage/Controlpanels/Relations/Relations #: helpers/MessageLabels/MessageLabels # defaultMessage: Relations msgid "Relations" -msgstr "" +msgstr "Relaciones" #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Relations are editable with plone.api >= 2.0.3. msgid "Relations are editable with plone.api >= 2.0.3." -msgstr "" +msgstr "Las relaciones son editables con plone.api >= 2.0.3." #: helpers/MessageLabels/MessageLabels # defaultMessage: Relations updated msgid "Relations updated" -msgstr "" +msgstr "Relaciones actualizadas" #: components/theme/Search/Search # defaultMessage: Relevance @@ -2782,7 +2787,7 @@ msgstr "Limpiar el título del término" #: components/manage/Blocks/Teaser/Data # defaultMessage: Reset the block msgid "Reset the block" -msgstr "" +msgstr "Restablecer el bloque" #: components/manage/Widgets/QuerystringWidget # defaultMessage: Results limit @@ -2891,7 +2896,7 @@ msgstr "Guardado" #: components/manage/Contents/ContentsItem # defaultMessage: Scheduled msgid "Scheduled" -msgstr "" +msgstr "Programado" #: components/manage/Controlpanels/ContentTypesActions # defaultMessage: Schema @@ -2969,12 +2974,12 @@ msgstr "Resultados de búsqueda para {term}" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search sources by title or path msgid "Search sources by title or path" -msgstr "" +msgstr "Buscar orígenes por título o ruta" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search targets by title or path msgid "Search targets by title or path" -msgstr "" +msgstr "Buscar destinos por título o ruta" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search users… @@ -3017,7 +3022,7 @@ msgstr "Seleccionar columnas a mostrar" #: helpers/MessageLabels/MessageLabels # defaultMessage: Select relation msgid "Select relation" -msgstr "" +msgstr "Seleccionar relación" #: components/manage/Contents/ContentsWorkflowModal # defaultMessage: Select the transition to be used for modifying the items state. @@ -3027,7 +3032,7 @@ msgstr "Seleccione la transición a ser efectuada al cambiar el estado del conte #: helpers/MessageLabels/MessageLabels # defaultMessage: Selected msgid "Selected" -msgstr "" +msgstr "Seleccionado" #: components/manage/Widgets/RecurrenceWidget/Occurences # defaultMessage: Selected dates @@ -3126,7 +3131,7 @@ msgstr "Mostrar respuestas" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Show filters msgid "Show filters" -msgstr "" +msgstr "Mostrar filtros" #: helpers/MessageLabels/MessageLabels # defaultMessage: Show groups of users below @@ -3141,12 +3146,12 @@ msgstr "Mostrar elemento" #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: Show potential sources. Not only objects that are source of some relation. msgid "Show potential sources. Not only objects that are source of some relation." -msgstr "" +msgstr "Mostrar orígenes potenciales. No solo objetos que son orígenes de alguna relación." #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: Show potential targets. Not only objects that are target of some relation. msgid "Show potential targets. Not only objects that are target of some relation." -msgstr "" +msgstr "Mostrar destinos potenciales. No solo objetos que son destino de alguna relación." #: components/manage/Blocks/Search/schema # defaultMessage: Show search button? @@ -3218,7 +3223,7 @@ msgstr "Pequeño" #: components/manage/Controlpanels/Relations/Relations # defaultMessage: Some relations are broken. Please fix. msgid "Some relations are broken. Please fix." -msgstr "" +msgstr "Algunas relaciones están rotas. Por favor, arréglelas." #: error # defaultMessage: Sorry, something went wrong with your request @@ -3431,7 +3436,7 @@ msgstr "El camino de destino debe comenzar con /." #: components/manage/Blocks/Teaser/schema # defaultMessage: Teaser msgid "Teaser" -msgstr "" +msgstr "Destacado" #: components/manage/Widgets/SchemaWidget # defaultMessage: Text @@ -3627,7 +3632,7 @@ msgstr "Comentarios totales" #: components/manage/Contents/Contents # defaultMessage: Total items to be deleted: msgid "Total items to be deleted:" -msgstr "" +msgstr "Total de elementos a eliminar:" #: components/manage/Controlpanels/DatabaseInformation # defaultMessage: Total number of objects in each cache @@ -4152,7 +4157,7 @@ msgstr "Ha sido salido del sitio." #: components/manage/Controlpanels/Relations/Relations # defaultMessage: You have not the required permission for this control panel. msgid "You have not the required permission for this control panel." -msgstr "" +msgstr "No tiene el permiso requerido para este panel de control." #: components/theme/PasswordReset/RequestPasswordReset # defaultMessage: Your email is required for reset your password. @@ -4305,15 +4310,20 @@ msgstr "Cuándo" msgid "event_where" msgstr "Dónde" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "Este sitio web no acepta archivos de más de {limit}" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" -msgstr "" +msgstr "vaciar intIds y reconstruir relaciones" #: components/manage/Blocks/Teaser/schema # defaultMessage: Head title msgid "head_title" -msgstr "" +msgstr "Encabezado" #: components/theme/PasswordReset/RequestPasswordReset # defaultMessage: Password reset confirmation sent @@ -4414,7 +4424,7 @@ msgstr "Más usado" #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Found {sources} sources and {targets} targets. Narrow down to {max}! msgid "narrowDownRelations" -msgstr "" +msgstr "Se encontraron {sources} orígenes y {targets} destinos. ¡Reduzca a {max}!" #: components/manage/Blocks/Search/components/DateRangeFacetFilterListEntry #: components/manage/Blocks/Search/components/ToggleFacetFilterListEntry @@ -4472,7 +4482,7 @@ msgstr "Seleccione..." #: helpers/MessageLabels/MessageLabels # defaultMessage: rebuild relations msgid "rebuild relations" -msgstr "" +msgstr "reconstruir relaciones" #: components/theme/Search/Search # defaultMessage: results @@ -4677,7 +4687,7 @@ msgstr "Ordenar" #: helpers/MessageLabels/MessageLabels # defaultMessage: sources path msgid "sources path" -msgstr "" +msgstr "ruta de origen" #: config/Blocks # defaultMessage: Table @@ -4687,7 +4697,7 @@ msgstr "Tabla" #: helpers/MessageLabels/MessageLabels # defaultMessage: target path msgid "target path" -msgstr "" +msgstr "ruta de destino" #: config/Blocks # defaultMessage: Text diff --git a/locales/eu/LC_MESSAGES/volto.po b/locales/eu/LC_MESSAGES/volto.po index b19c9ad4de..85aa21e868 100644 --- a/locales/eu/LC_MESSAGES/volto.po +++ b/locales/eu/LC_MESSAGES/volto.po @@ -1901,6 +1901,11 @@ msgstr "" msgid "Link" msgstr "Esteka" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4301,6 +4306,11 @@ msgstr "Noiz" msgid "event_where" msgstr "Non" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/fi/LC_MESSAGES/volto.po b/locales/fi/LC_MESSAGES/volto.po index 19345cfa05..e3d3b2cc3f 100644 --- a/locales/fi/LC_MESSAGES/volto.po +++ b/locales/fi/LC_MESSAGES/volto.po @@ -1905,6 +1905,11 @@ msgstr "" msgid "Link" msgstr "Linkki" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4305,6 +4310,11 @@ msgstr "Milloin" msgid "event_where" msgstr "Missä" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/fr/LC_MESSAGES/volto.po b/locales/fr/LC_MESSAGES/volto.po index cfcd5a0af4..8862a6e1ac 100644 --- a/locales/fr/LC_MESSAGES/volto.po +++ b/locales/fr/LC_MESSAGES/volto.po @@ -1911,6 +1911,11 @@ msgstr "" msgid "Link" msgstr "Lien" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4311,6 +4316,11 @@ msgstr "Quand" msgid "event_where" msgstr "Où" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/it/LC_MESSAGES/volto.po b/locales/it/LC_MESSAGES/volto.po index 64d87aadc6..c081ae1612 100644 --- a/locales/it/LC_MESSAGES/volto.po +++ b/locales/it/LC_MESSAGES/volto.po @@ -1894,6 +1894,11 @@ msgstr "" msgid "Link" msgstr "Link" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4294,6 +4299,11 @@ msgstr "Quando" msgid "event_where" msgstr "Dove" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/ja/LC_MESSAGES/volto.po b/locales/ja/LC_MESSAGES/volto.po index 9aef53b157..81b4228ce9 100644 --- a/locales/ja/LC_MESSAGES/volto.po +++ b/locales/ja/LC_MESSAGES/volto.po @@ -1902,6 +1902,11 @@ msgstr "" msgid "Link" msgstr "リンク" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4302,6 +4307,11 @@ msgstr "日時" msgid "event_where" msgstr "場所" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/nl/LC_MESSAGES/volto.po b/locales/nl/LC_MESSAGES/volto.po index c563bf05bb..24d58061a5 100644 --- a/locales/nl/LC_MESSAGES/volto.po +++ b/locales/nl/LC_MESSAGES/volto.po @@ -1913,6 +1913,11 @@ msgstr "" msgid "Link" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4313,6 +4318,11 @@ msgstr "" msgid "event_where" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/pt/LC_MESSAGES/volto.po b/locales/pt/LC_MESSAGES/volto.po index b9f0a4602b..b5ecc4dd7d 100644 --- a/locales/pt/LC_MESSAGES/volto.po +++ b/locales/pt/LC_MESSAGES/volto.po @@ -1902,6 +1902,11 @@ msgstr "" msgid "Link" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4302,6 +4307,11 @@ msgstr "" msgid "event_where" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/pt_BR/LC_MESSAGES/volto.po b/locales/pt_BR/LC_MESSAGES/volto.po index dc69005027..19cfd33d8d 100644 --- a/locales/pt_BR/LC_MESSAGES/volto.po +++ b/locales/pt_BR/LC_MESSAGES/volto.po @@ -281,12 +281,12 @@ msgstr "Complemento atualizado com sucesso" #: components/manage/Blocks/Search/schema # defaultMessage: Advanced facet? msgid "Advanced facet?" -msgstr "" +msgstr "Facetas avançadas?" #: components/manage/Blocks/Search/schema # defaultMessage: Advanced facets are initially hidden and displayed on demand msgid "Advanced facets are initially hidden and displayed on demand" -msgstr "" +msgstr "Facetas avançadas ficam ocultas inicialmente e exibidas sob demanda" #: config/Views # defaultMessage: Album view @@ -478,7 +478,7 @@ msgstr "Navegação estrutural" #: components/manage/Controlpanels/Relations/BrokenRelations # defaultMessage: Broken relations msgid "Broken relations" -msgstr "" +msgstr "Relacionamentos rompidos" #: components/manage/Blocks/HeroImageLeft/Edit #: components/manage/Contents/ContentsUploadModal @@ -801,7 +801,7 @@ msgstr "Declaração de copyright ou outras informações de direitos sobre este #: helpers/MessageLabels/MessageLabels # defaultMessage: Create or delete relations to target msgid "Create or delete relations to target" -msgstr "" +msgstr "Crie ou remova relacionamentos para o destino" #: components/manage/Toolbar/More # defaultMessage: Create working copy @@ -1425,7 +1425,7 @@ msgstr "Nome do arquivo" #: helpers/MessageLabels/MessageLabels # defaultMessage: Filter msgid "Filter" -msgstr "" +msgstr "Filtrar" #: components/manage/Controlpanels/Rules/Rules # defaultMessage: Filter Rules: @@ -1455,7 +1455,7 @@ msgstr "Primeiro" #: helpers/MessageLabels/MessageLabels # defaultMessage: Fix relations msgid "Fix relations" -msgstr "" +msgstr "Consertar relacionamentos" #: components/manage/Blocks/Table/Edit # defaultMessage: Fixed width columns @@ -1596,7 +1596,7 @@ msgstr "Ocultar faceta?" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Hide filters msgid "Hide filters" -msgstr "" +msgstr "Ocultar filtros" #: components/manage/History/History #: components/manage/Toolbar/More @@ -1669,7 +1669,7 @@ msgstr "Galeria de imagem" #: components/manage/Blocks/Teaser/schema # defaultMessage: Image override msgid "Image override" -msgstr "" +msgstr "Substituir imagem" #: components/manage/Blocks/Image/schema # defaultMessage: Image size @@ -1732,7 +1732,7 @@ msgstr "Inserir linha antes" #: helpers/MessageLabels/MessageLabels # defaultMessage: Inspect relations msgid "Inspect relations" -msgstr "" +msgstr "Inspecionar relacionamentos" #: components/manage/Controlpanels/AddonsControlpanel # defaultMessage: Install @@ -1897,13 +1897,18 @@ msgstr "Esquerda" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Less filters msgid "Less filters" -msgstr "" +msgstr "Menos filtros" #: components/manage/Toolbar/Toolbar # defaultMessage: Link msgid "Link" msgstr "Link" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -2029,7 +2034,7 @@ msgstr "Adicionado manual ou automaticamente?" #: helpers/MessageLabels/MessageLabels # defaultMessage: Many relations found. Please search. msgid "Many relations found. Please search." -msgstr "" +msgstr "Muitos relacionamentos foram encontrados. Por favor realize uma busca" #: components/manage/Blocks/Maps/MapsSidebar #: components/manage/Blocks/Maps/schema @@ -2060,7 +2065,7 @@ msgstr "Médio" #: helpers/MessageLabels/MessageLabels # defaultMessage: Membership updated msgid "Membership updated" -msgstr "" +msgstr "Participação atualizada" #: components/theme/ContactForm/ContactForm # defaultMessage: Message @@ -2111,7 +2116,7 @@ msgstr "Mais" #: components/manage/Blocks/Search/components/Facets # defaultMessage: More filters msgid "More filters" -msgstr "" +msgstr "Mais filtros" #: components/manage/Controlpanels/UpgradeControlPanel # defaultMessage: More information about the upgrade procedure can be found in the documentation section of plone.org in the Upgrade Guide. @@ -2230,7 +2235,7 @@ msgstr "Nenhum complemento encontrado" #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: No broken relations found. msgid "No broken relations found." -msgstr "" +msgstr "Nenhum relacionamento rompido foi encontrado" #: components/theme/RequestTimeout/RequestTimeout # defaultMessage: There is no connection to the server, due to a timeout o no network connection. @@ -2255,7 +2260,7 @@ msgstr "Nenhuma imagem definida no campo de imagem" #: components/manage/Blocks/Listing/GalleryNoResultsComponent # defaultMessage: No images found. msgid "No images found." -msgstr "" +msgstr "Nenhuma imagem foi encontrada" #: components/manage/Blocks/Listing/ListingBody # defaultMessage: No items found in this container. @@ -2288,7 +2293,7 @@ msgstr "Sem opções" #: helpers/MessageLabels/MessageLabels # defaultMessage: No relation found msgid "No relation found" -msgstr "" +msgstr "Nenhum relacionamento foi encontrado" #: components/manage/BlockChooser/BlockChooser #: components/theme/Search/Search @@ -2473,7 +2478,7 @@ msgstr "Pessoas responsáveis pela criação do conteúdo deste item. Por favor, #: components/manage/Blocks/Teaser/DefaultBody # defaultMessage: Please choose an existing content as source for this element msgid "Please choose an existing content as source for this element" -msgstr "" +msgstr "Por favor selecione um conteúdo existente como fonte para este elemento" #: components/manage/Controlpanels/Controlpanels # defaultMessage: Please continue with the upgrade. @@ -2513,7 +2518,7 @@ msgstr "Por favor, atualize para plone.restapi >= 8.24.0." #: components/manage/Controlpanels/Relations/Relations # defaultMessage: Please upgrade to plone.restapi >= 8.35.3. msgid "Please upgrade to plone.restapi >= 8.35.3." -msgstr "" +msgstr "Por favor atualize a plone.restapi para versão 8.35.3 ou superior." #: components/theme/Footer/Footer # defaultMessage: Plone Foundation @@ -2612,12 +2617,12 @@ msgstr "Leia Mais…" #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Read only for this type of relation. msgid "Read only for this type of relation." -msgstr "" +msgstr "Apenas leitura para este tipo de relacionamento." #: components/manage/Contents/Contents # defaultMessage: Rearrange items by… msgid "Rearrange items by…" -msgstr "Reorganize itens por…" +msgstr "Reorganizar itens por…" #: components/manage/Widgets/RecurrenceWidget/EndField # defaultMessage: Ends @@ -2660,24 +2665,24 @@ msgstr "Formulário de cadastro" #: helpers/MessageLabels/MessageLabels # defaultMessage: Relation msgid "Relation name" -msgstr "" +msgstr "Nome do relacionamento" #: components/manage/Controlpanels/Controlpanels #: components/manage/Controlpanels/Relations/Relations #: helpers/MessageLabels/MessageLabels # defaultMessage: Relations msgid "Relations" -msgstr "" +msgstr "Relacionamentos" #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Relations are editable with plone.api >= 2.0.3. msgid "Relations are editable with plone.api >= 2.0.3." -msgstr "" +msgstr "Relacionamentos são editáveis com uso da plone.api versão 2.0.3 ou superior." #: helpers/MessageLabels/MessageLabels # defaultMessage: Relations updated msgid "Relations updated" -msgstr "" +msgstr "Relacionamentos atualizados" #: components/theme/Search/Search # defaultMessage: Relevance @@ -2781,7 +2786,7 @@ msgstr "Redefinir o título do termo" #: components/manage/Blocks/Teaser/Data # defaultMessage: Reset the block msgid "Reset the block" -msgstr "" +msgstr "Redefinir o bloco" #: components/manage/Widgets/QuerystringWidget # defaultMessage: Results limit @@ -2890,7 +2895,7 @@ msgstr "Salvo" #: components/manage/Contents/ContentsItem # defaultMessage: Scheduled msgid "Scheduled" -msgstr "" +msgstr "Agendado" #: components/manage/Controlpanels/ContentTypesActions # defaultMessage: Schema @@ -2968,12 +2973,12 @@ msgstr "Resultados da pesquisa para {term}" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search sources by title or path msgid "Search sources by title or path" -msgstr "" +msgstr "Pesquise fontes por título ou caminho" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search targets by title or path msgid "Search targets by title or path" -msgstr "" +msgstr "Pesquise destinos por título ou caminho" #: helpers/MessageLabels/MessageLabels # defaultMessage: Search users… @@ -3016,7 +3021,7 @@ msgstr "Selecione colunas para mostrar" #: helpers/MessageLabels/MessageLabels # defaultMessage: Select relation msgid "Select relation" -msgstr "" +msgstr "Selecione um relacionamento" #: components/manage/Contents/ContentsWorkflowModal # defaultMessage: Select the transition to be used for modifying the items state. @@ -3026,7 +3031,7 @@ msgstr "Selecione a transição a ser usada para modificar o estado dos itens." #: helpers/MessageLabels/MessageLabels # defaultMessage: Selected msgid "Selected" -msgstr "" +msgstr "Selecionado" #: components/manage/Widgets/RecurrenceWidget/Occurences # defaultMessage: Selected dates @@ -3125,7 +3130,7 @@ msgstr "Mostrar respostas" #: components/manage/Blocks/Search/components/Facets # defaultMessage: Show filters msgid "Show filters" -msgstr "" +msgstr "Exibir filtros" #: helpers/MessageLabels/MessageLabels # defaultMessage: Show groups of users below @@ -3140,12 +3145,12 @@ msgstr "Mostrar item" #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: Show potential sources. Not only objects that are source of some relation. msgid "Show potential sources. Not only objects that are source of some relation." -msgstr "" +msgstr "Exibir potenciais fontes. Não apenas os objetos que já são fonte de algum relacionamento." #: components/manage/Controlpanels/Relations/RelationsMatrix # defaultMessage: Show potential targets. Not only objects that are target of some relation. msgid "Show potential targets. Not only objects that are target of some relation." -msgstr "" +msgstr "Exibir potenciais destinos. Não apenas os objetos que já são destino de algum relacionamento." #: components/manage/Blocks/Search/schema # defaultMessage: Show search button? @@ -3217,7 +3222,7 @@ msgstr "Pequeno" #: components/manage/Controlpanels/Relations/Relations # defaultMessage: Some relations are broken. Please fix. msgid "Some relations are broken. Please fix." -msgstr "" +msgstr "Alguns relacionamentos estão rompidos. Por favor corrija-os." #: error # defaultMessage: Sorry, something went wrong with your request @@ -3430,7 +3435,7 @@ msgstr "O caminho da URL de destino deve começar com uma barra." #: components/manage/Blocks/Teaser/schema # defaultMessage: Teaser msgid "Teaser" -msgstr "" +msgstr "Destaque" #: components/manage/Widgets/SchemaWidget # defaultMessage: Text @@ -3626,7 +3631,7 @@ msgstr "Total de comentários" #: components/manage/Contents/Contents # defaultMessage: Total items to be deleted: msgid "Total items to be deleted:" -msgstr "" +msgstr "Total de itens a serem removidos:" #: components/manage/Controlpanels/DatabaseInformation # defaultMessage: Total number of objects in each cache @@ -4151,7 +4156,7 @@ msgstr "Você foi desconectado do site." #: components/manage/Controlpanels/Relations/Relations # defaultMessage: You have not the required permission for this control panel. msgid "You have not the required permission for this control panel." -msgstr "" +msgstr "Você não possui a permissão necessária para acessar esse painel de controle." #: components/theme/PasswordReset/RequestPasswordReset # defaultMessage: Your email is required for reset your password. @@ -4304,15 +4309,20 @@ msgstr "Quando" msgid "event_where" msgstr "Onde" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" -msgstr "" +msgstr "descarrega os intIds e recontrua os relacionamentos" #: components/manage/Blocks/Teaser/schema # defaultMessage: Head title msgid "head_title" -msgstr "" +msgstr "Título principal" #: components/theme/PasswordReset/RequestPasswordReset # defaultMessage: Password reset confirmation sent @@ -4413,7 +4423,7 @@ msgstr "Mais usados" #: components/manage/Controlpanels/Relations/RelationsListing # defaultMessage: Found {sources} sources and {targets} targets. Narrow down to {max}! msgid "narrowDownRelations" -msgstr "" +msgstr "Encontradas {sources} fontes e {targets} destinos. Por favor, reduza ao máximo de {max}!" #: components/manage/Blocks/Search/components/DateRangeFacetFilterListEntry #: components/manage/Blocks/Search/components/ToggleFacetFilterListEntry @@ -4471,7 +4481,7 @@ msgstr "Selecionar" #: helpers/MessageLabels/MessageLabels # defaultMessage: rebuild relations msgid "rebuild relations" -msgstr "" +msgstr "reconstruir relacionamentos" #: components/theme/Search/Search # defaultMessage: results @@ -4676,7 +4686,7 @@ msgstr "ordenar" #: helpers/MessageLabels/MessageLabels # defaultMessage: sources path msgid "sources path" -msgstr "" +msgstr "caminho da fonte" #: config/Blocks # defaultMessage: Table @@ -4686,7 +4696,7 @@ msgstr "Tabela" #: helpers/MessageLabels/MessageLabels # defaultMessage: target path msgid "target path" -msgstr "" +msgstr "caminho do destino" #: config/Blocks # defaultMessage: Text diff --git a/locales/ro/LC_MESSAGES/volto.po b/locales/ro/LC_MESSAGES/volto.po index 30009767d5..e3e7c2ae0c 100644 --- a/locales/ro/LC_MESSAGES/volto.po +++ b/locales/ro/LC_MESSAGES/volto.po @@ -1894,6 +1894,11 @@ msgstr "" msgid "Link" msgstr "Legătură" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4294,6 +4299,11 @@ msgstr "Data" msgid "event_where" msgstr "Locație" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/volto.pot b/locales/volto.pot index 0524dad36f..4ef602b388 100644 --- a/locales/volto.pot +++ b/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2023-06-02T14:22:59.421Z\n" +"POT-Creation-Date: 2023-06-13T20:41:58.672Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "MIME-Version: 1.0\n" @@ -1896,6 +1896,11 @@ msgstr "" msgid "Link" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4296,6 +4301,11 @@ msgstr "" msgid "event_where" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/locales/zh_CN/LC_MESSAGES/volto.po b/locales/zh_CN/LC_MESSAGES/volto.po index e8f5457cdd..0355a55f65 100644 --- a/locales/zh_CN/LC_MESSAGES/volto.po +++ b/locales/zh_CN/LC_MESSAGES/volto.po @@ -1900,6 +1900,11 @@ msgstr "" msgid "Link" msgstr "链接" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: Link copied to clipboard +msgid "Link copied to clipboard" +msgstr "" + #: components/manage/Blocks/HeroImageLeft/schema #: components/manage/Blocks/Listing/schema # defaultMessage: Link more @@ -4300,6 +4305,11 @@ msgstr "" msgid "event_where" msgstr "" +#: helpers/MessageLabels/MessageLabels +# defaultMessage: This website does not accept files larger than {limit} +msgid "fileTooLarge" +msgstr "" + #: helpers/MessageLabels/MessageLabels # defaultMessage: flush intIds and rebuild relations msgid "flush intIds and rebuild relations" diff --git a/news/4783.feature b/news/4783.feature deleted file mode 100644 index 531d971133..0000000000 --- a/news/4783.feature +++ /dev/null @@ -1,2 +0,0 @@ -Search Block: Add support for advanced facets that are only displayed on demand. -[pbauer, razvanMiu, claudiaifrim] diff --git a/news/4801.feature b/news/4801.feature deleted file mode 100644 index 6d0b273792..0000000000 --- a/news/4801.feature +++ /dev/null @@ -1 +0,0 @@ -Display PAS validation errors. [tschorr] diff --git a/news/4841.bugfix b/news/4841.bugfix deleted file mode 100644 index 65cdbba77d..0000000000 --- a/news/4841.bugfix +++ /dev/null @@ -1 +0,0 @@ -Do not break validation on required number field with value 0 @cekk diff --git a/news/4849.feature b/news/4849.feature deleted file mode 100644 index 7eab003a6b..0000000000 --- a/news/4849.feature +++ /dev/null @@ -1 +0,0 @@ -Use a Container from the registry in the Form component and fallback to the Semantic UI one. @sneridagh diff --git a/package.json b/package.json index 4dc9c19184..d2126596b8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ } ], "license": "MIT", - "version": "17.0.0-alpha.9", + "version": "17.0.0-alpha.14", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" @@ -289,6 +289,7 @@ "eslint-plugin-react-hooks": "4.0.2", "express": "4.17.3", "filesize": "6", + "github-slugger": "1.4.0", "glob": "7.1.6", "history": "4.10.1", "hoist-non-react-statics": "3.3.2", diff --git a/packages/generator-volto/generators/addon/templates/Makefile b/packages/generator-volto/generators/addon/templates/Makefile index 59c4f32e52..be8d5fb29c 100644 --- a/packages/generator-volto/generators/addon/templates/Makefile +++ b/packages/generator-volto/generators/addon/templates/Makefile @@ -11,15 +11,15 @@ MAKEFLAGS+=--warn-undefined-variables MAKEFLAGS+=--no-builtin-rules # Project settings -# Update the versions depending on your project requirements | Last Updated 2022-12-23 -DOCKER_IMAGE=plone/server-dev:6.0.2 -DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:6.0.2 +# Update the versions depending on your project requirements | Last Updated 2023-06-17 +DOCKER_IMAGE=plone/server-dev:6.0.5 +DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:6.0.5 KGS= NODEBIN = ./node_modules/.bin # Plone 5 legacy DOCKER_IMAGE5=plone/plone-backend:5.2.10 -KGS5=plone.restapi==8.35.1 plone.volto==4.0.7 plone.rest==3.0.0 +KGS5=plone.restapi==8.37.0 plone.volto==4.0.8 plone.rest==3.0.0 TESTING_ADDONS=plone.app.robotframework==2.0.0 plone.app.testing==7.0.0 DIR=$(shell basename $$(pwd)) diff --git a/packages/volto-slate/package.json b/packages/volto-slate/package.json index 5cffb00466..7d599abd35 100644 --- a/packages/volto-slate/package.json +++ b/packages/volto-slate/package.json @@ -1,6 +1,6 @@ { "name": "@plone/volto-slate", - "version": "17.0.0-alpha.9", + "version": "17.0.0-alpha.14", "description": "Slate.js integration with Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx b/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx index bdd3f42926..4bdcf09756 100644 --- a/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +++ b/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx @@ -6,7 +6,11 @@ import { defineMessages, useIntl } from 'react-intl'; import { useInView } from 'react-intersection-observer'; import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react'; -import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers'; +import { + flattenToAppURL, + getBaseUrl, + validateFileUploadSize, +} from '@plone/volto/helpers'; import config from '@plone/volto/registry'; import { BlockDataForm, @@ -71,6 +75,7 @@ export const DefaultTextBlockEditor = (props) => { const { slate } = config.settings; const { textblockExtensions } = slate; const { value } = data; + const intl = useIntl(); // const [addNewBlockOpened, setAddNewBlockOpened] = React.useState(); const [showDropzone, setShowDropzone] = React.useState(false); @@ -106,6 +111,7 @@ export const DefaultTextBlockEditor = (props) => { files.forEach((file) => { const [mime] = file.type.split('/'); if (mime !== 'image') return; + if (!validateFileUploadSize(file, intl.formatMessage)) return; readAsDataURL(file).then((data) => { const fields = data.match(/^data:(.*);(.*),(.*)$/); @@ -127,7 +133,7 @@ export const DefaultTextBlockEditor = (props) => { }); setShowDropzone(false); }, - [pathname, uploadContent, block], + [pathname, uploadContent, block, intl.formatMessage], ); const { loaded, loading } = uploadRequest; @@ -178,7 +184,6 @@ export const DefaultTextBlockEditor = (props) => { instructions = formDescription; } - const intl = useIntl(); const placeholder = data.placeholder || formTitle || intl.formatMessage(messages.text); const schema = TextBlockSchema(data); diff --git a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx index 5e8accb82c..7f9f623fbd 100644 --- a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +++ b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx @@ -1,26 +1,30 @@ -import { serializeNodes } from '@plone/volto-slate/editor/render'; +import { + serializeNodes, + serializeNodesToText, +} from '@plone/volto-slate/editor/render'; import config from '@plone/volto/registry'; +import { isEqual } from 'lodash'; +import Slugger from 'github-slugger'; const TextBlockView = (props) => { const { id, data, styling = {} } = props; const { value, override_toc } = data; const metadata = props.metadata || props.properties; - return serializeNodes( - value, - (node, path) => { - const res = { ...styling }; - if (node.type) { - if ( - config.settings.slate.topLevelTargetElements.includes(node.type) || - override_toc - ) { - res.id = id; - } + const { topLevelTargetElements } = config.settings.slate; + + const getAttributes = (node, path) => { + const res = { ...styling }; + if (node.type && isEqual(path, [0])) { + if (topLevelTargetElements.includes(node.type) || override_toc) { + const text = serializeNodesToText(node?.children || []); + const slug = Slugger.slug(text); + res.id = slug || id; } - return res; - }, - { metadata: metadata }, - ); + } + return res; + }; + + return serializeNodes(value, getAttributes, { metadata: metadata }); }; export default TextBlockView; diff --git a/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js b/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js index 3dab777f0e..f4f8eaf08d 100644 --- a/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js +++ b/packages/volto-slate/src/blocks/Text/extensions/withDeserializers.js @@ -1,7 +1,7 @@ import isUrl from 'is-url'; import imageExtensions from 'image-extensions'; import { blockTagDeserializer } from '@plone/volto-slate/editor/deserialize'; -import { getBaseUrl } from '@plone/volto/helpers'; +import { getBaseUrl, validateFileUploadSize } from '@plone/volto/helpers'; import { v4 as uuid } from 'uuid'; import { Transforms } from 'slate'; @@ -66,7 +66,9 @@ export const withDeserializers = (editor) => { ...editor.dataTransferHandlers, files: (files) => { const unprocessed = []; + const { intl } = editor.getBlockProps(); for (const file of files) { + if (!validateFileUploadSize(file, intl.formatMessage)) return; const reader = new FileReader(); const [mime] = file.type.split('/'); if (mime === 'image') { diff --git a/packages/volto-slate/src/editor/config.jsx b/packages/volto-slate/src/editor/config.jsx index d97bb021d7..cdacc04daa 100644 --- a/packages/volto-slate/src/editor/config.jsx +++ b/packages/volto-slate/src/editor/config.jsx @@ -43,6 +43,7 @@ import { bTagDeserializer, codeTagDeserializer, } from './deserialize'; +import { renderLinkElement } from './render'; // Registry of available buttons export const buttons = { @@ -234,10 +235,10 @@ export const defaultBlockType = 'p'; export const elements = { default: ({ attributes, children }) =>

{children}

, - h1: ({ attributes, children }) =>

{children}

, - h2: ({ attributes, children }) =>

{children}

, - h3: ({ attributes, children }) =>

{children}

, - h4: ({ attributes, children }) =>

{children}

, + h1: renderLinkElement('h1'), + h2: renderLinkElement('h2'), + h3: renderLinkElement('h3'), + h4: renderLinkElement('h4'), li: ({ attributes, children }) =>
  • {children}
  • , ol: ({ attributes, children }) =>
      {children}
    , diff --git a/packages/volto-slate/src/editor/less/slate.less b/packages/volto-slate/src/editor/less/slate.less new file mode 100644 index 0000000000..954ee09ab2 --- /dev/null +++ b/packages/volto-slate/src/editor/less/slate.less @@ -0,0 +1,28 @@ +h1, +h2, +h3, +h4 { + &:hover { + a.anchor { + svg { + opacity: 1; + transform: rotate(15deg); + } + } + } + + a.anchor { + position: absolute; + display: inline-block; + margin-left: 5px; + vertical-align: middle; + + svg { + width: 1.6ch; + fill: #42526e; + opacity: 0; + transform: rotate(15deg) translate(-8px, 2px); + transition: opacity 0.2s ease 0s, transform 0.2s ease 0s; + } + } +} diff --git a/packages/volto-slate/src/editor/plugins/StyleMenu/StyleMenu.jsx b/packages/volto-slate/src/editor/plugins/StyleMenu/StyleMenu.jsx index af37a9f5f0..2771759873 100644 --- a/packages/volto-slate/src/editor/plugins/StyleMenu/StyleMenu.jsx +++ b/packages/volto-slate/src/editor/plugins/StyleMenu/StyleMenu.jsx @@ -28,13 +28,13 @@ const StyleMenuButton = ({ icon, active, ...props }) => ( ); -const MenuOpts = ({ editor, toSelect, option, ...rest }) => { +const MenuOpts = ({ editor, toSelect, option, type }) => { const isActive = toSelect.includes(option); return ( { @@ -118,7 +118,12 @@ const StylingsButton = (props) => { content={intl.formatMessage(messages.inlineStyle)} /> {inlineOpts.map((option, index) => ( - + ))} )} @@ -129,7 +134,12 @@ const StylingsButton = (props) => { content={intl.formatMessage(messages.paragraphStyle)} /> {blockOpts.map((option, index) => ( - + ))} )} diff --git a/packages/volto-slate/src/editor/render.jsx b/packages/volto-slate/src/editor/render.jsx index bfc34cb66e..45ab42ca75 100644 --- a/packages/volto-slate/src/editor/render.jsx +++ b/packages/volto-slate/src/editor/render.jsx @@ -1,9 +1,18 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; +import { useLocation } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { useIntl } from 'react-intl'; import { Node, Text } from 'slate'; import cx from 'classnames'; -import { isEmpty, isEqual, omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; +import { UniversalLink, Toast } from '@plone/volto/components'; +import { messages, addAppURL } from '@plone/volto/helpers'; +import useClipboard from '@plone/volto/hooks/clipboard/useClipboard'; import config from '@plone/volto/registry'; +import linkSVG from '@plone/volto/icons/link.svg'; + +import './less/slate.less'; const OMITTED = ['editor', 'path']; @@ -106,13 +115,7 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => { mode="view" key={path} data-slate-data={node.data ? serializeData(node) : null} - attributes={ - isEqual(path, [0]) - ? getAttributes - ? getAttributes(node, path) - : null - : null - } + attributes={getAttributes ? getAttributes(node, path) : null} extras={extras} > {_serializeNodes(Array.from(Node.children(editor, path)))} @@ -153,3 +156,60 @@ export const serializeNodesToText = (nodes) => { export const serializeNodesToHtml = (nodes) => renderToStaticMarkup(serializeNodes(nodes)); + +export const renderLinkElement = (tagName) => { + function LinkElement({ + attributes, + children, + mode = 'edit', + className = null, + }) { + const { slate = {} } = config.settings; + const Tag = tagName; + const slug = attributes.id || ''; + const location = useLocation(); + const appPathname = addAppURL(location.pathname); + // eslint-disable-next-line no-unused-vars + const [copied, copy, setCopied] = useClipboard( + appPathname.concat(`#${slug}`), + ); + const intl = useIntl(); + + return slate.useLinkedHeadings === false ? ( + + {children} + + ) : ( + + {children} + {mode === 'view' && slug && ( + + )} + + ); + } + LinkElement.displayName = `${tagName}LinkElement`; + return LinkElement; +}; diff --git a/src/components/manage/Blocks/HeroImageLeft/Edit.jsx b/src/components/manage/Blocks/HeroImageLeft/Edit.jsx index a93014fd28..422769a376 100644 --- a/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +++ b/src/components/manage/Blocks/HeroImageLeft/Edit.jsx @@ -14,7 +14,11 @@ import { defineMessages, injectIntl } from 'react-intl'; import cx from 'classnames'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; -import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers'; +import { + flattenToAppURL, + getBaseUrl, + validateFileUploadSize, +} from '@plone/volto/helpers'; import { createContent } from '@plone/volto/actions'; import { Icon, SidebarPortal, LinkMore } from '@plone/volto/components'; @@ -275,6 +279,7 @@ class EditComponent extends Component { */ onUploadImage({ target }) { const file = target.files[0]; + if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return; this.setState({ uploading: true, }); diff --git a/src/components/manage/Blocks/Image/Edit.jsx b/src/components/manage/Blocks/Image/Edit.jsx index 63087dabf2..2e7548f19b 100644 --- a/src/components/manage/Blocks/Image/Edit.jsx +++ b/src/components/manage/Blocks/Image/Edit.jsx @@ -21,6 +21,7 @@ import { flattenToAppURL, getBaseUrl, isInternalURL, + validateFileUploadSize, } from '@plone/volto/helpers'; import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg'; @@ -125,6 +126,7 @@ class Edit extends Component { onUploadImage = (e) => { e.stopPropagation(); const file = e.target.files[0]; + if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return; this.setState({ uploading: true, }); @@ -178,23 +180,25 @@ class Edit extends Component { * @param {array} files File objects * @returns {undefined} */ - onDrop = (file) => { - this.setState({ - uploading: true, - }); + onDrop = (files) => { + if (!validateFileUploadSize(files[0], this.props.intl.formatMessage)) { + this.setState({ dragging: false }); + return; + } + this.setState({ uploading: true }); - readAsDataURL(file[0]).then((data) => { + readAsDataURL(files[0]).then((data) => { const fields = data.match(/^data:(.*);(.*),(.*)$/); this.props.createContent( getBaseUrl(this.props.pathname), { '@type': 'Image', - title: file[0].name, + title: files[0].name, image: { data: fields[3], encoding: fields[2], 'content-type': fields[1], - filename: file[0].name, + filename: files[0].name, }, }, this.props.block, diff --git a/src/components/manage/Blocks/Listing/ListingBody.jsx b/src/components/manage/Blocks/Listing/ListingBody.jsx index 0d8e54003f..227b473c8f 100644 --- a/src/components/manage/Blocks/Listing/ListingBody.jsx +++ b/src/components/manage/Blocks/Listing/ListingBody.jsx @@ -1,14 +1,35 @@ -import React, { createRef } from 'react'; +import React, { createRef, useMemo } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import cx from 'classnames'; import { Pagination, Dimmer, Loader } from 'semantic-ui-react'; +import Slugger from 'github-slugger'; import { Icon } from '@plone/volto/components'; +import { renderLinkElement } from '@plone/volto-slate/editor/render'; import config from '@plone/volto/registry'; import withQuerystringResults from './withQuerystringResults'; import paginationLeftSVG from '@plone/volto/icons/left-key.svg'; import paginationRightSVG from '@plone/volto/icons/right-key.svg'; +const Headline = ({ headlineTag, id, data = {}, listingItems, isEditMode }) => { + let attr = { id }; + const slug = Slugger.slug(data.headline); + attr.id = slug || id; + const LinkedHeadline = useMemo(() => renderLinkElement(headlineTag), [ + headlineTag, + ]); + return ( + 0, + })} + /> + ); +}; + const ListingBody = withQuerystringResults((props) => { const { data = {}, @@ -22,6 +43,7 @@ const ListingBody = withQuerystringResults((props) => { nextBatch, isFolderContentsListing, hasLoaded, + id, } = props; let ListingBodyTemplate; @@ -50,13 +72,13 @@ const ListingBody = withQuerystringResults((props) => { return ( <> {data.headline && ( - 0, - })} - > - {data.headline} - + )} {listingItems?.length > 0 ? (
    diff --git a/src/components/manage/Blocks/Listing/ListingBody.test.jsx b/src/components/manage/Blocks/Listing/ListingBody.test.jsx index d52c7df814..8a5c359972 100644 --- a/src/components/manage/Blocks/Listing/ListingBody.test.jsx +++ b/src/components/manage/Blocks/Listing/ListingBody.test.jsx @@ -36,6 +36,26 @@ test('renders a ListingBody component', () => { content: { data: { is_folderish: true, + blocks: { + '839ee00b-013b-4f4a-9b10-8867938fdac3': { + '@type': 'listing', + block: '839ee00b-013b-4f4a-9b10-8867938fdac3', + headlineTag: 'h2', + query: [], + querystring: { + b_size: '2', + query: [ + { + i: 'path', + o: 'plone.app.querystring.operation.string.absolutePath', + v: '/', + }, + ], + sort_order: 'ascending', + }, + variation: 'default', + }, + }, }, }, intl: { diff --git a/src/components/manage/Blocks/Listing/__snapshots__/ListingBody.test.jsx.snap b/src/components/manage/Blocks/Listing/__snapshots__/ListingBody.test.jsx.snap index e81db3c83f..5fa1511b42 100644 --- a/src/components/manage/Blocks/Listing/__snapshots__/ListingBody.test.jsx.snap +++ b/src/components/manage/Blocks/Listing/__snapshots__/ListingBody.test.jsx.snap @@ -5,7 +5,7 @@ exports[`renders a ListingBody component 1`] = ` className="emptyListing" >
    state.querystringsearch.subrequests, ); @@ -50,7 +52,7 @@ export default function withQuerystringResults(WrappedComponent) { const folderItems = content?.is_folderish ? content.items : []; const hasQuery = querystring?.query?.length > 0; - const hasLoaded = hasQuery ? !querystringResults?.[id]?.loading : true; + const hasLoaded = hasQuery ? querystringResults?.[id]?.loaded : true; const listingItems = querystring?.query?.length > 0 && querystringResults?.[id] @@ -109,6 +111,8 @@ export default function withQuerystringResults(WrappedComponent) { } else { dispatch(getContent(initialPath, null, null, currentPage)); } + adaptedQueryRef.current = adaptedQuery; + currentPageRef.current = currentPage; }, [ id, isImageGallery, diff --git a/src/components/manage/Blocks/Search/hocs/withSearch.jsx b/src/components/manage/Blocks/Search/hocs/withSearch.jsx index 332d6feb4f..b810cc282f 100644 --- a/src/components/manage/Blocks/Search/hocs/withSearch.jsx +++ b/src/components/manage/Blocks/Search/hocs/withSearch.jsx @@ -148,12 +148,16 @@ const getSearchFields = (searchData) => { }; /** - * A HOC that will mirror the search block state to a hash location + * A hook that will mirror the search block state to a hash location */ const useHashState = () => { const location = useLocation(); const history = useHistory(); + /** + * Required to maintain parameter compatibility. + With this we will maintain support for receiving hash (#) and search (?) type parameters. + */ const oldState = React.useMemo(() => { return { ...qs.parse(location.search), @@ -169,7 +173,7 @@ const useHashState = () => { const setSearchData = React.useCallback( (searchData) => { - const newParams = qs.parse(location.hash); + const newParams = qs.parse(location.search); let changed = false; @@ -186,11 +190,11 @@ const useHashState = () => { if (changed) { history.push({ - hash: qs.stringify(newParams), + search: qs.stringify(newParams), }); } }, - [history, oldState, location.hash], + [history, oldState, location.search], ); return [current, setSearchData]; diff --git a/src/components/manage/Blocks/Title/View.jsx b/src/components/manage/Blocks/Title/View.jsx index b5836f8109..8486785fe7 100644 --- a/src/components/manage/Blocks/Title/View.jsx +++ b/src/components/manage/Blocks/Title/View.jsx @@ -3,19 +3,29 @@ * @module volto-slate/blocks/Title/TitleBlockView */ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import Slugger from 'github-slugger'; +import { renderLinkElement } from '@plone/volto-slate/editor/render'; /** * View title block component. * @class View * @extends Component */ -const TitleBlockView = ({ properties, metadata }) => { +const TitleBlockView = ({ properties, metadata, id, children }) => { + let attr = { id }; + const title = (properties || metadata)['title']; + const slug = Slugger.slug(title); + attr.id = slug || id; + const LinkedTitle = useMemo(() => renderLinkElement('h1'), []); return ( -

    - {(metadata || properties)['title'] || ''} -

    + ); }; diff --git a/src/components/manage/Blocks/Title/View.test.jsx b/src/components/manage/Blocks/Title/View.test.jsx index 80683d24c9..9c2c53d29e 100644 --- a/src/components/manage/Blocks/Title/View.test.jsx +++ b/src/components/manage/Blocks/Title/View.test.jsx @@ -1,10 +1,25 @@ import React from 'react'; import renderer from 'react-test-renderer'; +import configureStore from 'redux-mock-store'; +import { Provider } from 'react-intl-redux'; +import { MemoryRouter } from 'react-router-dom'; import View from './View'; +const mockStore = configureStore(); + test('renders a view title component', () => { + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); const component = renderer.create( - , + + + + + , ); const json = component.toJSON(); expect(json).toMatchSnapshot(); diff --git a/src/components/manage/Blocks/Title/__snapshots__/View.test.jsx.snap b/src/components/manage/Blocks/Title/__snapshots__/View.test.jsx.snap index 4e31d1dc1b..cc62ae1b9c 100644 --- a/src/components/manage/Blocks/Title/__snapshots__/View.test.jsx.snap +++ b/src/components/manage/Blocks/Title/__snapshots__/View.test.jsx.snap @@ -3,7 +3,29 @@ exports[`renders a view title component 1`] = `

    My Title +

    `; diff --git a/src/components/manage/Blocks/ToC/View.jsx b/src/components/manage/Blocks/ToC/View.jsx index 92f0a5f033..a0cd337f53 100644 --- a/src/components/manage/Blocks/ToC/View.jsx +++ b/src/components/manage/Blocks/ToC/View.jsx @@ -56,7 +56,14 @@ const View = (props) => { const items = []; if (!level || !levels.includes(level)) return; tocEntriesLayout.push(id); - tocEntries[id] = { level, title: title || block.plaintext, items, id }; + tocEntries[id] = { + level, + title: title || block.plaintext, + items, + id, + override_toc: block.override_toc, + plaintext: block.plaintext, + }; if (level < rootLevel) { rootLevel = level; } diff --git a/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx b/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx index 97fdbbcd10..148a8d88e3 100644 --- a/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +++ b/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx @@ -8,15 +8,27 @@ import PropTypes from 'prop-types'; import { map } from 'lodash'; import { List } from 'semantic-ui-react'; import { FormattedMessage, injectIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import AnchorLink from 'react-anchor-link-smooth-scroll'; +import Slugger from 'github-slugger'; -const RenderListItems = ({ items, data }) => { +const RenderListItems = ({ items, data, history }) => { return map(items, (item) => { - const { id, level, title } = item; + const { id, level, title, override_toc, plaintext } = item; + const slug = override_toc + ? Slugger.slug(plaintext) + : Slugger.slug(title) || id; return ( item && ( - {title} + { + history.push({ hash: slug }); + }} + > + {title} + {item.items?.length > 0 && ( { * @extends Component */ const View = ({ data, tocEntries }) => { + const history = useHistory(); return ( <> {data.title && !data.hide_title ? ( @@ -57,7 +70,7 @@ const View = ({ data, tocEntries }) => { bulleted={!data.ordered} as={data.ordered ? 'ol' : 'ul'} > - + ); diff --git a/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx b/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx index 8239a96e97..0bd1ccf8b0 100644 --- a/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +++ b/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx @@ -9,15 +9,19 @@ import { map } from 'lodash'; import { Menu } from 'semantic-ui-react'; import { FormattedMessage, injectIntl } from 'react-intl'; import AnchorLink from 'react-anchor-link-smooth-scroll'; +import Slugger from 'github-slugger'; const RenderMenuItems = ({ items }) => { return map(items, (item) => { - const { id, level, title } = item; + const { id, level, title, override_toc, plaintext } = item; + const slug = override_toc + ? Slugger.slug(plaintext) + : Slugger.slug(title) || id; return ( item && ( - {title} + {title} {item.items?.length > 0 && } diff --git a/src/components/manage/Contents/Contents.jsx b/src/components/manage/Contents/Contents.jsx index 3b3097d387..e4289c8e9c 100644 --- a/src/components/manage/Contents/Contents.jsx +++ b/src/components/manage/Contents/Contents.jsx @@ -12,7 +12,7 @@ import { Link } from 'react-router-dom'; import { Button, Confirm, - Container, + Container as SemanticContainer, Divider, Dropdown, Menu, @@ -70,6 +70,7 @@ import { import { Helmet, getBaseUrl } from '@plone/volto/helpers'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import config from '@plone/volto/registry'; import backSVG from '@plone/volto/icons/back.svg'; import cutSVG from '@plone/volto/icons/cut.svg'; @@ -1177,6 +1178,9 @@ class Contents extends Component { (this.props.orderRequest?.loading && !this.props.orderRequest?.error) || (this.props.searchRequest?.loading && !this.props.searchRequest?.error); + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + return this.props.token && this.props.objectActions?.length > 0 ? ( <> {folderContentsAction ? ( diff --git a/src/components/manage/Contents/ContentsUploadModal.jsx b/src/components/manage/Contents/ContentsUploadModal.jsx index 8733c05826..f393f3f118 100644 --- a/src/components/manage/Contents/ContentsUploadModal.jsx +++ b/src/components/manage/Contents/ContentsUploadModal.jsx @@ -25,6 +25,7 @@ import { readAsDataURL } from 'promise-file-reader'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedRelativeDate } from '@plone/volto/components'; import { createContent } from '@plone/volto/actions'; +import { validateFileUploadSize } from '@plone/volto/helpers'; const Dropzone = loadable(() => import('react-dropzone')); @@ -121,14 +122,18 @@ class ContentsUploadModal extends Component { * @returns {undefined} */ onDrop = async (files) => { + const validFiles = []; for (let i = 0; i < files.length; i++) { - await readAsDataURL(files[i]).then((data) => { - const fields = data.match(/^data:(.*);(.*),(.*)$/); - files[i].preview = fields[0]; - }); + if (validateFileUploadSize(files[i], this.props.intl.formatMessage)) { + await readAsDataURL(files[i]).then((data) => { + const fields = data.match(/^data:(.*);(.*),(.*)$/); + files[i].preview = fields[0]; + }); + validFiles.push(files[i]); + } } this.setState({ - files: concat(this.state.files, files), + files: concat(this.state.files, validFiles), }); }; diff --git a/src/components/manage/History/History.jsx b/src/components/manage/History/History.jsx index fb420bbadb..5fa61001a4 100644 --- a/src/components/manage/History/History.jsx +++ b/src/components/manage/History/History.jsx @@ -9,7 +9,13 @@ import { Helmet } from '@plone/volto/helpers'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { Container, Dropdown, Icon, Segment, Table } from 'semantic-ui-react'; +import { + Container as SemanticContainer, + Dropdown, + Icon, + Segment, + Table, +} from 'semantic-ui-react'; import { concat, map, reverse, find } from 'lodash'; import { Portal } from 'react-portal'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; @@ -24,6 +30,7 @@ import { } from '@plone/volto/components'; import { getHistory, revertHistory, listActions } from '@plone/volto/actions'; import { getBaseUrl } from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; import backSVG from '@plone/volto/icons/back.svg'; @@ -147,6 +154,9 @@ class History extends Component { }); const entries = this.processHistoryEntries(); + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + return !historyAction ? ( <> {this.props.token ? ( diff --git a/src/components/manage/Sharing/Sharing.jsx b/src/components/manage/Sharing/Sharing.jsx index 943f236ddb..047c706b93 100644 --- a/src/components/manage/Sharing/Sharing.jsx +++ b/src/components/manage/Sharing/Sharing.jsx @@ -14,7 +14,7 @@ import { Portal } from 'react-portal'; import { Button, Checkbox, - Container, + Container as SemanticContainer, Form, Icon as IconOld, Input, @@ -28,6 +28,7 @@ import { updateSharing, getSharing } from '@plone/volto/actions'; import { getBaseUrl } from '@plone/volto/helpers'; import { Icon, Toolbar, Toast } from '@plone/volto/components'; import { toast } from 'react-toastify'; +import config from '@plone/volto/registry'; import aheadSVG from '@plone/volto/icons/ahead.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; @@ -288,6 +289,9 @@ class SharingComponent extends Component { * @returns {string} Markup for the component. */ render() { + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + return ( diff --git a/src/components/manage/Toast/Toast.jsx b/src/components/manage/Toast/Toast.jsx index bd3421519e..96f4355656 100644 --- a/src/components/manage/Toast/Toast.jsx +++ b/src/components/manage/Toast/Toast.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Icon } from '@plone/volto/components'; +import Icon from '@plone/volto/components/theme/Icon/Icon'; import successSVG from '@plone/volto/icons/ready.svg'; import infoSVG from '@plone/volto/icons/info.svg'; diff --git a/src/components/manage/Widgets/ColorPickerWidget.jsx b/src/components/manage/Widgets/ColorPickerWidget.jsx index 929c0095e7..97c88c99be 100644 --- a/src/components/manage/Widgets/ColorPickerWidget.jsx +++ b/src/components/manage/Widgets/ColorPickerWidget.jsx @@ -51,7 +51,12 @@ const ColorPickerWidget = (props) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - onChange(id, color.name); + onChange( + id, + value === color.name + ? props.missing_value + : color.name, + ); }} active={value === color.name} circular diff --git a/src/components/manage/Widgets/FileWidget.jsx b/src/components/manage/Widgets/FileWidget.jsx index 046437c33e..34db736933 100644 --- a/src/components/manage/Widgets/FileWidget.jsx +++ b/src/components/manage/Widgets/FileWidget.jsx @@ -11,7 +11,7 @@ import { injectIntl } from 'react-intl'; import deleteSVG from '@plone/volto/icons/delete.svg'; import { Icon, FormFieldWrapper } from '@plone/volto/components'; import loadable from '@loadable/component'; -import { flattenToAppURL } from '@plone/volto/helpers'; +import { flattenToAppURL, validateFileUploadSize } from '@plone/volto/helpers'; import { defineMessages, useIntl } from 'react-intl'; const imageMimetypes = [ @@ -95,6 +95,7 @@ const FileWidget = (props) => { */ const onDrop = (files) => { const file = files[0]; + if (!validateFileUploadSize(file, intl.formatMessage)) return; readAsDataURL(file).then((data) => { const fields = data.match(/^data:(.*);(.*),(.*)$/); onChange(id, { diff --git a/src/components/theme/Anontools/Anontools.jsx b/src/components/theme/Anontools/Anontools.jsx index 2b9e9fb5b6..ccff3638b0 100644 --- a/src/components/theme/Anontools/Anontools.jsx +++ b/src/components/theme/Anontools/Anontools.jsx @@ -1,83 +1,56 @@ -/** - * Anontools component. - * @module components/theme/Anontools/Anontools - */ - -import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { Menu } from 'semantic-ui-react'; import { FormattedMessage } from 'react-intl'; +import { flattenToAppURL } from '@plone/volto/helpers'; +import { useToken } from '@plone/volto/hooks/userSession/useToken'; +import { useContent } from '@plone/volto/hooks/content/useContent'; import config from '@plone/volto/registry'; -/** - * Anontools container class. - */ -export class Anontools extends Component { - /** - * Property types. - * @property {Object} propTypes Property types. - * @static - */ - static propTypes = { - token: PropTypes.string, - content: PropTypes.shape({ - '@id': PropTypes.string, - }), - }; - - /** - * Default properties. - * @property {Object} defaultProps Default properties. - * @static - */ - static defaultProps = { - token: null, - content: { - '@id': null, - }, - }; +const Anontools = () => { + const token = useToken(); + const { data: content } = useContent(); - /** - * Render method. - * @method render - * @returns {string} Markup for the component. - */ - render() { - const { settings } = config; - return ( - !this.props.token && ( - + const { settings } = config; + return ( + !token && ( + + + + + + + {settings.showSelfRegistration && ( - - + + - {settings.showSelfRegistration && ( - - - - - - )} - - ) - ); - } -} + )} + + ) + ); +}; + +export default Anontools; + +Anontools.propTypes = { + token: PropTypes.string, + content: PropTypes.shape({ + '@id': PropTypes.string, + }), +}; -export default connect((state) => ({ - token: state.userSession.token, - content: state.content.data, -}))(Anontools); +Anontools.defaultProps = { + token: null, + content: { + '@id': null, + }, +}; diff --git a/src/components/theme/Anontools/Anontools.test.jsx b/src/components/theme/Anontools/Anontools.test.jsx index 628e214925..28f54f9544 100644 --- a/src/components/theme/Anontools/Anontools.test.jsx +++ b/src/components/theme/Anontools/Anontools.test.jsx @@ -12,7 +12,14 @@ describe('Anontools', () => { it('renders an anontools component when no token is specified', () => { const store = mockStore({ userSession: { token: null }, - content: { data: { '@id': 'myid' } }, + content: { + data: { '@id': 'myid' }, + get: { + loading: false, + loaded: true, + error: null, + }, + }, intl: { locale: 'en', messages: {}, @@ -32,7 +39,14 @@ describe('Anontools', () => { it('should not render an anontools component when a token is specified', () => { const store = mockStore({ userSession: { token: '1234' }, - content: { data: {} }, + content: { + data: {}, + get: { + loading: false, + loaded: true, + error: null, + }, + }, intl: { locale: 'en', messages: {}, diff --git a/src/components/theme/Header/Header.jsx b/src/components/theme/Header/Header.jsx index 491f92d2ca..2cda868be9 100644 --- a/src/components/theme/Header/Header.jsx +++ b/src/components/theme/Header/Header.jsx @@ -1,12 +1,6 @@ -/** - * Header component. - * @module components/theme/Header/Header - */ - -import React, { Component } from 'react'; import { Container, Segment } from 'semantic-ui-react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useToken } from '@plone/volto/hooks/userSession/useToken'; import { Anontools, @@ -16,65 +10,45 @@ import { SearchWidget, } from '@plone/volto/components'; -/** - * Header component class. - * @class Header - * @extends Component - */ -class Header extends Component { - /** - * Property types. - * @property {Object} propTypes Property types. - * @static - */ - static propTypes = { - token: PropTypes.string, - pathname: PropTypes.string.isRequired, - }; - - /** - * Default properties. - * @property {Object} defaultProps Default properties. - * @static - */ - static defaultProps = { - token: null, - }; +const Header = ({ pathname }) => { + const token = useToken(); - /** - * Render method. - * @method render - * @returns {string} Markup for the component. - */ - render() { - return ( - - -
    -
    -
    - -
    - + return ( + + +
    +
    +
    +
    -
    - - {!this.props.token && ( -
    - -
    - )} -
    - + +
    +
    + + {!token && ( +
    +
    + )} +
    +
    - - - ); - } -} +
    + + + ); +}; + +export default Header; + +Header.propTypes = { + token: PropTypes.string, + pathname: PropTypes.string.isRequired, + content: PropTypes.objectOf(PropTypes.any), +}; -export default connect((state) => ({ - token: state.userSession.token, -}))(Header); +Header.defaultProps = { + token: null, + content: null, +}; diff --git a/src/components/theme/Header/Header.md b/src/components/theme/Header/Header.md deleted file mode 100644 index b097be635d..0000000000 --- a/src/components/theme/Header/Header.md +++ /dev/null @@ -1,27 +0,0 @@ -Header example: - -```jsx static -
    -``` - -Output: - -```jsx noeditor -const { Provider } = require('react-intl-redux'); -const configureStore = require('redux-mock-store').default; -const store = configureStore()({ - userSession: { - login: {}, - }, - intl: { - locale: 'en', - messages: {}, - }, -}); - -
    - -
    - -
    ; -``` diff --git a/src/components/theme/Header/Header.test.jsx b/src/components/theme/Header/Header.test.jsx index 8002a8cbba..4144f673bc 100644 --- a/src/components/theme/Header/Header.test.jsx +++ b/src/components/theme/Header/Header.test.jsx @@ -39,4 +39,22 @@ describe('Header', () => { const json = component.toJSON(); expect(json).toMatchSnapshot(); }); + + it('renders a header component - auth', () => { + const store = mockStore({ + userSession: { token: '1234567890' }, + intl: { + locale: 'en', + messages: {}, + }, + }); + + const component = renderer.create( + +
    + , + ); + const json = component.toJSON(); + expect(json).toMatchSnapshot(); + }); }); diff --git a/src/components/theme/Header/__snapshots__/Header.test.jsx.snap b/src/components/theme/Header/__snapshots__/Header.test.jsx.snap index 5cd7084418..43c47d0a58 100644 --- a/src/components/theme/Header/__snapshots__/Header.test.jsx.snap +++ b/src/components/theme/Header/__snapshots__/Header.test.jsx.snap @@ -1,5 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Header renders a header component - auth 1`] = ` +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; + exports[`Header renders a header component 1`] = `
    - hasBlocksData(content) ? ( -
    +const NewsItemView = ({ content }) => { + const Container = + config.getComponent({ name: 'Container' }).component || SemanticContainer; + + return hasBlocksData(content) ? ( + -
    + ) : ( {content.title && ( @@ -57,6 +61,7 @@ const NewsItemView = ({ content }) => )} ); +}; /** * Property types. diff --git a/src/config/index.js b/src/config/index.js index 3496ae2083..5047b221eb 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -150,6 +150,7 @@ let config = { }, appExtras: [], maxResponseSize: 2000000000, // This is superagent default (200 mb) + maxFileUploadSize: null, serverConfig, storeExtenders: [], showTags: true, diff --git a/src/helpers/Extensions/withBlockSchemaEnhancer.js b/src/helpers/Extensions/withBlockSchemaEnhancer.js index dbd60aa252..ddd497aee7 100644 --- a/src/helpers/Extensions/withBlockSchemaEnhancer.js +++ b/src/helpers/Extensions/withBlockSchemaEnhancer.js @@ -1,6 +1,7 @@ import React from 'react'; import { defineMessages } from 'react-intl'; import { useIntl } from 'react-intl'; +import { find, isEmpty } from 'lodash'; import config from '@plone/volto/registry'; import { cloneDeepSchema } from '@plone/volto/helpers/Utils/Utils'; @@ -291,20 +292,23 @@ export const EMPTY_STYLES_SCHEMA = { }; /** - * Creates the `styles` field and fieldset in a schema + * Adds the `styles` field and 'styling' fieldset in a given schema */ export const addStyling = ({ schema, formData, intl }) => { - schema.fieldsets.push({ - id: 'styling', - title: intl.formatMessage(messages.styling), - fields: ['styles'], - }); + if (isEmpty(find(schema.fieldsets, { id: 'styling' }))) { + schema.fieldsets.push({ + id: 'styling', + title: intl.formatMessage(messages.styling), + fields: ['styles'], + }); + + schema.properties.styles = { + widget: 'object', + title: intl.formatMessage(messages.styling), + schema: cloneDeepSchema(EMPTY_STYLES_SCHEMA), + }; + } - schema.properties.styles = { - widget: 'object', - title: intl.formatMessage(messages.styling), - schema: EMPTY_STYLES_SCHEMA, - }; return schema; }; diff --git a/src/helpers/Extensions/withBlockSchemaEnhancer.test.js b/src/helpers/Extensions/withBlockSchemaEnhancer.test.js index e02a533d16..d5e29f4105 100644 --- a/src/helpers/Extensions/withBlockSchemaEnhancer.test.js +++ b/src/helpers/Extensions/withBlockSchemaEnhancer.test.js @@ -2,6 +2,7 @@ import { addExtensionFieldToSchema, applySchemaEnhancer, composeSchema, + addStyling, } from './withBlockSchemaEnhancer'; import config from '@plone/volto/registry'; @@ -246,3 +247,147 @@ describe('composeSchema', () => { expect(res).toStrictEqual([6, 9]); }); }); + +describe('addStyling', () => { + it('returns an enhanced schema with the styling wrapper object on it', () => { + const intl = { formatMessage: () => 'Styling' }; + + const schema = { + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: [], + }, + ], + properties: {}, + required: [], + }; + + const result = addStyling({ schema, intl }); + + expect(result).toStrictEqual({ + fieldsets: [ + { id: 'default', title: 'Default', fields: [] }, + { id: 'styling', title: 'Styling', fields: ['styles'] }, + ], + properties: { + styles: { + widget: 'object', + title: 'Styling', + schema: { + fieldsets: [ + { + fields: [], + id: 'default', + title: 'Default', + }, + ], + properties: {}, + required: [], + }, + }, + }, + required: [], + }); + }); + + it('multiple schema enhancers', () => { + const intl = { formatMessage: () => 'Styling' }; + + const schema1 = { + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: [], + }, + ], + properties: {}, + required: [], + }; + + const schema2 = { + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: [], + }, + ], + properties: {}, + required: [], + }; + + const result = addStyling({ schema: schema1, intl }); + + // We add some fields to the styling schema + result.properties.styles.schema.properties.align = { + widget: 'align', + title: 'align', + actions: ['left', 'right', 'center'], + default: 'left', + }; + + result.properties.styles.schema.fieldsets[0].fields = ['align']; + + const result2 = addStyling({ schema: schema2, intl }); + + expect(result).toStrictEqual({ + fieldsets: [ + { id: 'default', title: 'Default', fields: [] }, + { id: 'styling', title: 'Styling', fields: ['styles'] }, + ], + properties: { + styles: { + widget: 'object', + title: 'Styling', + schema: { + fieldsets: [ + { + fields: ['align'], + id: 'default', + title: 'Default', + }, + ], + properties: { + align: { + widget: 'align', + title: 'align', + actions: ['left', 'right', 'center'], + default: 'left', + }, + }, + required: [], + }, + }, + }, + required: [], + }); + + expect(result2).toStrictEqual({ + fieldsets: [ + { id: 'default', title: 'Default', fields: [] }, + { id: 'styling', title: 'Styling', fields: ['styles'] }, + ], + properties: { + styles: { + widget: 'object', + title: 'Styling', + schema: { + fieldsets: [ + { + fields: [], + id: 'default', + title: 'Default', + }, + ], + properties: {}, + required: [], + }, + }, + }, + required: [], + }); + }); +}); diff --git a/src/helpers/FormValidation/FormValidation.js b/src/helpers/FormValidation/FormValidation.js index 1f34ca05d5..52941afa75 100644 --- a/src/helpers/FormValidation/FormValidation.js +++ b/src/helpers/FormValidation/FormValidation.js @@ -1,5 +1,8 @@ import { map, uniq, keys, intersection, isEmpty } from 'lodash'; import { messages } from '../MessageLabels/MessageLabels'; +import config from '@plone/volto/registry'; +import { toast } from 'react-toastify'; +import Toast from '@plone/volto/components/manage/Toast/Toast'; /** * Will return the intl message if invalid @@ -369,3 +372,29 @@ class FormValidation { } export default FormValidation; + +/** + * Check if a file upload is within the maximum size limit. + * @param {File} file + * @param {Function} intlFunc + * @returns {Boolean} + */ +export const validateFileUploadSize = (file, intlFunc) => { + const isValid = + !config.settings.maxFileUploadSize || + file.size <= config.settings.maxFileUploadSize; + if (!isValid) { + toast.error( + , + ); + } + return isValid; +}; diff --git a/src/helpers/MessageLabels/MessageLabels.js b/src/helpers/MessageLabels/MessageLabels.js index 69c8d3cfe9..2e1c4f6b90 100644 --- a/src/helpers/MessageLabels/MessageLabels.js +++ b/src/helpers/MessageLabels/MessageLabels.js @@ -260,6 +260,10 @@ export const messages = defineMessages({ id: 'Show groups of users below', defaultMessage: 'Show groups of users below', }, + urlClipboardCopy: { + id: 'Link copied to clipboard', + defaultMessage: 'Link copied to clipboard', + }, inspectRelations: { id: 'Inspect relations', defaultMessage: 'Inspect relations', @@ -332,4 +336,8 @@ export const messages = defineMessages({ id: 'Filter', defaultMessage: 'Filter', }, + fileTooLarge: { + id: 'fileTooLarge', + defaultMessage: 'This website does not accept files larger than {limit}', + }, }); diff --git a/src/helpers/ScrollToTop/ScrollToTop.jsx b/src/helpers/ScrollToTop/ScrollToTop.jsx index 6d4a87bcc7..8122fbea63 100644 --- a/src/helpers/ScrollToTop/ScrollToTop.jsx +++ b/src/helpers/ScrollToTop/ScrollToTop.jsx @@ -28,15 +28,17 @@ class ScrollToTop extends React.Component { * @memberof ScrollToTop */ componentDidUpdate(prevProps) { + const { location } = this.props; const noInitialBlocksFocus = // Do not scroll on /edit config.blocks?.initialBlocksFocus === null ? this.props.location?.pathname.slice(-5) !== '/edit' : true; + + const isHash = location?.hash || location?.pathname.hash; if ( - !this.props.location?.hash && - !this.props.location?.pathname.hash && + !isHash && noInitialBlocksFocus && - this.props.location?.pathname !== prevProps.location?.pathname + location?.pathname !== prevProps.location?.pathname ) { window.scrollTo(0, 0); } diff --git a/src/helpers/Utils/usePagination.js b/src/helpers/Utils/usePagination.js index acb12d2ac5..083ec48682 100644 --- a/src/helpers/Utils/usePagination.js +++ b/src/helpers/Utils/usePagination.js @@ -1,25 +1,83 @@ -import React from 'react'; -import { isEqual } from 'lodash'; -import { usePrevious } from './usePrevious'; -import useDeepCompareEffect from 'use-deep-compare-effect'; +import React, { useRef, useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import qs from 'query-string'; +import { useSelector } from 'react-redux'; +import { slugify } from '@plone/volto/helpers/Utils/Utils'; + +/** + * @function useCreatePageQueryStringKey + * @description A hook that creates a key with an id if there are multiple blocks with pagination. + * @returns {string} Example: page || page_012345678 + */ +const useCreatePageQueryStringKey = (id) => { + const blockTypesWithPagination = ['search', 'listing']; + const blocks = useSelector((state) => state?.content?.data?.blocks) || []; + const blocksLayout = + useSelector((state) => state?.content?.data?.blocks_layout?.items) || []; + const displayedBlocks = blocksLayout?.map((item) => blocks[item]); + const hasMultiplePaginations = + displayedBlocks.filter((item) => + blockTypesWithPagination.includes(item['@type']), + ).length > 1 || false; + + return hasMultiplePaginations ? slugify(`page-${id}`) : 'page'; +}; + +const useGetBlockType = (id) => { + const blocks = useSelector((state) => state?.content?.data?.blocks) || []; + const block = blocks[id]; + return block ? block?.['@type'] : null; +}; /** * A pagination helper that tracks the query and resets pagination in case the * query changes. */ -export const usePagination = (query, defaultPage = 1) => { - const previousQuery = usePrevious(query); - const [currentPage, setCurrentPage] = React.useState(defaultPage); +export const usePagination = (id = null, defaultPage = 1) => { + const location = useLocation(); + const history = useHistory(); + const pageQueryStringKey = useCreatePageQueryStringKey(id); + const block_type = useGetBlockType(id); + const pageQueryParam = + qs.parse(location.search)[pageQueryStringKey] || defaultPage; + const [currentPage, setCurrentPageState] = React.useState( + parseInt(pageQueryParam), + ); + const setCurrentPage = (page) => { + setCurrentPageState(page); + const newParams = { + ...qs.parse(location.search), + [pageQueryStringKey]: page, + }; + history.push({ search: qs.stringify(newParams) }); + }; - useDeepCompareEffect(() => { - setCurrentPage(defaultPage); - }, [query, previousQuery, defaultPage]); + const queryRef = useRef(qs.parse(location.search)?.query); + useEffect(() => { + if ( + queryRef.current !== qs.parse(location.search)?.query && + block_type === 'search' + ) { + setCurrentPageState(defaultPage); + const newParams = { + ...qs.parse(location.search), + [pageQueryStringKey]: defaultPage, + }; + delete newParams[pageQueryStringKey]; + history.replace({ search: qs.stringify(newParams) }); + queryRef.current = qs.parse(location.search)?.query; + } else { + setCurrentPageState( + parseInt( + qs.parse(location.search)?.[pageQueryStringKey] || defaultPage, + ), + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.search, block_type]); return { - currentPage: - previousQuery && !isEqual(previousQuery, query) - ? defaultPage - : currentPage, + currentPage, setCurrentPage, }; }; diff --git a/src/helpers/Utils/usePagination.test.js b/src/helpers/Utils/usePagination.test.js new file mode 100644 index 0000000000..26924d78a0 --- /dev/null +++ b/src/helpers/Utils/usePagination.test.js @@ -0,0 +1,115 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { usePagination } from './usePagination'; +import * as redux from 'react-redux'; +import routeData from 'react-router'; +import { slugify } from '@plone/volto/helpers/Utils/Utils'; + +const searchBlockId = '545b33de-92cf-4747-969d-68851837b317'; +const searchBlockId2 = '454b33de-92cf-4747-969d-68851837b713'; +const searchBlock = { + '@type': 'search', + query: { + b_size: '4', + query: [ + { + i: 'path', + o: 'plone.app.querystring.operation.string.relativePath', + v: '', + }, + ], + sort_order: 'ascending', + }, + showSearchInput: true, + showTotalResults: true, +}; +let state = { + content: { + data: { + blocks: { + [searchBlockId]: searchBlock, + }, + blocks_layout: { + items: [searchBlockId], + }, + }, + }, +}; + +let mockUseLocationValue = { + pathname: '/testroute', + search: '', +}; + +const setUp = (searchParam, numberOfSearches) => { + mockUseLocationValue.search = searchParam; + if (numberOfSearches > 1) { + state.content.data.blocks[searchBlockId2] = searchBlock; + state.content.data.blocks_layout.items.push(searchBlockId2); + } + return renderHook(({ id, defaultPage }) => usePagination(id, defaultPage), { + initialProps: { + id: searchBlockId, + defaultPage: 1, + }, + }); +}; + +describe(`Tests for usePagination, for the block ${searchBlockId}`, () => { + const useLocation = jest.spyOn(routeData, 'useLocation'); + const useHistory = jest.spyOn(routeData, 'useHistory'); + const useSelector = jest.spyOn(redux, 'useSelector'); + beforeEach(() => { + useLocation.mockReturnValue(mockUseLocationValue); + useHistory.mockReturnValue({ replace: jest.fn() }); + useSelector.mockImplementation((cb) => cb(state)); + }); + + it('1 paginated block with id and defaultPage 1 - shoud be 1', () => { + const { result } = setUp(); + expect(result.current.currentPage).toBe(1); + }); + + it('1 paginated block without params - shoud be 1', () => { + const { result } = setUp(); + expect(result.current.currentPage).toBe(1); + }); + + const param1 = '?page=2'; + it(`1 paginated block with params: ${param1} - shoud be 2`, () => { + const { result } = setUp(param1); + expect(result.current.currentPage).toBe(2); + }); + + const param2 = `?${slugify(`page-${searchBlockId}`)}=2`; + it(`2 paginated blocks with current block in the params: ${param2} - shoud be 2`, () => { + const { result } = setUp(param2, 2); + expect(result.current.currentPage).toBe(2); + }); + + const param3 = `?${slugify(`page-${searchBlockId2}`)}=2`; + it(`2 paginated blocks with the other block in the params: ${param3} - shoud be 1`, () => { + const { result } = setUp(param3, 2); + expect(result.current.currentPage).toBe(1); + }); + + const param4 = `?${slugify(`page-${searchBlockId}`)}=2&${slugify( + `page-${searchBlockId2}`, + )}=1`; + it(`2 paginated blocks with both blocks in the params, current 2: ${param4} - shoud be 2`, () => { + const { result } = setUp(param4, 2); + expect(result.current.currentPage).toBe(2); + }); + + const param5 = `?${slugify(`page-${searchBlockId}`)}=1&${slugify( + `page-${searchBlockId2}`, + )}=2`; + it(`2 paginated blocks with both blocks in the params, current 1: ${param5} - shoud be 1`, () => { + const { result } = setUp(param5, 2); + expect(result.current.currentPage).toBe(1); + }); + + it(`2 paginated blocks with wrong page param: ${param1} - shoud be 1`, () => { + const { result } = setUp(param1, 2); + expect(result.current.currentPage).toBe(1); + }); +}); diff --git a/src/helpers/index.js b/src/helpers/index.js index faac5f7b43..1a05433740 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -71,7 +71,9 @@ export { export langmap from './LanguageMap/LanguageMap'; export Helmet from './Helmet/Helmet'; -export FormValidation from './FormValidation/FormValidation'; +export FormValidation, { + validateFileUploadSize, +} from './FormValidation/FormValidation'; export { difference, getColor, diff --git a/src/hooks/clipboard/useClipboard.js b/src/hooks/clipboard/useClipboard.js new file mode 100644 index 0000000000..eaabbf7a15 --- /dev/null +++ b/src/hooks/clipboard/useClipboard.js @@ -0,0 +1,26 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; + +export default function useClipboard(clipboardText = '') { + const stringToCopy = useRef(clipboardText); + const [copied, setCopied] = useState(false); + + //synchronous: window.clipboardData.setData(options.format || "text", text); + const copyToClipboard = async (text) => { + if ('clipboard' in navigator) { + return await navigator.clipboard.writeText(text); + } else { + return document.execCommand('copy', true, text); + } + }; + + const copyAction = useCallback(() => { + const copiedString = copyToClipboard(stringToCopy.current); + setCopied(copiedString); + }, [stringToCopy]); + + useEffect(() => { + stringToCopy.current = clipboardText; + }, [clipboardText]); + + return [copied, copyAction, setCopied]; +} diff --git a/src/hooks/content/useContent.js b/src/hooks/content/useContent.js new file mode 100644 index 0000000000..3001c1d7a4 --- /dev/null +++ b/src/hooks/content/useContent.js @@ -0,0 +1,31 @@ +import { useSelector, shallowEqual } from 'react-redux'; + +/** + * useContent hook + * + * This hook returns the current content that is stored in the Redux store in the + * `content` reducer, and returns it along with the related state (loading/loaded/error). + * + * @export + * @return {{ data: ContentData, loading: boolean, loaded: boolean, error: Error }} + */ +export function useContent() { + const data = useSelector((state) => state.content.data, shallowEqual); + const loading = useSelector((state) => state.content.get.loading); + const loaded = useSelector((state) => state.content.get.loaded); + const error = useSelector((state) => state.content.get.error, shallowEqual); + + return { data, loading, loaded, error }; +} + +// For reference purposes: Potential future useQuery version +// export function useContent() { +// // the cache will need to know the current location +// const pathname = useLocation(); +// const query = useQuery(getContentQuery({ path })) + +// // This might not be needed if we rename the properties +// const {isLoading: loading, isSuccess: loaded, ...rest} = query; + +// return { loading, loaded, ...rest }; +// } diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000000..ab9a86a472 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,2 @@ +export useClipboard from '@plone/volto/hooks/clipboard/useClipboard'; +export useToken from '@plone/volto/hooks/userSession/useToken'; diff --git a/src/hooks/userSession/useToken.js b/src/hooks/userSession/useToken.js new file mode 100644 index 0000000000..2d7d350f8d --- /dev/null +++ b/src/hooks/userSession/useToken.js @@ -0,0 +1,5 @@ +import { useSelector, shallowEqual } from 'react-redux'; + +export function useToken() { + return useSelector((state) => state.userSession.token, shallowEqual); +} diff --git a/theme/themes/pastanaga/extras/contents.less b/theme/themes/pastanaga/extras/contents.less index 149384714a..c4cea98911 100644 --- a/theme/themes/pastanaga/extras/contents.less +++ b/theme/themes/pastanaga/extras/contents.less @@ -94,6 +94,7 @@ .contents-table-wrapper { width: 100%; + overflow-x: auto; .ui.attached.table { width: 100%; diff --git a/yarn.lock b/yarn.lock index c4a6999d35..3188760ebf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2690,6 +2690,7 @@ __metadata: express: 4.17.3 filesize: 6 full-icu: 1.4.0 + github-slugger: 1.4.0 glob: 7.1.6 history: 4.10.1 hoist-non-react-statics: 3.3.2 @@ -12198,6 +12199,13 @@ __metadata: languageName: node linkType: hard +"github-slugger@npm:1.4.0": + version: 1.4.0 + resolution: "github-slugger@npm:1.4.0" + checksum: 4f52e7a21f5c6a4c5328f01fe4fe13ae8881fea78bfe31f9e72c4038f97e3e70d52fb85aa7633a52c501dc2486874474d9abd22aa61cbe9b113099a495551c6b + languageName: node + linkType: hard + "github-slugger@npm:^1.0.0": version: 1.5.0 resolution: "github-slugger@npm:1.5.0"