Hello world
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [ + { + text: 'Hello world', + }, + ], + }, + ]); + }); + + it('two root tags', () => { + const html = 'Hello world
something
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [ + { + text: 'Hello world', + }, + ], + }, + { + type: 'p', + children: [ + { + text: 'something', + }, + ], + }, + ]); + }); + + it('takes inline elements', () => { + const html = 'hello'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'strong', + children: [ + { + text: 'hello', + }, + ], + }, + ]); + }); + + it('strips unknown elements', () => { + const html = 'hello
world'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [{ text: 'hello' }], + }, + { text: ' ' }, + { + type: 'i', + children: [{ text: 'world' }], + }, + ]); + }); + + it('preserves spaces between one inline node and one block node', () => { + const html = 'helloworld
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'i', + children: [{ text: 'hello' }], + }, + { text: ' ' }, + { + type: 'p', + children: [{ text: 'world' }], + }, + ]); + }); + + it('replaces a single newline inside text with a space', () => { + const html = 'hello\nworld'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'i', + children: [{ text: 'hello world' }], + }, + ]); + }); + + it('replaces multiple newlines inside text with a space', () => { + const html = 'hello\n\n\nworld'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'i', + children: [{ text: 'hello world' }], + }, + ]); + }); + + it('removes whitespace between block elements', () => { + const html = 'hello
\n \nworld
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [{ text: 'hello' }], + }, + { + type: 'p', + children: [{ text: 'world' }], + }, + ]); + }); + + it('transforms newlines at beginning of tags to space', () => { + const html = 'hello\nworld'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'b', + children: [{ text: 'hello' }], + }, + { + type: 'i', + children: [{ text: ' world' }], + }, + ]); + }); + + it('transforms newlines after a space', () => { + const html = 'Lorem Ipsum\nis simply dummy text\n
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [ + { + type: 'strong', + children: [{ text: 'Lorem Ipsum' }], + }, + { text: ' is simply dummy text' }, + ], + }, + ]); + }); + + it('transforms newlines to space after an inline tag', () => { + const html = 'Lorem Ipsum\nis simply dummy text
'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'p', + children: [ + { + type: 'strong', + children: [{ text: 'Lorem Ipsum' }], + }, + { text: ' is simply dummy text' }, + ], + }, + ]); + }); + + it('it removes new lines at beginning of text of block nodes', () => { + const html = 'hello\n world\n
dot'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'b', + children: [{ text: 'hello' }], + }, + { + type: 'p', + children: [{ text: 'world' }], + }, + { + type: 'i', + children: [{ text: 'dot' }], + }, + ]); + }); + + it('it removes consecutive space in inline nodes', () => { + const html = `hello world`; + // console.log(JSON.stringify(tojson(html), null, 2)); + + expect(tojson(html)).toStrictEqual([ + { + type: 'b', + children: [{ text: 'hello ' }], + }, + { + type: 'i', + children: [{ text: 'world' }], + }, + ]); + }); + + it('it handles text fragments', () => { + const html = + 'Lorem Ipsum\nis simply dummy text of the printing and typesetting industry.'; + + expect(tojson(html)).toStrictEqual([ + { + type: 'strong', + children: [{ text: 'Lorem Ipsum' }], + }, + { + text: ' is simply dummy text of the printing and typesetting industry.', + }, + ]); + }); + + it('handles chrome + firefox style copy', () => { + const html = `Hello world`; + + expect(tojson(html)).toStrictEqual([ + { + type: 'strong', + children: [{ text: 'Hello' }], + }, + { + text: ' ', + }, + { + text: 'world', + }, + ]); + }); +}); diff --git a/__tests__/webpack-relative-resolver.test.js b/__tests__/webpack-relative-resolver.test.js index 41519c4c53..960d81a28a 100644 --- a/__tests__/webpack-relative-resolver.test.js +++ b/__tests__/webpack-relative-resolver.test.js @@ -120,10 +120,16 @@ describe('functions as a Webpack resolver plugin', () => { const resolved = []; const resolver = { - plugin(typ, resolveCallback) { - resolveCallback(req, () => flag.push(true)); + ensureHook(typ) { + return this; }, - doResolve(type, req, _, callback) { + getHook(typ) { + return this; + }, + tapAsync: (name, callback) => { + callback(req, {}); + }, + doResolve(type, req, _, resolveContext, callback) { flag.push(true); resolved.push(req.request); }, @@ -143,10 +149,16 @@ describe('functions as a Webpack resolver plugin', () => { const resolved = []; const resolver = { - plugin(typ, resolveCallback) { - resolveCallback(req, () => flag.push(true)); + ensureHook(typ) { + return this; + }, + getHook(typ) { + return this; + }, + tapAsync: (name, callback) => { + callback(req, {}, () => flag.push(true)); }, - doResolve(type, req, _, callback) { + doResolve(type, req, _, resolveContext, callback) { flag.push(true); resolved.push(req.request); }, diff --git a/addon-registry.js b/addon-registry.js index 3dfea21f45..666c0bcd04 100644 --- a/addon-registry.js +++ b/addon-registry.js @@ -148,7 +148,7 @@ class AddonConfigurationRegistry { }, ); - this.initRazzleExtenders(); + this.initAddonExtenders(); } /** @@ -310,18 +310,26 @@ class AddonConfigurationRegistry { } /** - * Allow addons to provide razzle.config extenders. These extenders - * modules (named razzle.extend.js) need to provide two functions: + * Allow addons to provide various extenders. + * + * The razzle.extend.js modules (named razzle.extend.js) needs to provide + * two functions: * `plugins(defaultPlugins) => plugins` and * `modify(...) => config` + * + * The eslint.extend.js */ - initRazzleExtenders() { + initAddonExtenders() { this.getAddons().forEach((addon) => { const base = path.dirname(addon.packageJson); const razzlePath = path.resolve(`${base}/razzle.extend.js`); if (fs.existsSync(razzlePath)) { addon.razzleExtender = razzlePath; } + const eslintPath = path.resolve(`${base}/eslint.extend.js`); + if (fs.existsSync(eslintPath)) { + addon.eslintExtender = eslintPath; + } }); } @@ -340,6 +348,12 @@ class AddonConfigurationRegistry { .filter((e) => e); } + getEslintExtenders() { + return this.getAddons() + .map((o) => o.eslintExtender) + .filter((e) => e); + } + /** * Returns a mapping name:diskpath to be uses in webpack's resolve aliases */ diff --git a/api/Makefile b/api/Makefile index fbe15f133d..3820acf081 100644 --- a/api/Makefile +++ b/api/Makefile @@ -4,7 +4,7 @@ # https://tech.davis-hansson.com/p/make/ SHELL:=bash .ONESHELL: -.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c +.SHELLFLAGS:=-eu -o pipefail -c .SILENT: .DELETE_ON_ERROR: MAKEFLAGS+=--warn-undefined-variables diff --git a/api/README.rst b/api/README.rst index c249527cbc..3203aea283 100644 --- a/api/README.rst +++ b/api/README.rst @@ -45,7 +45,7 @@ are required for cryptography, image handling/scaling support and compression. You can find more up to date details on installing these necessary libraries for linux systems in the 'Mastering Plone' training at --> https://training.plone.org/5/mastering-plone/installation.html#installing-plone-backend +-> https://training.plone.org/mastering-plone/installation.html#installing-plone-backend For Windows you could try WSL (Windows subsystem for Linux) and for Mac OS X for example Homebrew to install these necessary support libraries. diff --git a/api/buildout.cfg b/api/buildout.cfg index fba6d75667..feb689d278 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.0b3/versions.cfg + http://dist.plone.org/release/6.0.2/versions.cfg version-constraints.cfg versions.cfg parts = instance plonesite site-packages test robot-server diff --git a/api/versions.cfg b/api/versions.cfg index 2541a60e2c..d404ee2ec3 100644 --- a/api/versions.cfg +++ b/api/versions.cfg @@ -5,8 +5,8 @@ async-generator = 1.10 collective.folderishtypes = 3.0.0 collective.recipe.plonesite = 1.12.0 h11 = 0.12.0 -plone.restapi = 8.32.0 -plone.volto = 4.0.0a13 +plone.restapi = 8.35.1 +plone.volto = 4.0.7 prompt-toolkit = 2.0.10 pyOpenSSL = 21.0.0 robotframework-debuglibrary = 2.2.2 diff --git a/cypress.config.js b/cypress.config.js index 8e6be41a2b..4f2286287f 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,7 +5,7 @@ module.exports = defineConfig({ chromeWebSecurity: false, projectId: 'hvviu4', e2e: { - baseUrl: 'http://localhost:3000', + baseUrl: 'http://127.0.0.1:3000', excludeSpecPattern: ['*~'], specPattern: 'cypress/tests/**/*.{js,jsx,ts,tsx}', }, diff --git a/cypress/fixtures/halfdome2022.jpg b/cypress/fixtures/halfdome2022.jpg new file mode 100644 index 0000000000..58c9411ca2 Binary files /dev/null and b/cypress/fixtures/halfdome2022.jpg differ diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5739a60e38..cd2e6f23d4 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -2,7 +2,7 @@ import '@testing-library/cypress/add-commands'; import { getIfExists } from '../helpers'; import { ploneAuth } from './constants'; -const HOSTNAME = Cypress.env('BACKEND_HOST') || 'localhost'; +const HOSTNAME = Cypress.env('BACKEND_HOST') || '127.0.0.1'; const GUILLOTINA_API_URL = `http://${HOSTNAME}:8081/db/web`; const PLONE_SITE_ID = Cypress.env('SITE_ID') || 'plone'; const PLONE_API_URL = @@ -11,6 +11,11 @@ const PLONE_API_URL = const SLATE_SELECTOR = '.content-area .slate-editor [contenteditable=true]'; const SLATE_TITLE_SELECTOR = '.block.inner.title [contenteditable="true"]'; +const TABLE_SLATE_SELECTOR = + '.celled.fixed.table tbody tr:nth-child(1) td:first-child() [contenteditable="true"]'; +const TABLE_HEAD_SLATE_SELECTOR = + '.celled.fixed.table thead tr th:first-child() [contenteditable="true"]'; + const ploneAuthObj = { user: ploneAuth[0], pass: ploneAuth[1], @@ -46,9 +51,12 @@ Cypress.Commands.add( contentType, contentId, contentTitle, + contentDescription, path = '', allow_discussion = false, transition = '', + bodyModifier = (body) => body, + image = false, }) => { let api_url, auth; if (Cypress.env('API') === 'guillotina') { @@ -61,40 +69,44 @@ Cypress.Commands.add( api_url = PLONE_API_URL; auth = ploneAuthObj; } + + const defaultParams = { + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + description: contentDescription, + allow_discussion: allow_discussion, + }, + }; + if (contentType === 'File') { - return cy.request({ - method: 'POST', - url: `${api_url}/${path}`, - headers: { - Accept: 'application/json', - }, - auth: auth, - body: { - '@type': contentType, - id: contentId, - title: contentTitle, + const params = { + ...defaultParams, + body: bodyModifier({ + ...defaultParams.body, file: { data: 'dGVzdGZpbGUK', encoding: 'base64', filename: 'lorem.txt', 'content-type': 'text/plain', }, - allow_discussion: allow_discussion, - }, - }); + }), + }; + + return cy.request(params); } if (contentType === 'Image') { - return cy.request({ - method: 'POST', - url: `${api_url}/${path}`, - headers: { - Accept: 'application/json', - }, - auth: auth, - body: { - '@type': contentType, - id: contentId, - title: contentTitle, + const params = { + ...defaultParams, + body: bodyModifier({ + ...defaultParams.body, image: { data: 'iVBORw0KGgoAAAANSUhEUgAAANcAAAA4CAMAAABZsZ3QAAAAM1BMVEX29fK42OU+oMvn7u9drtIPisHI4OhstdWZyt4fkcXX5+sAg74umMhNp86p0eJ7vNiKw9v/UV4wAAAAAXRSTlMAQObYZgAABBxJREFUeF7tmuty4yAMhZG4X2zn/Z92J5tsBJwWXG/i3XR6frW2Y/SBLIRAfaQUDNt8E5tLUt9BycfcKfq3R6Mlfyimtx4rzp+K3dtibXkor99zsEqLYZltblTecciogoh+TXfY1Ve4dn07rCDGG9dHSEEOg/GmXl0U1XDxTKxNK5De7BxsyyBr6gGm2/vPxKJ8F6f7BXKfRMp1xIWK9A+5ks25alSb353dWnDJN1k35EL5f8dVGifTf/4tjUuuFq7u4srmXC60yAmldLXIWbg65RKU87lcGxJCFqUPv0IacW0PmSivOZFLE908inPToMmii/roG+MRV/O8FU88i8tFsxV3a06MFUw0Qu7RmAtdV5/HVVaOVMTWNOWSwMljLhzhcB6XIS7OK5V6AvRDNN7t5VJWQs1J40UmalbK56usBG/CuCHSYuc+rkUGeMCViNRARPrzW52N3oQLe6WifNliSuuGaH3czbVNudI9s7ZLUCLHVwWlyES522o1t14uvmbblmVTKqFjaZYJFSTPP4dLL1kU1z7p0lzdbRulmEWLxoQX+z9ce7A8GqEEucllLxePuZwdJl1Lezu0hoswvTPt61DrFcRuujV/2cmlxaGBC7Aw6cpovGANwRiSdOAWJ5AGy4gLL64dl0QhUEAuEUNws+XxV+OKGPdw/hESGYF9XEGaFC7sNLMSXWJjHsnanYi87VK428N2uxpOjOFANcagLM5l+7mSycM8KknZpKLcGi6jmzWGr/vLurZ/0g4u9AZuAoeb5r1ceQhyiTPY1E4wUR6u/F3H2ojSpXMMriBPT9cezTto8Cx+MsglHL4fv1Rxrb1LVw9yvyQpJ3AhFnLZfuRLH2QsOG3FGGD20X/th/u5bFAt16Bt308KjF+MNOXgl/SquIEySX3GhaZvc67KZbDxcCDORz2N8yCWPaY5lyQZO7lQ29fnZbt3Xu6qoge4+DjXl/MocySPOp9rlvdyznahRyHEYd77v3LhugOXDv4J65QXfl803BDAdaWBEDhfVx7nKofjoVCgxnUAqw/UAUDPn788BDvQuG4TDtdtUPvzjSlXAB8DvaDOhhrmhwbywylXAm8CvaouikJTL93gs3y7Yy4VYbIxOHrcMizPqWOjqO9l3Uz52kibQy4xxOgqhJvD+w5rvokOcAlGvNCfeqCv1ste1stzLm0f71Iq3ZfTrPfuE5nhPtF+LvQE2lffQC7pYtQy3tdzdrKvd5TLVVzDetScS3nEKmmwDyt1Cev1kX3YfbvzNK4fzrlw+cB6vm+uiUgf2zdXI62241LawCb7Pi5FXFPF8KpzDoF/Sw2lg+GrHNbno1mhPu+VCF/vfMnw06PnUl6j48dVHD3jHNHPua+fc3o/5yp/zsGi0vYtzi3Pz5mHd4T6BWMIlewacd63AAAAAElFTkSuQmCC', @@ -102,38 +114,75 @@ Cypress.Commands.add( filename: 'image.png', 'content-type': 'image/png', }, - }, - }); + }), + }; + + return cy.request(params); } if ( ['Document', 'News Item', 'Folder', 'CMSFolder'].includes(contentType) ) { - return cy - .request({ - method: 'POST', - url: `${api_url}/${path}`, - headers: { - Accept: 'application/json', + const params = { + ...defaultParams, + body: { + ...defaultParams.body, + blocks: { + 'd3f1c443-583f-4e8e-a682-3bf25752a300': { '@type': 'title' }, + '7624cf59-05d0-4055-8f55-5fd6597d84b0': { '@type': 'slate' }, }, - auth: auth, - body: { - '@type': contentType, - id: contentId, - title: contentTitle, - blocks: { - 'd3f1c443-583f-4e8e-a682-3bf25752a300': { '@type': 'title' }, - '7624cf59-05d0-4055-8f55-5fd6597d84b0': { '@type': 'slate' }, - }, - blocks_layout: { - items: [ - 'd3f1c443-583f-4e8e-a682-3bf25752a300', - '7624cf59-05d0-4055-8f55-5fd6597d84b0', - ], - }, - allow_discussion: allow_discussion, + blocks_layout: { + items: [ + 'd3f1c443-583f-4e8e-a682-3bf25752a300', + '7624cf59-05d0-4055-8f55-5fd6597d84b0', + ], }, - }) - .then(() => { + }, + }; + + if (image) { + let sourceFilename = 'cypress/fixtures/halfdome2022.jpg'; + let imageObject = { + encoding: 'base64', + filename: 'image.jpg', + 'content-type': 'image/jpg', + }; + if (typeof image === 'object') { + sourceFilename = image.sourceFilename; + imageObject = { + ...imageObject, + ...image, + }; + } + cy.readFile(sourceFilename, 'base64').then((encodedImage) => { + const withImageParams = { + ...params, + body: bodyModifier({ + ...params.body, + preview_image: { + ...imageObject, + data: encodedImage, + }, + }), + }; + + return cy.request(withImageParams).then(() => { + if (transition) { + cy.setWorkflow({ + path: path || contentId, + review_state: transition, + }); + } + console.log(`${contentType} created`); + }); + }); + } else { + const documentParams = { + ...params, + body: bodyModifier({ + ...params.body, + }), + }; + return cy.request(documentParams).then(() => { if (transition) { cy.setWorkflow({ path: path || contentId, @@ -142,6 +191,7 @@ Cypress.Commands.add( } console.log(`${contentType} created`); }); + } } else { return cy .request({ @@ -151,12 +201,12 @@ Cypress.Commands.add( Accept: 'application/json', }, auth: auth, - body: { + body: bodyModifier({ '@type': contentType, id: contentId, title: contentTitle, allow_discussion: allow_discussion, - }, + }), }) .then(() => { if (transition) { @@ -195,10 +245,34 @@ Cypress.Commands.add('removeContent', ({ path = '' }) => { }); }); +// Get content +Cypress.Commands.add('getContent', ({ path = '' }) => { + let api_url, auth; + if (Cypress.env('API') === 'guillotina') { + api_url = GUILLOTINA_API_URL; + auth = { + user: 'root', + pass: 'root', + }; + } else { + api_url = PLONE_API_URL; + auth = ploneAuthObj; + } + + return cy.request({ + method: 'get', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + }); +}); + // --- Add DX Content-Type ---------------------------------------------------------- Cypress.Commands.add('addContentType', (name) => { let api_url, auth; - api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:8080/Plone'; auth = ploneAuthObj; return cy @@ -219,7 +293,7 @@ Cypress.Commands.add('addContentType', (name) => { // --- Remove DX behavior ---------------------------------------------------------- Cypress.Commands.add('removeContentType', (name) => { let api_url, auth; - api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:8080/Plone'; auth = ploneAuthObj; return cy @@ -238,7 +312,7 @@ Cypress.Commands.add('removeContentType', (name) => { // --- Add DX field ---------------------------------------------------------- Cypress.Commands.add('addSlateJSONField', (type, name) => { let api_url, auth; - api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:8080/Plone'; auth = ploneAuthObj; return cy @@ -263,7 +337,7 @@ Cypress.Commands.add('addSlateJSONField', (type, name) => { // --- Remove DX field ---------------------------------------------------------- Cypress.Commands.add('removeSlateJSONField', (type, name) => { let api_url, auth; - api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:8080/Plone'; auth = ploneAuthObj; return cy @@ -291,7 +365,7 @@ Cypress.Commands.add( password = 'password', roles = ['Member', 'Reader', 'Editor'], groups = { - '@id': 'http://localhost:3000/@users', + '@id': 'http://127.0.0.1:3000/@users', items: [ { id: 'AuthenticatedUsers', @@ -375,7 +449,7 @@ Cypress.Commands.add( password = ploneAuth[1], roles = ['Member', 'Reader'], users = { - '@id': 'http://localhost:3000/@groups', + '@id': 'http://127.0.0.1:3000/@groups', items: [], items_total: 0, }, @@ -589,7 +663,7 @@ Cypress.Commands.add( (query, htmlContent) => { return cy .wrap(query) - .type(' ') + .type(' {backspace}') .trigger('paste', createHtmlPasteEvent(htmlContent)); }, ); @@ -737,6 +811,12 @@ function createHtmlPasteEvent(htmlContent) { ); } +Cypress.Commands.add('addNewBlock', (blockName, createNewSlate = false) => { + let block; + block = cy.getSlate(createNewSlate).type(`/${blockName}{enter}`); + return block; +}); + Cypress.Commands.add('navigate', (route = '') => { return cy.window().its('appHistory').invoke('push', route); }); @@ -749,3 +829,23 @@ Cypress.Commands.add('settings', (key, value) => { return cy.window().its('settings'); }); Cypress.Commands.add('getIfExists', getIfExists); + +Cypress.Commands.add('getTableSlate', (header = false) => { + let slate; + + cy.addNewBlock('table'); + cy.wait(2000); + + const selector = header ? TABLE_HEAD_SLATE_SELECTOR : TABLE_SLATE_SELECTOR; + + cy.getIfExists( + selector, + () => { + slate = cy.get(selector).last(); + }, + () => { + slate = cy.get(selector, { timeout: 10000 }).last(); + }, + ); + return slate; +}); diff --git a/cypress/support/guillotina.js b/cypress/support/guillotina.js index 5300ff21cd..f8f9275e3f 100644 --- a/cypress/support/guillotina.js +++ b/cypress/support/guillotina.js @@ -3,7 +3,7 @@ export function setupGuillotina() { Authorization: 'Basic cm9vdDpyb290', 'Content-Type': 'application/json', }; - const api_url = 'http://localhost:8081/db'; + const api_url = 'http://127.0.0.1:8081/db'; cy.request({ method: 'POST', @@ -62,7 +62,7 @@ export function tearDownGuillotina({ allowFail = false } = {}) { Authorization: 'Basic cm9vdDpyb290', 'Content-Type': 'application/json', }; - const api_url = 'http://localhost:8081/db'; + const api_url = 'http://127.0.0.1:8081/db'; cy.request({ method: 'DELETE', diff --git a/cypress/support/reset-fixture.js b/cypress/support/reset-fixture.js index a9a5fad79c..ebe6fffaf6 100644 --- a/cypress/support/reset-fixture.js +++ b/cypress/support/reset-fixture.js @@ -1,5 +1,5 @@ function setup() { - const api_url = Cypress.env('API_PATH') || 'http://localhost:55001/plone'; + const api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:55001/plone'; cy.request({ method: 'POST', url: `${api_url}/RobotRemote`, @@ -10,7 +10,7 @@ function setup() { } function teardown() { - const api_url = Cypress.env('API_PATH') || 'http://localhost:55001/plone'; + const api_url = Cypress.env('API_PATH') || 'http://127.0.0.1:55001/plone'; cy.request({ method: 'POST', url: `${api_url}/RobotRemote`, diff --git a/cypress/support/upgradetests.js b/cypress/support/upgradetests.js index 8d8d976636..03205a9c70 100644 --- a/cypress/support/upgradetests.js +++ b/cypress/support/upgradetests.js @@ -6,7 +6,7 @@ export const getsystemNeedsUpgrade = { plone_gs_metadata_version_file_system: '6008', plone_gs_metadata_version_installed: '6006', plone_restapi_version: '8.32.0', - plone_version: '6.0.0b3', + plone_version: '6.0.0', python_version: '3.9.14 (main, Sep 13 2022, 03:20:56) \n[GCC 10.2.1 20210110]', upgrade: true, diff --git a/cypress/tests/core/basic/autologin.js b/cypress/tests/core/basic/autologin.js index 214d03e478..2e3a5627ee 100644 --- a/cypress/tests/core/basic/autologin.js +++ b/cypress/tests/core/basic/autologin.js @@ -2,7 +2,7 @@ import { ploneAuth } from '../../../support/constants'; describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:55001/plone'; + const api_url = 'http://127.0.0.1:55001/plone'; const user = ploneAuth[0]; const password = ploneAuth[1]; diff --git a/cypress/tests/core/basic/language-controlpanel.js b/cypress/tests/core/basic/language-controlpanel.js new file mode 100644 index 0000000000..fb734abde2 --- /dev/null +++ b/cypress/tests/core/basic/language-controlpanel.js @@ -0,0 +1,26 @@ +describe('Language control-panel', () => { + beforeEach(() => { + cy.visit('/'); + cy.autologin(); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + }); + + it('Displays all valid fields in language control-panel', () => { + cy.visit('/controlpanel/language'); + cy.get('main').within(() => { + cy.get('.field-wrapper-default_language').should('be.visible'); + cy.get('.field-wrapper-available_languages').should('be.visible'); + cy.get('.field-wrapper-use_combined_language_codes').should('be.visible'); + }); + }); + + it('Does not display unwanted fields in language control-panel', () => { + cy.visit('/controlpanel/language'); + cy.get('main').within(() => { + cy.get('.field-wrapper-always_show_selector').should('not.exist'); + cy.get('.field-wrapper-display_flags').should('not.exist'); + }); + }); +}); diff --git a/cypress/tests/core/basic/sharing.js b/cypress/tests/core/basic/sharing.js index de532dacf2..423f1e5beb 100644 --- a/cypress/tests/core/basic/sharing.js +++ b/cypress/tests/core/basic/sharing.js @@ -67,7 +67,7 @@ describe('Sharing Tests', () => { cy.visit('/logout'); cy.wait('@logout'); - cy.autologin('test-user'); + cy.autologin('test-user', 'correct horse battery staple'); cy.visit('/my-page'); cy.findByRole('heading', { name: /my page/i }).should('exist'); }); diff --git a/cypress/tests/core/blocks/blocks-autofocus.js b/cypress/tests/core/blocks/blocks-autofocus.js index 225942ae5f..9e9c27be21 100644 --- a/cypress/tests/core/blocks/blocks-autofocus.js +++ b/cypress/tests/core/blocks/blocks-autofocus.js @@ -17,15 +17,10 @@ describe('New Block Auto Focus Tests', () => { cy.navigate('/my-page/edit'); cy.intercept('GET', '/**/my-page').as('content'); cy.intercept('PATCH', '*').as('save'); - // when I add a text block - //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); }); it('Press Enter on a description block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Text').click(); - cy.get('.blocks-chooser .text').contains('Description').click(); + cy.addNewBlock('description'); cy.get('.documentDescription').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -35,8 +30,10 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a text block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Text').click(); - cy.get('.blocks-chooser .text').contains('Text').click(); + cy.getSlate().click(); + cy.get('button.block-add-button').click(); + cy.get('.blocks-chooser .title').contains('Text').click({ force: true }); + cy.get('.blocks-chooser .text').contains('Text').click({ force: true }); cy.get('.text-slate-editor-inner').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -46,8 +43,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a image block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Media').click(); - cy.get('.blocks-chooser .media').contains('Image').click(); + cy.addNewBlock('image'); cy.get('.block-editor-image').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -57,8 +53,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a video block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Media').click(); - cy.get('.blocks-chooser .media').contains('Video').click(); + cy.addNewBlock('video'); cy.get('.block-editor-video').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -68,8 +63,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a listing block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); cy.get('.block-editor-listing').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -79,8 +73,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a table of contents block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Table of Contents').click(); + cy.addNewBlock('contents'); cy.get('.block-editor-toc').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -90,8 +83,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a maps block adds new autofocused default block', () => { - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); cy.get('.block-editor-maps').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -101,9 +93,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a html block adds new autofocused default block', () => { - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('HTML').click(); + cy.addNewBlock('html'); cy.get('.block-editor-html').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) @@ -113,9 +103,7 @@ describe('New Block Auto Focus Tests', () => { }); it('Press Enter on a search block adds new autofocused default block', () => { - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Search').click(); + cy.addNewBlock('search'); cy.get('.block-editor-search').first().click().type('{enter}'); cy.get('*[class^="block-editor"]') .eq(2) diff --git a/cypress/tests/core/blocks/blocks-chooser.js b/cypress/tests/core/blocks/blocks-chooser.js new file mode 100644 index 0000000000..f5816a8db7 --- /dev/null +++ b/cypress/tests/core/blocks/blocks-chooser.js @@ -0,0 +1,62 @@ +describe('Blocks Tests', () => { + beforeEach(() => { + cy.intercept('POST', '*').as('saveImage'); + cy.intercept('GET', '/**/image.png/@@images/image').as('getImage'); + // given a logged in editor and a page in edit mode + cy.visit('/'); + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'My Page', + }); + cy.visit('/my-page'); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('my-page'); + cy.navigate('/my-page/edit'); + }); + + it('Add image block', () => { + // when I add an image block + cy.getSlate().click(); + cy.get('.ui.basic.icon.button.block-add-button').click(); + cy.get('.ui.basic.icon.button.image').contains('Image').click(); + cy.get('.block.image .ui.input input[type="text"]').type( + `https://github.com/plone/volto/raw/master/logos/volto-colorful.png{enter}`, + ); + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/my-page'); + + // then the page view should contain the image block + cy.get('#page-document img').should( + 'have.attr', + 'src', + 'https://github.com/plone/volto/raw/master/logos/volto-colorful.png', + ); + + cy.get('#page-document img') + .should('be.visible') + .and(($img) => { + // "naturalWidth" and "naturalHeight" are set when the image loads + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); + }); + + it('Press Enter on a listing block adds new autofocused default block', () => { + cy.getSlate().click(); + cy.get('button.block-add-button').click(); + cy.get('.blocks-chooser .title').contains('Common').click(); + cy.get('.blocks-chooser .common') + .contains('Listing') + .click({ force: true }); + cy.get('.block-editor-listing').first().click().type('{enter}'); + cy.get('*[class^="block-editor"]') + .eq(2) + .within(() => { + return cy.get('.selected'); + }); + }); +}); diff --git a/cypress/tests/core/blocks/blocks-copypaste.js b/cypress/tests/core/blocks/blocks-copypaste.js index 6cfa17154f..8b1f0d7091 100644 --- a/cypress/tests/core/blocks/blocks-copypaste.js +++ b/cypress/tests/core/blocks/blocks-copypaste.js @@ -20,10 +20,7 @@ describe('Blocks copy/paste', () => { cy.intercept('PATCH', '/**/my-page').as('save'); cy.intercept('GET', '/**/my-page').as('content'); // GIVEN: A page with multiple blocks - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); cy.get(`.block.maps .toolbar-inner .ui.input input`) .type( '', @@ -62,10 +59,7 @@ describe('Blocks copy/paste', () => { // GIVEN: A page with multiple blocks cy.intercept('PATCH', '/**/my-page').as('save'); cy.intercept('GET', '/**/my-page').as('content'); - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); cy.get(`.block.maps .toolbar-inner .ui.input input`) .type( '', @@ -105,10 +99,7 @@ describe('Blocks copy/paste', () => { cy.intercept('PATCH', '/**/my-page').as('save'); cy.intercept('GET', '/**/my-page').as('content'); // GIVEN: A page with multiple blocks - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); cy.get(`.block.maps .toolbar-inner .ui.input input`) .type( '', diff --git a/cypress/tests/core/blocks/blocks-listing.js b/cypress/tests/core/blocks/blocks-listing.js index c6de188ce0..bea65dac47 100644 --- a/cypress/tests/core/blocks/blocks-listing.js +++ b/cypress/tests/core/blocks/blocks-listing.js @@ -55,10 +55,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('My title'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //verify before save cy.get(`.block.listing .listing-body:first-of-type`).contains( @@ -115,10 +112,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('My title'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //verify before save cy.get(`.block.listing .listing-body:first-of-type`).contains( @@ -137,7 +131,7 @@ describe('Listing Block Tests', () => { ); }); - it.only('Add Listing block - results preview', () => { + it('Add Listing block - results preview', () => { cy.intercept('PATCH', '/**/my-page').as('save'); cy.intercept('GET', '/**/my-page').as('content'); cy.intercept('GET', '/**/@types/Document').as('schema'); @@ -171,10 +165,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('My title'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); cy.get('.sidebar-container .tabs-wrapper .menu .item') .contains('Block') @@ -250,10 +241,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('My title'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //verify before save cy.get(`.block.listing .listing-body:first-of-type`).contains( @@ -308,10 +296,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('My title'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //******** add Type criteria filter cy.get('.querystring-widget .fields').contains('Add criteria').click(); @@ -372,10 +357,7 @@ describe('Listing Block Tests', () => { //add listing block cy.scrollTo('bottom'); - cy.getSlate(true).click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing', true); //******** add Type criteria filter cy.get('.sidebar-container .tabs-wrapper .menu .item') @@ -451,10 +433,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('Listing block - Test Criteria: short-name'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //******** add short-name criteria filter cy.get('.sidebar-container .tabs-wrapper .menu .item') @@ -544,10 +523,7 @@ describe('Listing Block Tests', () => { ); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //******** add location criteria filter cy.get('.sidebar-container .tabs-wrapper .menu .item') @@ -636,10 +612,7 @@ describe('Listing Block Tests', () => { ); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //******** add location criteria filter cy.get('.sidebar-container .tabs-wrapper .menu .item') @@ -730,10 +703,7 @@ describe('Listing Block Tests', () => { cy.wait('@schema'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //******** add location criteria filter cy.get('.sidebar-container .tabs-wrapper .menu .item') @@ -816,10 +786,7 @@ describe('Listing Block Tests', () => { cy.clearSlateTitle().type('Listing block - respect batching and limits'); //add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); //verify before save cy.get(`.block.listing .listing-body:first-of-type`).contains('My Folder'); diff --git a/cypress/tests/core/blocks/blocks-map.js b/cypress/tests/core/blocks/blocks-map.js index c4872e9671..9644513af9 100644 --- a/cypress/tests/core/blocks/blocks-map.js +++ b/cypress/tests/core/blocks/blocks-map.js @@ -18,10 +18,8 @@ describe('Map Block Tests', () => { it('Add maps block - Google Maps', () => { // when I add a maps block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); + cy.get(`.block.maps .toolbar-inner .ui.input input`) .type( '', @@ -43,10 +41,8 @@ describe('Map Block Tests', () => { it('Add maps block - OpenStreet Maps', () => { // when I add a maps block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Maps').click(); + cy.addNewBlock('maps'); + cy.get(`.block.maps .toolbar-inner .ui.input input`) .type( 'This is HTML`, ); @@ -92,10 +89,7 @@ describe('Blocks Tests', () => { cy.intercept('PATCH', '*').as('save'); cy.intercept('GET', '/**/my-page').as('content'); // Edit - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.ui.buttons .button.slateTable').click(); + cy.addNewBlock('table'); cy.wait(2000); cy.get( '.celled.fixed.table thead tr th:first-child() [contenteditable="true"]', @@ -235,4 +229,19 @@ describe('Blocks Tests', () => { // 'header-two', // ); // }); + + it('Handles unknown blocks', () => { + cy.createContent({ + contentType: 'Document', + contentId: 'test-doc', + contentTitle: 'my test document', + bodyModifier(body) { + body.blocks['abc'] = { '@type': 'missing' }; + body.blocks_layout.items.push('abc'); + return body; + }, + }); + cy.visit('/test-doc'); + cy.get('#page-document div').should('have.text', 'Unknown Block missing'); + }); }); diff --git a/cypress/tests/core/blocks/hero.js b/cypress/tests/core/blocks/hero.js index d93012c22b..445dbc13f3 100644 --- a/cypress/tests/core/blocks/hero.js +++ b/cypress/tests/core/blocks/hero.js @@ -29,10 +29,7 @@ describe('Blocks Tests', () => { cy.navigate('/my-page/edit'); cy.wait('@schema'); - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .hero').contains('Hero').click(); + cy.addNewBlock('hero'); // cy.fixture(expectedFile).then(fileContent => { // cy.get(`.block.${block} [data-cy="dropzone]`).upload( diff --git a/cypress/tests/core/blocks/teaser.js b/cypress/tests/core/blocks/teaser.js new file mode 100644 index 0000000000..60a4c13a4c --- /dev/null +++ b/cypress/tests/core/blocks/teaser.js @@ -0,0 +1,49 @@ +context('Blocks Acceptance Tests', () => { + beforeEach(() => { + cy.visit('/'); + cy.viewport('macbook-16'); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Document', + }); + cy.autologin(); + }); + + it('As editor I can add a (standalone) Teaser block', () => { + // GIVEN a Document with the title document and a Document to reference with the title Blue Orchidees + cy.createContent({ + contentType: 'Document', + contentId: 'blue-orchidees', + contentTitle: 'Blue Orchidees', + contentDescription: 'are growing on the mountain tops', + image: true, + path: '/document', + }); + cy.visit('/document/edit'); + // WHEN I create a Teaser block + cy.get('.block .slate-editor [contenteditable=true]').click(); + cy.get('.button .block-add-button').click({ force: true }); + cy.get('.blocks-chooser .mostUsed .button.teaser') + .contains('Teaser') + .click({ force: true }); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.get('[aria-label="Select Blue Orchidees"]').dblclick(); + cy.wait(500); + cy.get('.align-buttons .ui.buttons button[aria-label="Center"]').click(); + cy.get('#toolbar-save').click(); + + // THEN I can see the Teaser block + cy.visit('/document'); + cy.get('.block.teaser').should('have.class', 'has--align--center'); + cy.get('.block.teaser .image-wrapper img') + .should('have.attr', 'src') + .and('include', '/document/blue-orchidees/@@images/preview_image-'); + cy.get('.block.teaser .content h2').contains('Blue Orchidees'); + cy.get('.block.teaser .content p').contains( + 'are growing on the mountain tops', + ); + }); +}); diff --git a/cypress/tests/core/guillotina/autologin.js b/cypress/tests/core/guillotina/autologin.js index 67eb37e9be..fce720381b 100644 --- a/cypress/tests/core/guillotina/autologin.js +++ b/cypress/tests/core/guillotina/autologin.js @@ -1,6 +1,6 @@ describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:8081/db/web'; + const api_url = 'http://127.0.0.1:8081/db/web'; const user = 'admin'; const password = 'admin'; diff --git a/cypress/tests/core/volto-slate/09-block-slate-slashmenu.js b/cypress/tests/core/volto-slate/09-block-slate-slashmenu.js new file mode 100644 index 0000000000..8cce4cb989 --- /dev/null +++ b/cypress/tests/core/volto-slate/09-block-slate-slashmenu.js @@ -0,0 +1,14 @@ +import { slateBeforeEach } from '../../../support/e2e'; + +describe('SlashMenu Test: Shortcuts', () => { + beforeEach(slateBeforeEach); + + it('As editor I can create a Table block using the SlashMenu shortcut', function () { + // Use SlashMenu shortcut to create a table block + cy.getSlateEditorAndType('/t').type('{enter}'); + cy.toolbarSave(); + + // then the page view should contain a table + cy.get('#page-document table').should('be.visible'); + }); +}); diff --git a/cypress/tests/core/volto-slate/24-block-slate-format-boldlists.js b/cypress/tests/core/volto-slate/24-block-slate-format-boldlists.js index 1ebb4fb339..1971244b8f 100644 --- a/cypress/tests/core/volto-slate/24-block-slate-format-boldlists.js +++ b/cypress/tests/core/volto-slate/24-block-slate-format-boldlists.js @@ -40,31 +40,31 @@ describe('Block Tests: Bold Bulleted lists', () => { 'sleep furiously.', ); }); + it('As editor I can paste internal(slate formatted) formatted bulleted lists', function () { // Complete chained commands - cy.getSlateEditorAndType('This is slate"s own bold content'); - cy.setSlateSelection('This is slate"s own'); + cy.getSlateEditorAndType("This is slate's own bold content"); + cy.setSlateSelection("This is slate's own"); //create a bold bullted list cy.clickSlateButton('Bulleted list'); cy.clickSlateButton('Bold'); //copy content "This is slate"s own" - cy.setSlateCursor('content') - .type('{enter}') - .pasteClipboard( - 'This is slate"s own', - ); + cy.setSlateCursor('content').type('{enter}'); + cy.getSlate().pasteClipboard( + "This is slate's own", + ); // Save cy.toolbarSave(); cy.get('[id="page-document"] ul li:nth-child(1) strong').contains( - 'This is slate"s own', + "This is slate's own", ); //pasted content cy.get('[id="page-document"] ul li:nth-child(2) strong').contains( - 'This is slate"s own', + "This is slate's own", ); }); @@ -83,6 +83,7 @@ describe('Block Tests: Bold Bulleted lists', () => { // Save cy.toolbarSave(); + // cy.pause(); cy.get('[id="page-document"] ul li:nth-child(1) strong').contains( 'This is slate"s own bold content', diff --git a/cypress/tests/core/volto-slate/27-block-slate-paste-html.js b/cypress/tests/core/volto-slate/27-block-slate-paste-html.js new file mode 100644 index 0000000000..74a815f29c --- /dev/null +++ b/cypress/tests/core/volto-slate/27-block-slate-paste-html.js @@ -0,0 +1,47 @@ +import { slateBeforeEach } from '../../../support/e2e'; + +describe('Block Tests: external text containing html contents/tags ', () => { + beforeEach(slateBeforeEach); + + it('should paste external text containing html', function () { + // cy.getSlateEditorAndType('Let"s paste external html texts'); + // cy.setSlateCursor('texts').type('{enter}'); + cy.getSlate().pasteClipboard( + '
For simplicity, emissions arising (CRF 3B) were presented for all livestock type h CH4 and N2O), e CO2e value.single CO2e figure.
', + ); + + // Save + cy.toolbarSave(); + + cy.get('[id="page-document"] p').should('have.length', 1); + }); + + it('should paste external formatted text and does not split the blocks', function () { + // The idea is pasteClipboard should only apply on its attached slate block + // by not splitting them into blocks. + cy.getSlate().pasteClipboard( + `Lorem Ipsum +is simply dummy text of the printing and typesetting industry. +
`, + ); + + // Save + cy.toolbarSave(); + cy.get('[id="page-document"] > p:nth-of-type(1)').should( + 'have.html', + `Lorem Ipsum is simply dummy text of the printing and typesetting industry.`, + ); + }); + + it('should paste external text containing empty anchor links', function () { + cy.getSlate().pasteClipboard( + ` + `, + ); + + // Save + cy.toolbarSave(); + + cy.get('[id="page-document"] p a').should('have.length', 1); + }); +}); diff --git a/cypress/tests/core/volto-slate/28-table-block-slate-paste.js b/cypress/tests/core/volto-slate/28-table-block-slate-paste.js new file mode 100644 index 0000000000..50b3b64627 --- /dev/null +++ b/cypress/tests/core/volto-slate/28-table-block-slate-paste.js @@ -0,0 +1,55 @@ +import { slateBeforeEach } from '../../../support/volto-slate'; + +describe('Block Tests: pasting content in table block', () => { + beforeEach(slateBeforeEach); + + it('should paste text', function () { + cy.intercept('PATCH', '/**/my-page').as('save'); + + // Paste + cy.getTableSlate(true) + .focus() + .click() + .pasteClipboard('Some Text from Clipboard'); + + cy.getTableSlate() + .focus() + .click() + .pasteClipboard('Some Text from Clipboard'); + + // Save + cy.toolbarSave(); + cy.wait('@save'); + + // View + cy.get('.celled.fixed.table thead tr th:first').contains( + 'Some Text from Clipboard', + ); + cy.get( + '.celled.fixed.table tbody tr:nth-child(1) td:first-child()', + ).contains('Some Text from Clipboard'); + }); + + it('should paste external text containing html', function () { + // Paste + cy.getTableSlate(true) + .focus() + .click() + .pasteClipboard( + 'For simplicity, emissions arising (CRF 3B) were presented for all livestock type h CH4 and N2O), e CO2e value.single CO2e figure.
', + ); + + cy.getTableSlate() + .focus() + .click() + .pasteClipboard( + 'For simplicity, emissions arising (CRF 3B) were presented for all livestock type h CH4 and N2O), e CO2e value.single CO2e figure.
', + ); + + // Save + cy.toolbarSave(); + + // View + cy.get('[id="page-document"] p').should('have.length', 2); + }); +}); diff --git a/cypress/tests/coresandbox/autologin.js b/cypress/tests/coresandbox/autologin.js index 45c13788ca..bc4d02606d 100644 --- a/cypress/tests/coresandbox/autologin.js +++ b/cypress/tests/coresandbox/autologin.js @@ -2,7 +2,7 @@ import { ploneAuth } from '../../support/constants'; describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:55001/plone'; + const api_url = 'http://127.0.0.1:55001/plone'; const user = ploneAuth[0]; const password = ploneAuth[1]; diff --git a/cypress/tests/coresandbox/defaultview.js b/cypress/tests/coresandbox/defaultview.js new file mode 100644 index 0000000000..e3955c0dce --- /dev/null +++ b/cypress/tests/coresandbox/defaultview.js @@ -0,0 +1,39 @@ +context('Block Default View / Edit Acceptance Tests', () => { + describe('Block Default View / Edit', () => { + beforeEach(() => { + // given a logged in editor and a page in edit mode + cy.visit('/'); + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.visit('/document'); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('document'); + cy.navigate('/document/edit'); + cy.getSlateTitle(); + }); + + it('I can add a block with Default View / Edit based on schema and interact with it', function () { + cy.getSlate().click(); + cy.get('.button .block-add-button').click({ force: true }); + cy.get('.blocks-chooser .mostUsed .button.testBlockDefaultView').click(); + + cy.get('#field-fieldAfterObjectList') + .click() + .type('Colorless green ideas sleep furiously.'); + + cy.get('.page-block').contains('Colorless green ideas sleep furiously.'); + + cy.get('#toolbar-save').click(); + cy.get('[id="page-document"]').contains( + 'Colorless green ideas sleep furiously.', + ); + }); + }); +}); diff --git a/cypress/tests/coresandbox/fields.js b/cypress/tests/coresandbox/fields.js index 7770b54596..ddf4230bd2 100644 --- a/cypress/tests/coresandbox/fields.js +++ b/cypress/tests/coresandbox/fields.js @@ -1,4 +1,106 @@ context('Special fields Acceptance Tests', () => { + describe('Form with default values', () => { + beforeEach(() => { + // given a logged in editor and a page in edit mode + cy.visit('/'); + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.visit('/document'); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('document'); + cy.navigate('/document/edit'); + cy.getSlateTitle(); + }); + + it('As an editor I can add a block that has default values', () => { + cy.intercept('PATCH', '/**/document').as('save'); + cy.intercept('GET', '/**/@types/Document').as('schema'); + cy.getSlate().click(); + cy.get('.button .block-add-button').click({ force: true }); + cy.wait(100); + cy.get('.blocks-chooser .mostUsed .button.testBlock').click(); + + cy.findByLabelText('Field with default').click(); + cy.get('#field-firstWithDefault').should( + 'have.value', + 'Some default value', + ); + + cy.findByLabelText('Add item').click(); + cy.findAllByText('Item #1').should('have.length', 1); + + cy.findByLabelText('Extra').should('have.value', 'Extra default'); + cy.get('#toolbar-save').click(); + cy.wait('@save'); + + cy.navigate('/document/edit'); + cy.wait('@schema'); + + cy.findAllByText('Test Block Edit').click(); + + cy.get('#field-firstWithDefault').should( + 'have.value', + 'Some default value', + ); + cy.findByLabelText('Extra').should('have.value', 'Extra default'); + + cy.getContent({ path: '/document' }).should((response) => { + const { body } = response; + const [, testBlock] = Object.entries(body.blocks).find( + ([, block]) => block['@type'] === 'testBlock', + ); + expect(testBlock.style).to.deep.equal({ color: 'red' }); + expect(testBlock.slides[0].extraDefault).to.deep.equal('Extra default'); + }); + }); + }); + + describe('HTML Richtext Widget', () => { + beforeEach(() => { + // given a logged in editor and a page in edit mode + cy.visit('/'); + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.visit('/document'); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('document'); + cy.navigate('/document/edit'); + cy.getSlateTitle(); + }); + + it('Handles whitespaces properly', () => { + cy.intercept('PATCH', '/**/document').as('save'); + cy.getSlate().click(); + cy.get('.button .block-add-button').click({ force: true }); + cy.get('.blocks-chooser .mostUsed .button.testBlock').click(); + cy.get('#fieldset-default-field-label-html').click(); + cy.get('.slate_wysiwyg_box [contenteditable=true]').type( + ' hello world ', + ); + cy.get('#toolbar-save').click(); + cy.wait('@save'); + + cy.get('.test-block').should( + 'contain.text', + 'hello world
', + ); + }); + }); + describe('ObjectListWidget', () => { beforeEach(() => { // given a logged in editor and a page in edit mode @@ -53,6 +155,7 @@ context('Special fields Acceptance Tests', () => { cy.findAllByText('Item #3').should('have.length', 0); }); }); + describe('Variation field', () => { beforeEach(() => { // given a logged in editor and a page in edit mode @@ -83,6 +186,7 @@ context('Special fields Acceptance Tests', () => { cy.findByText('Custom'); }); }); + describe('ObjectBrowserWidget', () => { beforeEach(() => { // given a logged in editor and a page in edit mode @@ -108,6 +212,7 @@ context('Special fields Acceptance Tests', () => { cy.navigate('/document/edit'); cy.getSlateTitle(); }); + it('As editor I can add a block with an objetBrowserWidget and the context path is preserved', function () { cy.getSlate().click(); cy.get('.button .block-add-button').click({ force: true }); diff --git a/cypress/tests/coresandbox/listingblock.js b/cypress/tests/coresandbox/listingblock.js index a49dc6ad3e..82fe619aec 100644 --- a/cypress/tests/coresandbox/listingblock.js +++ b/cypress/tests/coresandbox/listingblock.js @@ -40,10 +40,7 @@ context('Listing block tests', () => { cy.navigate('/document/edit'); // Add listing block - cy.getSlate().click(); - cy.get('button.block-add-button').click(); - cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.addNewBlock('listing'); // select variation cy.get('#field-variation') @@ -88,10 +85,13 @@ context('Listing block tests', () => { cy.navigate('/document/edit'); // Add listing block + cy.addNewBlock('listing'); cy.getSlate().click(); cy.get('button.block-add-button').click(); cy.get('.blocks-chooser .title').contains('Common').click(); - cy.get('.blocks-chooser .common').contains('Listing').click(); + cy.get('.blocks-chooser .common') + .contains('Listing') + .click({ force: true }); // select variation cy.get('#field-variation') diff --git a/cypress/tests/guillotina/autologin.js b/cypress/tests/guillotina/autologin.js index 67eb37e9be..fce720381b 100644 --- a/cypress/tests/guillotina/autologin.js +++ b/cypress/tests/guillotina/autologin.js @@ -1,6 +1,6 @@ describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:8081/db/web'; + const api_url = 'http://127.0.0.1:8081/db/web'; const user = 'admin'; const password = 'admin'; diff --git a/cypress/tests/multilingual/autologin.js b/cypress/tests/multilingual/autologin.js index 45c13788ca..bc4d02606d 100644 --- a/cypress/tests/multilingual/autologin.js +++ b/cypress/tests/multilingual/autologin.js @@ -2,7 +2,7 @@ import { ploneAuth } from '../../support/constants'; describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:55001/plone'; + const api_url = 'http://127.0.0.1:55001/plone'; const user = ploneAuth[0]; const password = ploneAuth[1]; diff --git a/cypress/tests/workingCopy/autologin.js b/cypress/tests/workingCopy/autologin.js index 45c13788ca..bc4d02606d 100644 --- a/cypress/tests/workingCopy/autologin.js +++ b/cypress/tests/workingCopy/autologin.js @@ -2,7 +2,7 @@ import { ploneAuth } from '../../support/constants'; describe('Autologin Tests', () => { it('Autologin as an standalone test', function () { - const api_url = 'http://localhost:55001/plone'; + const api_url = 'http://127.0.0.1:55001/plone'; const user = ploneAuth[0]; const password = ploneAuth[1]; diff --git a/docker-compose.yml b/docker-compose.yml index 7bd820f2bd..82a49830b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,14 +2,13 @@ version: '3.3' services: backend: - image: plone/plone-backend:6.0.0b2 + image: plone/plone-backend:6.0.1 # Plone 5.2 series can be used too - # image: plone/plone-backend:5.2.9 + # image: plone/plone-backend:5.2.10 ports: - '8080:8080' environment: - SITE=Plone - - 'ADDONS=plone.restapi==8.29.0 plone.volto==4.0.0a13 plone.rest==2.0.0a5' - 'PROFILES=plone.volto:default-homepage' labels: - traefik.enable=true @@ -47,7 +46,7 @@ services: reverse-proxy: # The official v2 Traefik docker image - image: traefik:v2.8 + image: traefik:v2.9 # Enables the web UI and tells Traefik to listen to docker command: --api.insecure=true --providers.docker ports: @@ -57,7 +56,7 @@ services: - "8888:8080" volumes: # So that Traefik can listen to the Docker events - - /var/run/docker.sock:/var/run/docker.sock + - /var/run/docker.sock:/var/run/docker.sock:ro labels: - traefik.http.middlewares.gzip.compress=true - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 diff --git a/docs/source/_static/copy.svg b/docs/source/_static/copy.svg new file mode 100644 index 0000000000..3e9fd912ba --- /dev/null +++ b/docs/source/_static/copy.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index 0f540ff020..d792d49f36 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -35,6 +35,12 @@ figure img, width: 200px; margin-bottom: 1rem; } + +img.inline { + margin: 0; + height: 1em; +} + span.linenos { padding-right: 1em; } @@ -234,3 +240,7 @@ span.guilabel, span.menuselection { font-style: italic; white-space: nowrap; } + +video { + width: 100%; +} diff --git a/docs/source/_static/cut.svg b/docs/source/_static/cut.svg new file mode 100644 index 0000000000..6a3b112b14 --- /dev/null +++ b/docs/source/_static/cut.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/docs/source/_static/paste.svg b/docs/source/_static/paste.svg new file mode 100644 index 0000000000..91e9435607 --- /dev/null +++ b/docs/source/_static/paste.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/source/_static/user-manual/blocks/add-new-block.gif b/docs/source/_static/user-manual/blocks/add-new-block.gif new file mode 100644 index 0000000000..29425365e9 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/add-new-block.gif differ diff --git a/docs/source/_static/user-manual/blocks/block-copy-cut.mp4 b/docs/source/_static/user-manual/blocks/block-copy-cut.mp4 new file mode 100644 index 0000000000..2a6a0658eb Binary files /dev/null and b/docs/source/_static/user-manual/blocks/block-copy-cut.mp4 differ diff --git a/docs/source/_static/user-manual/blocks/block-left-add-icon.png b/docs/source/_static/user-manual/blocks/block-left-add-icon.png new file mode 100644 index 0000000000..bcb7c63d93 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/block-left-add-icon.png differ diff --git a/docs/source/_static/user-manual/blocks/block-paste.mp4 b/docs/source/_static/user-manual/blocks/block-paste.mp4 new file mode 100644 index 0000000000..05ffbc66e6 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/block-paste.mp4 differ diff --git a/docs/source/_static/user-manual/blocks/block-types-menu.png b/docs/source/_static/user-manual/blocks/block-types-menu.png new file mode 100644 index 0000000000..7bd3a57916 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/block-types-menu.png differ diff --git a/docs/source/_static/user-manual/blocks/grid-block-manage-blocks.png b/docs/source/_static/user-manual/blocks/grid-block-manage-blocks.png new file mode 100644 index 0000000000..46554cffd1 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/grid-block-manage-blocks.png differ diff --git a/docs/source/_static/user-manual/blocks/grid-block-number-of-columns.png b/docs/source/_static/user-manual/blocks/grid-block-number-of-columns.png new file mode 100644 index 0000000000..9d3e887867 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/grid-block-number-of-columns.png differ diff --git a/docs/source/_static/user-manual/blocks/hero-block.png b/docs/source/_static/user-manual/blocks/hero-block.png new file mode 100644 index 0000000000..6b284bac90 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/hero-block.png differ diff --git a/docs/source/_static/user-manual/blocks/html-block.png b/docs/source/_static/user-manual/blocks/html-block.png new file mode 100644 index 0000000000..64481f922f Binary files /dev/null and b/docs/source/_static/user-manual/blocks/html-block.png differ diff --git a/docs/source/_static/user-manual/blocks/image-block-configuration-options.png b/docs/source/_static/user-manual/blocks/image-block-configuration-options.png new file mode 100644 index 0000000000..555308d43d Binary files /dev/null and b/docs/source/_static/user-manual/blocks/image-block-configuration-options.png differ diff --git a/docs/source/_static/user-manual/blocks/image-block.png b/docs/source/_static/user-manual/blocks/image-block.png new file mode 100644 index 0000000000..2cdba7290b Binary files /dev/null and b/docs/source/_static/user-manual/blocks/image-block.png differ diff --git a/docs/source/_static/user-manual/blocks/images-grid-block-manage-images.png b/docs/source/_static/user-manual/blocks/images-grid-block-manage-images.png new file mode 100644 index 0000000000..efa356c7cd Binary files /dev/null and b/docs/source/_static/user-manual/blocks/images-grid-block-manage-images.png differ diff --git a/docs/source/_static/user-manual/blocks/images-grid-block-number-of-columns.png b/docs/source/_static/user-manual/blocks/images-grid-block-number-of-columns.png new file mode 100644 index 0000000000..9d3e887867 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/images-grid-block-number-of-columns.png differ diff --git a/docs/source/_static/user-manual/blocks/listing-block-configuration.png b/docs/source/_static/user-manual/blocks/listing-block-configuration.png new file mode 100644 index 0000000000..4ef4628d93 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/listing-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/listing-block.png b/docs/source/_static/user-manual/blocks/listing-block.png new file mode 100644 index 0000000000..8073df0fbd Binary files /dev/null and b/docs/source/_static/user-manual/blocks/listing-block.png differ diff --git a/docs/source/_static/user-manual/blocks/map-blocks-configuration.png b/docs/source/_static/user-manual/blocks/map-blocks-configuration.png new file mode 100644 index 0000000000..c6213fe009 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/map-blocks-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/maps-block.png b/docs/source/_static/user-manual/blocks/maps-block.png new file mode 100644 index 0000000000..fd08e0b62e Binary files /dev/null and b/docs/source/_static/user-manual/blocks/maps-block.png differ diff --git a/docs/source/_static/user-manual/blocks/search-block-configuration.png b/docs/source/_static/user-manual/blocks/search-block-configuration.png new file mode 100644 index 0000000000..43f174a8df Binary files /dev/null and b/docs/source/_static/user-manual/blocks/search-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/search-block.png b/docs/source/_static/user-manual/blocks/search-block.png new file mode 100644 index 0000000000..70c6e72ea7 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/search-block.png differ diff --git a/docs/source/_static/user-manual/blocks/table-block-configuration.png b/docs/source/_static/user-manual/blocks/table-block-configuration.png new file mode 100644 index 0000000000..236762bf2c Binary files /dev/null and b/docs/source/_static/user-manual/blocks/table-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/table-block.png b/docs/source/_static/user-manual/blocks/table-block.png new file mode 100644 index 0000000000..ca73e45924 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/table-block.png differ diff --git a/docs/source/_static/user-manual/blocks/table-of-contents-block-configuration.png b/docs/source/_static/user-manual/blocks/table-of-contents-block-configuration.png new file mode 100644 index 0000000000..6da6c6ad64 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/table-of-contents-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/table-of-contents-block.png b/docs/source/_static/user-manual/blocks/table-of-contents-block.png new file mode 100644 index 0000000000..73faaba5da Binary files /dev/null and b/docs/source/_static/user-manual/blocks/table-of-contents-block.png differ diff --git a/docs/source/_static/user-manual/blocks/teaser-block-configuration.png b/docs/source/_static/user-manual/blocks/teaser-block-configuration.png new file mode 100644 index 0000000000..55c21c78cd Binary files /dev/null and b/docs/source/_static/user-manual/blocks/teaser-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/teaser-block.png b/docs/source/_static/user-manual/blocks/teaser-block.png new file mode 100644 index 0000000000..b8a500982b Binary files /dev/null and b/docs/source/_static/user-manual/blocks/teaser-block.png differ diff --git a/docs/source/_static/user-manual/blocks/video-block-configuration.png b/docs/source/_static/user-manual/blocks/video-block-configuration.png new file mode 100644 index 0000000000..b6c5283ca3 Binary files /dev/null and b/docs/source/_static/user-manual/blocks/video-block-configuration.png differ diff --git a/docs/source/_static/user-manual/blocks/video-block.png b/docs/source/_static/user-manual/blocks/video-block.png new file mode 100644 index 0000000000..870ac385bc Binary files /dev/null and b/docs/source/_static/user-manual/blocks/video-block.png differ diff --git a/docs/source/addons/best-practices.md b/docs/source/addons/best-practices.md index ae5bcbe773..e58eb3c8e2 100644 --- a/docs/source/addons/best-practices.md +++ b/docs/source/addons/best-practices.md @@ -24,7 +24,7 @@ register the most basic configuration of that widget with a name that can be used. On more complicated cases, see if you can structure your code to use the -`settings` configuration registry, or stash your configuration in your block +`settings` {term}`configuration registry`, or stash your configuration in your block registration, for example. As an example: let's say we're building a Color Picker widget and we want to @@ -36,7 +36,7 @@ multiple instances of that color widget with custom color palettes. ### Provide additional configuration -An add-on can ship with multiple Volto configuration loaders. This makes it +An add-on can ship with multiple {term}`Volto configuration loader`s. This makes it possible to provide configuration methods for demo purposes, for example, or to ship with a default "shallow" integration, then provide another separate configuration loader for a deeper integration. diff --git a/docs/source/addons/index.md b/docs/source/addons/index.md index de1b615520..cf456c215b 100644 --- a/docs/source/addons/index.md +++ b/docs/source/addons/index.md @@ -19,86 +19,86 @@ best-practices There are several advanced scenarios where we might want to have more control and flexibility beyond using the plain Volto project to build a site. -We can build Volto add-on products and make them available as generic -Javascript packages that can be included in any Volto project. By doing so we +We can build Volto {term}`add-on` products and make them available as generic +JavaScript packages that can be included in any Volto project. By doing so we can provide code and component reutilization across projects and, of course, benefit from open source collaboration. ```{note} -By declaring a Javascript package as a "Volto addon", Volto provides +By declaring a JavaScript package as a Volto add-on, Volto provides several integration features: language features (so they can be transpiled by Babel), whole-process customization via razzle.extend.js and -integration with Volto's configuration registry. +integration with Volto's {term}`configuration registry`. ``` -The addon can be published to an NPM registry or directly installed from github +The add-on can be published to an NPM registry or directly installed from github by Yarn. By using [mrs-develop](https://github.com/collective/mrs-developer), it's possible to have a workflow similar to zc.buildout's mr.developer, where -you can "checkout" an addon for development. +you can "checkout" an add-on for development. -An addon can be almost anything that a Volto project can be. They can: +An add-on can be almost anything that a Volto project can be. They can: - provide additional views and blocks - override or extend Volto's builtin views, blocks, settings -- shadow (customize) Volto's (or another addon's) modules +- shadow (customize) Volto's (or another add-on's) modules - register custom routes -- provide custom Redux actions and reducers +- provide custom {term}`Redux` actions and reducers - register custom Express middleware for Volto's server process - tweak Volto's Webpack configuration, load custom Razzle and Webpack plugins - even provide a custom theme, just like a regular Volto project does. -## Configuring a Volto project to use an addon +## Configuring a Volto project to use an add-on -You can install a Volto addon just like any other JS package: +You can install a Volto add-on just like any other JS package: ```shell -yarn add name-of-addon +yarn add name-of-add-on ``` -If the addon is not published on NPM, you can retrieve it directly from Github: +If the add-on is not published on NPM, you can retrieve it directly from Github: ```shell yarn add collective/volto-dropdownmenu ``` -Next, you'll need to add the addon (identified by its JS package name) to the +Next, you'll need to add the add-on (identified by its JS package name) to the `addons` key of your Volto project's `package.json`. More details in the next section. -### Loading addon configuration +### Loading add-on configuration -As a convenience, an addon can export configuration functions that can mutate, -in-place, the overall Volto configuration registry. An addon can export multiple +As a convenience, an add-on can export configuration functions that can mutate, +in-place, the overall Volto {term}`configuration registry`. An add-on can export multiple configurations methods, making it possible to selectively choose which specific -addon functionality you want to load. +add-on functionality you want to load. -In your Volto project's ``package.json`` you can allow the addon to alter the -global configuration by adding, in the ``addons`` key, a list of volto addon +In your Volto project's ``package.json`` you can allow the add-on to alter the +global configuration by adding, in the `addons` key, a list of volto add-on package names, like: ```js { "name": "my-nice-volto-project", - ... + "addons": [ - "acme-volto-foo-addon", - "@plone/some-addon", - "collective-another-volto-addon" + "acme-volto-foo-add-on", + "@plone/some-add-on", + "collective-another-volto-add-on" ], - ... + } ``` ```{warning} -Adding the addon package to the `addons` key is obligatory! It allows Volto +Adding the add-on package to the `addons` key is mandatory! It allows Volto to treat that package properly and provide it with BabelJS language features. In Plone terminology, it is like including a Python egg to the `zcml` section of zc.buildout. ``` -Some addons might choose to allow the Volto project to selectively load some of +Some add-ons might choose to allow the Volto project to selectively load some of their configuration, so they may offer additional configuration functions, -which you can load by overloading the addon name in the ``addons`` package.json +which you can load by overloading the add-on name in the `addons` package.json key, like so: ```{code-block} json @@ -107,23 +107,23 @@ key, like so: { "name": "my-nice-volto-project", "addons": [ - "acme-volto-foo-addon:loadOptionalBlocks,overrideSomeDefaultBlock", + "acme-volto-foo-add-on:loadOptionalBlocks,overrideSomeDefaultBlock", "volto-ga" ], } ``` ```{note} -The additional comma-separated names should be exported from the addon +The additional comma-separated names should be exported from the add-on package's ``index.js``. The main configuration function should be exported as -the default. An addon's default configuration method will always be loaded. +the default. An add-on's default configuration method will always be loaded. ``` -If for some reason, you want to manually load the addon, you could always do, +If for some reason, you want to manually load the add-on, you could always do, in your project's ``config.js`` module: ```js -import loadExampleAddon, { enableOptionalBlocks } from 'volto-example-addon'; +import loadExampleAddon, { enableOptionalBlocks } from 'volto-example-add-on'; import * as voltoConfig from '@plone/volto/config'; const config = enableOptionalBlocks(loadExampleAddon(voltoConfig)); @@ -131,7 +131,6 @@ const config = enableOptionalBlocks(loadExampleAddon(voltoConfig)); export blocks = { ...config.blocks, } -... ``` As this is a common operation, Volto provides a helper method for this: @@ -151,26 +150,27 @@ export blocks = { ``` The `applyConfig` helper ensures that each configuration methods returns the -config object, avoiding odd and hard to track errors when developing addons. +config object, avoiding odd and hard to track errors when developing add-ons. -## Creating addons +## Creating add-ons -Volto addon packages are just CommonJS packages. The only requirement is that +Volto add-on packages are just CommonJS packages. The only requirement is that they point the `main` key of their `package.json` to a module that exports, as -a default function that acts as a Volto configuration loader. +a default function that acts as a {term}`Volto configuration loader`. -Although you could simply use `npm init` to generate an addon initial code, +Although you could simply use `npm init` to generate an add-on initial code, we now have a nice [Yeoman-based generator](https://github.com/plone/generator-volto) that you can use: -``` +```shell npm install -g @plone/generator-volto yo @plone/volto:addon [` block instead of a `` block. Please check if you have a CSS bound to that node and adjust accordingly.
+The `LinkView` component with the literal `The link address is: ` block instead of a `` block.
+Please check if you have a CSS bound to that node and adjust accordingly.
### Rename core-sandbox fixture to coresandbox
@@ -739,20 +881,20 @@ The `getVocabulary` action has changed API. Before, it used separate positional
## Upgrading to Volto 13.x.x
-## Deprecating NodeJS 10
+### Deprecating NodeJS 10
Since April 30th, 2021 NodeJS 10 is out of Long Term Support by the NodeJS community, so
we are deprecating it in Volto 13. Please update your projects to a NodeJS LTS version
(12 or 14 at the moment of this writing).
-## Seamless mode is the default in development mode
+### Seamless mode is the default in development mode
Not really a breaking change, but it's worth noting it. By default, Volto 13 in
development mode uses the internal proxy in seamless mode otherwise configured
differently. To learn more about the seamless mode read: {doc}`../deploying/seamless-mode`
and {doc}`../configuration/zero-config-builds`.
-## Refactored Listing block using schemas and ObjectWidget
+### Refactored Listing block using schemas and ObjectWidget
The Listing block has been heavily refactored using schema forms and `BlockDataForm`
as well as the other new internal artifacts to leverage blocks variations and extensions at the same time
@@ -766,7 +908,7 @@ The advantage of this is that now you can use the `QuerystringWidget` with schem
data forms in a reusable way in your custom blocks. See the Listing block code for
further references.
-### Migrate your existing listing blocks
+#### Migrate your existing listing blocks
**(Updated: 2021/06/12)** If you have an existing Volto installation and you are using
listing blocks, you must run an upgrade step in order to match the new listing
@@ -849,7 +991,7 @@ When an official integration package exists, these upgrade steps in the backend
will be provided in there.
```
-### Update your custom variations (templates) in your project listing blocks
+#### Update your custom variations (templates) in your project listing blocks
In the case that you have custom templates for your listing blocks in your projects, it's required that you update the definitions to match the new core variations syntax.
@@ -879,7 +1021,7 @@ To this:
]
```
-## Control panel icons are now SVG based instead of font based
+### Control panel icons are now SVG based instead of font based
It was long due, the control panel overview route `/controlpanel` is now using SVG icons
from the Pastanaga icon set, instead of the deprecated font ones. If you have customized
@@ -893,13 +1035,13 @@ import config from '@plone/volto/registry'
config.settings.controlPanelsIcons.mynewcontrolpanelid = myfancyiconSVG;
```
-## Login form UI and accessibility updated
+### Login form UI and accessibility updated
Not really a breaking change, but it's worth to note that we changed the look and feel of
the login form and improved its usability and accessibility. Another move towards the new
Quanta look and feel.
-## Changes in the Table block feature set and messages
+### Changes in the Table block feature set and messages
The "inverted" option in Table Block was removed since it was useless with the current
CSS set. Better naming of options and labels in table block (English). Updating the i18n
@@ -923,7 +1065,7 @@ dependency" problems in Volto, due to the very nature of the solution (importing
us. In fact, circular dependencies are common in NodeJS world, and the very nature of
how it works make them "workable" thanks to the NodeJS own import resolution algorithm.
So the "build" always works, although we have the circular dependencies, but that leads to weird problems
-like (just to mention one of them) the HMR (Hot Module Reloader) not working properly.
+like (just to mention one of them) the {term}`hot module replacement` (HMR) not working properly.
That's why in this version we are introducing the new Volto's Configuration Registry.
It's a centralized singleton that is populated from the core config module and can be
@@ -1357,7 +1499,7 @@ compiling. Migrate your code or if you want to use the proposal anyways, you'll
provide the configuration to your own project (babel.config.js) in your project root
folder.
-You might still be using the old-style connecting of your components to the Redux store using
+You might still be using the old-style connecting of your components to the {term}`Redux` store using
`@connect` decorator, in that case, take a look at any connected component in Volto to
have a glimpse on how to migrate the code.
@@ -1366,7 +1508,7 @@ you are good to go, and you don't have to do anything.
### Hoisting problems on some setups
-Some people were experimenting weird hoisting issues when installing dependencies. This
+Some people were experimenting weird {term}`hoisting` issues when installing dependencies. This
was caused by Babel deprecated proposals packages and its peer dependencies that
sometimes conflicted with other installed packages.
@@ -2040,7 +2182,9 @@ The blocks engine now takes care of the keyboard navigation of the blocks, so yo
The focus management is also transferred to the engine, so it's not needed for your block to manage the focus. However, if your block does indeed require to manage its own focus, then you should mark it with the `blockHasOwnFocusManagement` property in the blocks configuration object:
-``` js hl_lines="10"
+```{code-block} jsx
+:linenos:
+:emphasize-lines: 10
text: {
id: 'text',
title: 'Text',
diff --git a/docs/source/user-manual/blocks.md b/docs/source/user-manual/blocks.md
new file mode 100644
index 0000000000..9e4c744d4d
--- /dev/null
+++ b/docs/source/user-manual/blocks.md
@@ -0,0 +1,604 @@
+---
+myst:
+ html_meta:
+ "description": "User manual for how to edit blocks in Volto, the Plone 6 frontend."
+ "property=og:description": "User manual for how to edit blocks in Volto, the Plone 6 frontend."
+ "property=og:title": "How to edit content using Volto blocks"
+ "keywords": "Volto, Plone, frontend, React, User manual, edit blocks"
+---
+
+(edit-content-using-blocks-label)=
+
+# Edit content using blocks
+
+Volto features the [Pastanaga UI](https://github.com/plone/pastanaga), allowing you to visually compose a page using blocks.
+The blocks editor allows you to add, modify, reorder, and delete blocks given your requirements.
+Blocks provide the user the ability to display content in a specific way, although they can also define behavior and have specific features.
+
+
+(manage-blocks-label)=
+
+## Manage blocks
+
+In Volto, "blocks" are individual pieces of content that can be added to a page or other content area.
+These blocks can be used to add different types of content—such as text, images, or multimedia—and can be arranged and customized to create a wide range of different layouts.
+
+Blocks are a key feature of Volto, and are designed to make it easy for users to add and manage content on their website.
+They are created using React components, which are modular pieces of code that can be easily reused and customized.
+
+
+(create-a-block-label)=
+
+### Create a block
+
+To create or add an empty block after an existing block, click in the block, then hit the {kbd}`Enter` key.
+A new empty block appears.
+
+```{image} ../_static/user-manual/blocks/add-new-block.gif
+:alt: Add new block
+```
+
+```{note}
+There is a new experimental feature that places a `+` below a block when it is active or moused over, and when clicked inserts an empty block below the current block.
+
+See https://github.com/plone/volto/pull/3815 for details of the feature and how to enable it.
+```
+
+(configure-a-block-label)=
+
+### Configure a block
+
+When you select a block, its block editor appears in the right margin of the page.
+Almost all blocks have some configuration options.
+
+
+(rearrange-blocks-label)=
+
+### Rearrange blocks
+
+To rearrange blocks, to the right of the block you want to move, click on its drag handle, move the block where you want it in the page, and release the drag handle.
+
+
+(delete-a-block-label)=
+
+### Delete a block
+
+To delete a block, to the right of the block, click its delete button, a trash can icon.
+
+
+(default-block-types-label)=
+
+## Default block types
+
+Volto offers several default block types out of the box.
+You can access and choose a block type to add to your content type when you have an empty block in it.
+
+Now with your empty block available, you can select its type in one of two ways.
+
+1. Click the `+` button to the left of the empty block.
+
+ ```{image} ../_static/user-manual/blocks/block-left-add-icon.png
+ :alt: Add block button
+ ```
+
+2. Type `/` inside the empty block to open the block types menu.
+ You can type a few letters to filter available block types.
+ You can use the up and down arrow keys to navigate within the list of block types.
+ To select the block type, you can click or tap on it, or use the {kbd}`Enter` key.
+
+ ```{image} ../_static/user-manual/blocks/block-types-menu.png
+ :alt: Block types menu
+ ```
+
+
+(user-manual-description-block-label)=
+
+### Description block
+
+A description block accepts plain text.
+When displayed, it appears as the description in the page, and for search engine optimization in HTML meta tags as `` and ``.
+
+
+(user-manual-grid-block-label)=
+
+### Grid block
+
+A grid block creates a single row of columns in a grid, which can be used to display content in a structured, organized way.
+You can select the number of columns to insert.
+
+```{image} ../_static/user-manual/blocks/grid-block-number-of-columns.png
+:alt: Choose the number of columns to insert in a grid block.
+```
+
+After choosing the number of columns to insert in a grid block, you can manage the columns.
+
+```{image} ../_static/user-manual/blocks/grid-block-manage-blocks.png
+:alt: Add a specific block type into a grid block's column
+```
+
+- Specify the block type in a column by clicking its `+` button.
+- Rearrange the order of columns in the grid block by dragging and dropping them.
+- Add a column to the grid block by clicking the `+` button above and to the left of it.
+- Remove a column from a grid block by clicking its `×` button.
+
+
+(user-manual-html-block-label)=
+
+### HTML block
+
+An HTML block allows users to add custom HTML code to a page.
+This can be useful for adding custom functionality or styling to a page, or for integrating with external services or applications.
+For example, you can insert an HTML snippet or widget from a third party service to embed a calendar, payment or donation button, or social media into a page.
+
+```{image} ../_static/user-manual/blocks/html-block.png
+:alt: HTML block
+```
+
+To use an HTML block, you need to have some knowledge of how to write HTML, unless you are provided an HTML code snippet from a third party that you can copy and paste into the block.
+
+
+(user-manual-hero-block-label)=
+
+### Hero block
+
+A hero block creates a full-width banner or header for a page.
+It is typically used to highlight important content or to create a visual impact at the top of a page.
+
+```{image} ../_static/user-manual/blocks/hero-block.png
+:alt: Hero block
+```
+
+Hero blocks typically include a background image or color.
+They can also include a title, description, and links to other pages in your site.
+
+You can use the block editor to configure its options.
+You can set the background image or color, its title and description, and links.
+For links, you can enter an external URL or select a page in your site by clicking the list icon, and give the link a title.
+
+
+(user-manual-image-block-label)=
+
+### Image block
+
+An image block lets a user insert an image into a page and configure its attributes.
+
+```{image} ../_static/user-manual/blocks/image-block.png
+:alt: Image block
+```
+
+After inserting an image block, an image must be specified by any of the following methods.
+- Choose an existing image in the site by clicking the block's list icon.
+- Upload a new image by either clicking the block's upload icon or drag and drop.
+- Enter a remote image's URL in the block's text area.
+ Click the arrow icon to save the URL.
+
+Once you have specified an image, its configurable options become available.
+```{image} ../_static/user-manual/blocks/image-block-configuration-options.png
+:alt: Image block configuration options
+```
+
+Source
+: The path or URL to the image.
+
+Alt text
+: Alternative text (alt text) is used by screen readers and search engines to describe the image.
+ Alt text should not be used for decorative images, as it adds noise to the screen reader.
+
+Alignment
+: Options for alignment include left, right, center, and full width.
+
+Image size
+: The image size determines its relative display width, either small, medium, or large.
+
+Link to
+: You can enter a URL in the text field, or click the list icon and choose a page in your website, as the target for a link.
+ You can optionally have the link open in a new tab when the user clicks it by checking the checkbox {guilabel}`Open in a new tab`.
+
+
+(user-manual-images-grid-block-label)=
+
+### Images grid block
+
+An images grid block displays a row of images on a page.
+It is typically used to showcase a collection of images in a visually appealing way.
+It can be configured to display the images in different layouts and styles.
+
+```{image} ../_static/user-manual/blocks/images-grid-block-number-of-columns.png
+:alt: Choose the number of images to insert in an images grid block.
+```
+
+After choosing the number of images to insert in an images grid block, you can configure the images exactly as you would configure a single image in an image block.
+
+```{image} ../_static/user-manual/blocks/image-block-configuration-options.png
+:alt: Image block configuration options
+```
+
+Source
+: The path or URL to the image.
+
+Alt text
+: Alternative text (alt text) is used by screen readers and search engines to describe the image.
+ Alt text should not be used for decorative images, as it adds noise to the screen reader.
+
+Alignment
+: Options for alignment include left, right, center, and full width.
+
+Image size
+: The image size determines its relative display width, either small, medium, or large.
+
+Link to
+: You can enter a URL in the text field, or click the list icon and choose a page in your website, as the target for a link.
+ You can optionally have the link open in a new tab when the user clicks it by checking the checkbox {guilabel}`Open in a new tab`.
+
+You can also manage the images in the images grid block.
+
+```{image} ../_static/user-manual/blocks/images-grid-block-manage-images.png
+:alt: Manage images in an images gride block
+```
+
+- Rearrange the order of images in the images grid block by dragging and dropping them.
+- Add an image to the images grid block by clicking the `+` button above and to the left of it.
+- Remove an image from an images grid block by clicking its `×` button.
+
+
+After inserting an image grid block, an image must be specified by any of the following methods.
+
+
+(user-manual-listing-block-label)=
+
+### Listing block
+
+A listing block allows users to display a list of content items in your Plone site on a page.
+A site editor can configure the criteria to use for retrieving content items, including text, title, dates, and creator.
+The retrieved results can be configured with a sort order, limit of results, and whether to batch the results with pagination.
+
+```{image} ../_static/user-manual/blocks/listing-block.png
+:alt: Listing block
+```
+
+The listing block has several configuration options.
+
+```{image} ../_static/user-manual/blocks/listing-block-configuration.png
+:alt: Listing block configuration
+```
+
+Variation
+: Options for variation include {guilabel}`Default`, {guilabel}`Image gallery`, and {guilabel}`Summary`.
+
+Headline
+: Optionally add a headline to the listing block.
+
+Headline level
+: Headline level sets the level of the headline to either {guilabel}`H2` or {guilabel}`H3`.
+
+Criteria
+: Add criteria for the search.
+ Options include searching metadata, dates, and text.
+ Each criterion has its own options.
+ For example, you can configure a search for content that was created between two dates, or for its location within a path of your Plone site.
+
+Sort on
+: Sort the retrieved results by a given option.
+ Options include metadata, dates, and text.
+
+Results limit
+: Limit the number of results returned.
+
+Item batch size
+: Batch the search result items into a specified batch size.
+
+
+(user-manual-maps-block-label)=
+
+### Maps block
+
+A map block allows a user to add a map to a page.
+It is typically used to display a geographic location or region, or provide travel directions.
+
+```{image} ../_static/user-manual/blocks/maps-block.png
+:alt: Maps block
+```
+
+To use a map block, the third party map service must provide a snippet of HTML code that you can copy and paste into the map block.
+Usually the snippet includes an `