diff --git a/docs/source/user-manual/autosave.md b/docs/source/user-manual/autosave.md new file mode 100644 index 0000000000..278a0992a9 --- /dev/null +++ b/docs/source/user-manual/autosave.md @@ -0,0 +1,58 @@ +--- +myst: + html_meta: + "description": "User manual for how Volto autosaves data in Plone 6." + "property=og:description": "User manual for how Volto autosaves data in Plone 6." + "property=og:title": "How to autosave content in Volto when editing, adding, or commenting on content." + "keywords": "Volto, Plone, frontend, React, User manual, autosave, restore" +--- + +(autosave-content-label)= + +# Autosave content + +The autosave feature allows you to restore locally saved data, in case of accidental browser close, refresh, quit, or change page. +It uses the [`localStorage` property](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +It clears the data when either the form is saved or you cancel restoring the local data. +If local data is found for the specific content, a toast is shown that allows you to either restore ({guilabel}`OK`) or discard ({guilabel}`Cancel`) it. +If local data is older than data on the server, it will still show the toast, but it will specify that the found local data is older than the server data. + + +(autosave-edit-mode-label)= + +## Autosave edit mode + +A local copy of the form is saved in `localStorage` when you start to edit, not when you merely open the page in edit mode. +Changing the form will update the `localStorage` with a new complete copy of the form. +In case you close the tab, quit, refresh, change the page, or cancel editing, when you revisit the page in edit mode, it will display a toast for the found data. +Data is saved with a unique id: + +```js + const id = isEditForm + ? ['form', type, pathname].join('-') // edit + : type + ? ['form', pathname, type].join('-') // add + : schema?.properties?.comment + ? ['form', pathname, 'comment'].join('-') // comments + : ['form', pathname].join('-'); +``` + +Local data for the current content will be deleted, when you save the form or choose {guilabel}`Cancel` from the toast. + + +(autosave-new-content-label)= + +## Autosave new content + +When adding content, a copy of the form will be saved in `localStorage`, similar to edit mode. +But since the content hasn't been saved yet, we don't have an ID. +In this case the content type will be used. +Since it also uses the path to create the ID, the local data will be restored if you exit without saving, and only if you add the same content in the same path. + + +(autosave-comments-label)= + +## Autosave comments + +Comments are also saved locally, even though you are not in edit or add mode. +After restoring local data, if a comment is submitted, it will be deleted from `localStorage`. diff --git a/docs/source/user-manual/index.md b/docs/source/user-manual/index.md index 78e5317514..999e1a85eb 100644 --- a/docs/source/user-manual/index.md +++ b/docs/source/user-manual/index.md @@ -31,5 +31,6 @@ Note that the audience for these sources may be a Developer, not an Editor, and blocks copy-paste-blocks +autosave links-to-item ``` diff --git a/news/4168.feature b/news/4168.feature new file mode 100644 index 0000000000..e300fa6602 --- /dev/null +++ b/news/4168.feature @@ -0,0 +1,3 @@ +Add Auto-Save option. It will save a copy of the form, even comments even for adding a new content item. It detects if local data is less recent than server data. +Saving the form will delete the local data for the specific form. +User can choose to not restore the local data and this will also delete the local data. @tiberiuichim @rexalex @stevepiercy diff --git a/packages/volto/cypress/tests/core/basic/autosave.js b/packages/volto/cypress/tests/core/basic/autosave.js new file mode 100644 index 0000000000..ecba88512f --- /dev/null +++ b/packages/volto/cypress/tests/core/basic/autosave.js @@ -0,0 +1,225 @@ +describe('createContent Tests', () => { + beforeEach(() => { + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'comments-page', + contentTitle: 'Comments Page', + allow_discussion: true, + }); + cy.setRegistry( + 'plone.app.discussion.interfaces.IDiscussionSettings.globally_enabled', + true, + ); + cy.createContent({ + contentType: 'Document', + contentId: 'my-first-page', + contentTitle: 'My First Page', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-second-page', + contentTitle: 'My Second Page', + }); + }); + + it('As editor I can autosave when editing a content item', () => { + cy.visit('/my-first-page'); + + cy.log('adding a text block on the first page'); + + cy.navigate('/my-first-page/edit'); + cy.getSlateEditorAndType('My first text').contains('My first text'); + cy.wait(1000); + + cy.visit('/my-second-page'); + + cy.log('adding a text block on the second page'); + cy.navigate('/my-second-page/edit'); + + cy.getSlateEditorAndType('My second text').contains('My second text'); + cy.wait(1000); + + cy.log('visit first page and start editing'); + + cy.visit('/my-first-page'); + cy.navigate('/my-first-page/edit'); + cy.wait(1000); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.wait(1000); + + cy.getSlate().contains('My first text'); + + cy.log('visit second page and start editing'); + + cy.visit('/my-second-page'); + cy.navigate('/my-second-page/edit'); + cy.wait(1000); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.wait(1000); + + cy.getSlate().contains('My second text'); + cy.reload(); + + cy.log( + 'test is cancel load data will delete from storage (toast does not show)', + ); + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(1) + .click(); + + cy.wait(1000); + cy.reload(); + + cy.wait(1000); + + cy.contains('Autosaved content found').should('not.exist'); + }); + + it('As editor I can autosave when adding a content item', function () { + cy.visit('/'); + + cy.log( + 'adding a Document content type and refresh to verify if content is autosaved and retrieved', + ); + cy.get('#toolbar-add').click().get('#toolbar-add-document').click(); + cy.getSlateTitle().type('Page 1 title'); + cy.getSlateEditorAndType('Page 1 content').contains('Page 1 content'); + cy.wait(1000); + cy.reload(); + + cy.log('test if autosaved toast shows retrieved data and click OK to load'); + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.wait(1000); + + cy.log('test if autosaved data is loaded'); + cy.getSlateTitle().contains('Page 1 title'); + cy.getSlate().contains('Page 1 content'); + + cy.log( + 'test if draft is autosaved after I cancel adding a new page content type', + ); + + cy.get('button.button.cancel').click(); + + cy.wait(1000); + + cy.get('#toolbar-add').click().get('#toolbar-add-document').click(); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.wait(1000); + cy.getSlateTitle().contains('Page 1 title'); + cy.getSlate().contains('Page 1 content'); + + cy.wait(1000); + + cy.log('test if page content type is added as new page after Toolbar Save'); + + cy.get('#toolbar-save').focus().click(); + cy.wait(2000); + cy.contains('Page 1 title'); + cy.wait(1000); + + cy.log('test draft is deleted from local storage after save'); + + cy.visit('/'); + cy.get('#toolbar-add').click().get('#toolbar-add-document').click(); + + cy.wait(1000); + + cy.contains('Autosaved content found').should('not.exist'); + }); + + it('As editor I can autosave comments', function () { + cy.log('adding a comment and refresh,'); + cy.visit('/comments-page'); + cy.get('textarea[id="field-comment"]').clear().type('This is a comment'); + cy.wait(1000); + cy.reload(); + + cy.log('test if comment is retrieved from local storage'); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.get('#field-comment').contains('This is a comment'); + + cy.wait(1000); + cy.reload(); + + cy.log( + 'test if comment is deleted from local storage after selecting Cancel in the Autosave toast', + ); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(1) + .click(); + + cy.wait(1000); + cy.reload(); + cy.contains('Autosaved content found').should('not.exist'); + + cy.log('adding another comment and save it'); + + cy.get('textarea[id="field-comment"]') + .clear() + .type('This is a another comment'); + cy.wait(1000); + cy.reload(); + + cy.findByRole('alert') + .get('.toast-inner-content') + .contains('Autosaved content found') + .get('button.ui.icon.button.save.toast-box') + .eq(0) + .click(); + + cy.wait(1000); + + cy.get('button[type="submit"').click(); + + cy.get('.comment').contains('This is a another comment'); + + cy.log('test if the local storage comment was deleted after submit'); + + cy.wait(1000); + cy.reload(); + cy.contains('Autosaved content found').should('not.exist'); + }); +}); diff --git a/packages/volto/cypress/tests/core/blocks/blocks-grid.js b/packages/volto/cypress/tests/core/blocks/blocks-grid.js index 94b2fb56f3..d7973ab0af 100644 --- a/packages/volto/cypress/tests/core/blocks/blocks-grid.js +++ b/packages/volto/cypress/tests/core/blocks/blocks-grid.js @@ -47,7 +47,7 @@ context('Blocks Acceptance Tests', () => { cy.get('button[aria-label="Add block in position 1"]').click(); cy.get('.blocks-chooser [aria-label="Unfold Text blocks"]').click(); cy.wait(200); - cy.get('.blocks-chooser .text .button.slate').click(); + cy.get('.blocks-chooser .text .button.slate').click({ force: true }); cy.getSlateEditorSelectorAndType( '.block.gridBlock.selected .slate-editor [contenteditable=true]', 'Colorless green ideas sleep furiously.', @@ -90,7 +90,7 @@ context('Blocks Acceptance Tests', () => { cy.get('button[aria-label="Add block in position 1"]').click(); cy.get('.blocks-chooser [aria-label="Unfold Text blocks"]').click(); cy.wait(200); - cy.get('.blocks-chooser .text .button.slate').click(); + cy.get('.blocks-chooser .text .button.slate').click({ force: true }); cy.scrollTo('top'); cy.getSlateEditorSelectorAndType( diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index 124ebbcd01..f719de23e7 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -418,6 +418,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1109,6 +1114,11 @@ msgstr "Realment voleu suprimir l'usuari {username}?" msgid "Do you really want to delete this item?" msgstr "Realment voleu suprimir aquest element?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3636,6 +3646,11 @@ msgstr "El procés de registre ha estat satisfactori. Si us plau, comproveu la v msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 6011f4fed2..a985b8b222 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -417,6 +417,11 @@ msgstr "" msgid "Assignments" msgstr "Zuweisungen" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1108,6 +1113,11 @@ msgstr "Möchten Sie den Nutzer {username} wirklich löschen?" msgid "Do you really want to delete this item?" msgstr "Möchten Sie den Artikel wirklich löschen?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3635,6 +3645,11 @@ msgstr "Bitte prüfen Sie Ihr E-Mail Postfach. Sie sollten eine E-Mail erhalten msgid "The site configuration is outdated and needs to be upgraded." msgstr "Die Seitenkonfiguration ist veraltet und muss aktualisiert werden." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 19b258fecb..7e4097c496 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -412,6 +412,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1103,6 +1108,11 @@ msgstr "" msgid "Do you really want to delete this item?" msgstr "" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3630,6 +3640,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index f9f065a686..ba8a9881da 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -419,6 +419,11 @@ msgstr "" msgid "Assignments" msgstr "Tareas" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1110,6 +1115,11 @@ msgstr "¿Esta seguro que quiere eliminar el usuario {username}?" msgid "Do you really want to delete this item?" msgstr "¿Usted realmente quiere eliminar este elemento?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3637,6 +3647,11 @@ msgstr "El registro fue exitoso. Por favor, verifique su bandeja de entrada para msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configuración del sitio está anticuada y debe ser actualizada." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index 7064a7b56c..401fcd1431 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -419,6 +419,11 @@ msgstr "" msgid "Assignments" msgstr "Esleipenak" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1110,6 +1115,11 @@ msgstr "{username} erabiltzailea ezabatu egin nahi duzu?" msgid "Do you really want to delete this item?" msgstr "Elementu hau ezabatu egin nahi duzu?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3637,6 +3647,11 @@ msgstr "Izen-emate prozesua ondo egin duzu. Begiratu zure eposta, kontua aktibat msgid "The site configuration is outdated and needs to be upgraded." msgstr "Atariaren konfigurazioa zaharkituta dago eta eguneratu egin behar da." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index 03b2780a8c..275aa18832 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -417,6 +417,11 @@ msgstr "" msgid "Assignments" msgstr "Tehtävät" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1108,6 +1113,11 @@ msgstr "Haluatko varmasti poistaa käyttäjän {username}?" msgid "Do you really want to delete this item?" msgstr "Haluatko varmasti poistaa tämän sisällön?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3635,6 +3645,11 @@ msgstr "Rekisteröinti onnistui. Tarkista, saitko sähköpostiisi ohjeet käytt msgid "The site configuration is outdated and needs to be upgraded." msgstr "Sivuston konfiguraatio on vanhentunut ja se pitää päivittää." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 79c1ee9372..7716ff35e0 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -419,6 +419,11 @@ msgstr "" msgid "Assignments" msgstr "Affectations" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1110,6 +1115,11 @@ msgstr "Voulez-vous vraiment supprimer l'utilisateur {username} ?" msgid "Do you really want to delete this item?" msgstr "Voulez-vous vraiment supprimer cet élément ?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3637,6 +3647,11 @@ msgstr "Le processus d'inscription a réussi. Veuillez vérifier votre boîte e- msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configuration du site nécessite une mise à niveau." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index ee27a0e94c..72eaa4daea 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -412,6 +412,11 @@ msgstr "Assegnare il ruolo di {role} a {entry}" msgid "Assignments" msgstr "Assegnazione" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1103,6 +1108,11 @@ msgstr "Vuoi veramente eliminare l'utente {username}?" msgid "Do you really want to delete this item?" msgstr "Vuoi veramente eliminare questo elemento?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3630,6 +3640,11 @@ msgstr "La registrazione è avvenuta correttamente. Per favore controlla la tua msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configurazione del sito è obsoleta e deve essere aggiornata." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 23c0849a3c..46e64bef1f 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -417,6 +417,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1108,6 +1113,11 @@ msgstr "ユーザ {username} を削除してよろしいですか?" msgid "Do you really want to delete this item?" msgstr "このアイテムを削除してよろしいですか?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3635,6 +3645,11 @@ msgstr "The registration process has been successful. Please check your e-mail i msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index fe21ea8d91..e1ea88716b 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -416,6 +416,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1107,6 +1112,11 @@ msgstr "" msgid "Do you really want to delete this item?" msgstr "Weet u zeker dat u dit item wilt verwijderen?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3634,6 +3644,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index c341c5f237..beb4d42c94 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -417,6 +417,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1108,6 +1113,11 @@ msgstr "Quer mesmo eliminar o utilizador {username}?" msgid "Do you really want to delete this item?" msgstr "Quer mesmo eliminar este item?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3635,6 +3645,11 @@ msgstr "O processo de registo foi bem sucedido. Por favor verifique no seu e-mai msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index 663e5e5ab4..9bc93c8948 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -418,6 +418,11 @@ msgstr "Atribuir o papel de {role} à {entry}" msgid "Assignments" msgstr "Atribuições" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1109,6 +1114,11 @@ msgstr "Você realmente quer excluir o usuário {username}?" msgid "Do you really want to delete this item?" msgstr "Você realmente quer excluir este item?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3636,6 +3646,11 @@ msgstr "O processo de registro foi bem sucedido. Verifique sua caixa de entrada msgid "The site configuration is outdated and needs to be upgraded." msgstr "A configuração do site está desatualizada e precisa ser atualizada." +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index 2c75e1aa99..7c555cf985 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -412,6 +412,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1103,6 +1108,11 @@ msgstr "Doriți cu adevărat să ștergeți utilizatorul {username}?" msgid "Do you really want to delete this item?" msgstr "Doriți cu adevărat să ștergeți acest articol?" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3630,6 +3640,11 @@ msgstr "Procesul de înregistrare a avut succes. Vă rugăm să verificați căs msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index 414d63d872..e81678f9f4 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-02-26T09:23:35.781Z\n" +"POT-Creation-Date: 2024-03-02T17:43:06.261Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -414,6 +414,11 @@ msgstr "" msgid "Assignments" msgstr "" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1105,6 +1110,11 @@ msgstr "" msgid "Do you really want to delete this item?" msgstr "" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3632,6 +3642,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index 105ead2efc..7955b106c6 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -418,6 +418,11 @@ msgstr "" msgid "Assignments" msgstr "分配" +#. Default: "Autosaved content found" +#: helpers/Utils/withSaveAsDraft +msgid "Autosaved content found" +msgstr "" + #. Default: "Available" #: components/manage/Controlpanels/AddonsControlpanel msgid "Available" @@ -1109,6 +1114,11 @@ msgstr "确定要删除这个用户 {username}吗?" msgid "Do you really want to delete this item?" msgstr "确定要删除这个条目吗" +#. Default: "Do you want to restore the autosaved content?" +#: helpers/Utils/withSaveAsDraft +msgid "Do you want to restore the autosaved content?" +msgstr "" + #. Default: "Document" #: components/manage/Multilingual/TranslationObject #: components/manage/Sidebar/Sidebar @@ -3636,6 +3646,11 @@ msgstr "注册过程成功完成。请在您的电子邮箱中查看有关如何 msgid "The site configuration is outdated and needs to be upgraded." msgstr "网站配置已过时,需要升级。" +#. Default: "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +#: helpers/Utils/withSaveAsDraft +msgid "The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/package.json b/packages/volto/package.json index 0b1d9e518b..a3a8bfca5d 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -42,7 +42,7 @@ "build:types": "tsc --project tsconfig.declarations.json", "test": "razzle test --maxWorkers=50%", "test:ci": "CI=true NODE_ICU_DATA=node_modules/full-icu razzle test", - "test:husky": "CI=true yarn test --bail --findRelatedTests", + "test:husky": "CI=true pnpm test --bail --findRelatedTests", "test:debug": "node --inspect node_modules/.bin/jest --runInBand", "start:prod": "NODE_ENV=production node build/server.js", "prettier": "./node_modules/.bin/prettier --single-quote --check '{src,cypress}/**/*.{js,jsx,ts,tsx}' --check '*.js'", @@ -50,7 +50,7 @@ "prettier:husky": "prettier --single-quote --write", "stylelint": "./node_modules/.bin/stylelint 'theme/**/*.{css,less}' 'src/**/*.{css,less}'", "stylelint:overrides": "./node_modules/.bin/stylelint 'theme/**/*.overrides' 'src/**/*.overrides'", - "stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix", + "stylelint:fix": "pnpm stylelint --fix && pnpm stylelint:overrides --fix", "lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,ts,tsx,json}'", "lint:fix": "./node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx,ts,tsx,json}'", "lint:husky": "eslint --max-warnings=0 --fix", diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 40b8ae599a..c3c65046bb 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -15,6 +15,7 @@ import aheadSVG from '@plone/volto/icons/ahead.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import upSVG from '@plone/volto/icons/up-key.svg'; import downSVG from '@plone/volto/icons/down-key.svg'; +import withSaveAsDraft from '@plone/volto/helpers/Utils/withSaveAsDraft'; import { findIndex, isEmpty, @@ -248,6 +249,18 @@ class Form extends Component { this.onBlurField = this.onBlurField.bind(this); this.onClickInput = this.onClickInput.bind(this); this.onToggleMetadataFieldset = this.onToggleMetadataFieldset.bind(this); + this.updateFormDataWithSaved = this.updateFormDataWithSaved.bind(this); + } + + /** + * Function sent as callback to saveAsDraft when user + * choses to load local data + * @param {Object} savedFormData + */ + updateFormDataWithSaved(savedFormData) { + if (savedFormData) { + this.setState({ formData: savedFormData }); + } } /** @@ -257,6 +270,13 @@ class Form extends Component { * @param {Object} prevProps */ async componentDidUpdate(prevProps, prevState) { + // schema was just received async and plugged as prop + if (!prevProps.schema && this.props.schema) { + this.props.checkSavedDraft( + this.state.formData, + this.updateFormDataWithSaved, + ); + } let { requestError } = this.props; let errors = {}; let activeIndex = 0; @@ -280,6 +300,12 @@ class Form extends Component { this.props.onChangeFormData(this.state.formData); } } + + // on each formData update it will save the form to the localStorage + if (!isEqual(prevState?.formData, this.state.formData)) { + this.props.onSaveDraft(this.state.formData); + } + if ( this.props.global && !isEqual(this.props.globalData, prevProps.globalData) @@ -365,6 +391,9 @@ class Form extends Component { } } + // !! componentDidMount is called twice for Add + // setState passed through callback (updateFormDataWithSaved) is ignored for the first call + // only for the second call it will execute the setState /** * Component did mount * @method componentDidMount @@ -372,6 +401,15 @@ class Form extends Component { */ componentDidMount() { this.setState({ isClient: true }); + + // schema already exists in redux store + if (this.props.schema) { + this.props.checkSavedDraft( + this.state.formData, + this.updateFormDataWithSaved, + ); + return; + } } static getDerivedStateFromProps(props, state) { @@ -548,6 +586,8 @@ class Form extends Component { } else { // Get only the values that have been modified (Edit forms), send all in case that // it's an add form + this.props.onCancelDraft(); + if (this.props.isEditForm) { this.props.onSubmit(this.getOnlyFormModifiedValues()); } else { @@ -970,4 +1010,5 @@ export default compose( null, { forwardRef: true }, ), + withSaveAsDraft({ forwardRef: true }), )(FormIntl); diff --git a/packages/volto/src/components/manage/Form/Form.test.jsx b/packages/volto/src/components/manage/Form/Form.test.jsx index 1ac45f0e4d..8be705962a 100644 --- a/packages/volto/src/components/manage/Form/Form.test.jsx +++ b/packages/volto/src/components/manage/Form/Form.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; +import { MemoryRouter } from 'react-router-dom'; import Form from './Form'; @@ -26,26 +27,30 @@ describe('Form', () => { }, }, }); + const route = '/some-route'; const component = renderer.create( -
+ {}} - onCancel={() => {}} - /> + required: [], + }} + requestError={errorMessage} + onSubmit={() => {}} + onCancel={() => {}} + /> + + , , ); const json = component.toJSON(); diff --git a/packages/volto/src/components/manage/Form/__snapshots__/Form.test.jsx.snap b/packages/volto/src/components/manage/Form/__snapshots__/Form.test.jsx.snap index 1cf0e7b027..47ab796dd8 100644 --- a/packages/volto/src/components/manage/Form/__snapshots__/Form.test.jsx.snap +++ b/packages/volto/src/components/manage/Form/__snapshots__/Form.test.jsx.snap @@ -1,84 +1,87 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Form renders a form component 1`] = ` -
- -
-
-
-
- - + viewBox="" + xmlns="" + /> + + +
-
-
- -
+ + + , + ",", +] `; diff --git a/packages/volto/src/components/theme/Comments/Comments.test.jsx b/packages/volto/src/components/theme/Comments/Comments.test.jsx index 1ea8663e7d..0b486c8754 100644 --- a/packages/volto/src/components/theme/Comments/Comments.test.jsx +++ b/packages/volto/src/components/theme/Comments/Comments.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; +import { MemoryRouter } from 'react-router-dom'; import Comments from './Comments'; @@ -65,9 +66,12 @@ describe('Comments', () => { }, }, }); + const route = '/some-route'; const component = renderer.create( - + + + , ); const json = component.toJSON(); diff --git a/packages/volto/src/helpers/Utils/withSaveAsDraft.js b/packages/volto/src/helpers/Utils/withSaveAsDraft.js new file mode 100644 index 0000000000..85be517231 --- /dev/null +++ b/packages/volto/src/helpers/Utils/withSaveAsDraft.js @@ -0,0 +1,240 @@ +import React from 'react'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import isEqual from 'react-fast-compare'; +import { toast } from 'react-toastify'; +import { Toast, Icon } from '@plone/volto/components'; +import { Button } from 'semantic-ui-react'; +import checkSVG from '@plone/volto/icons/check.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import { useIntl, defineMessages } from 'react-intl'; +import { useLocation } from 'react-router-dom'; + +const messages = defineMessages({ + autoSaveFound: { + id: 'Autosaved content found', + defaultMessage: 'Autosaved content found', + }, + loadData: { + id: 'Do you want to restore the autosaved content?', + defaultMessage: 'Do you want to restore the autosaved content?', + }, + loadExpiredData: { + id: 'The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)', + defaultMessage: + 'The version of the autosaved content I found in your browser is older than that stored on the server. Do you want to restore the autosaved content? (You can undo the autosaved content and revert to the server version.)', + }, +}); + +function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +const mapSchemaToData = (schema, data) => { + if (!data) return {}; + const dataKeys = Object.keys(data); + return Object.assign( + {}, + ...Object.keys(schema.properties) + .filter((k) => dataKeys.includes(k)) + .map((k) => ({ [k]: data[k] })), + ); +}; + +// will be used to avoid using the first mount call if there is a second call +let mountTime; + +const getFormId = (props, location) => { + const { type, pathname = location.pathname, isEditForm, schema } = props; + const id = isEditForm + ? ['form', type, pathname].join('-') + : type + ? ['form', pathname, type].join('-') + : schema?.properties?.comment + ? ['form', pathname, 'comment'].join('-') + : ['form', pathname].join('-'); + + return id; +}; + +/** + * Toast content that has OK and Cancel buttons + * @param {function} onUpdate + * @param {function} onClose + * @param {string} userMessage + * @returns + */ +const ConfirmAutoSave = ({ onUpdate, onClose, userMessage }) => { + const handleClickOK = () => onUpdate(); + const handleClickCancel = () => onClose(); + + return ( +
+
{userMessage}
+ + +
+ ); +}; + +/** + * Will remove localStorage item using debounce + * @param {string} id + * @param {number} timerForDeletion + */ +const clearStorage = (id, timerForDeletion) => { + timerForDeletion.current && clearTimeout(timerForDeletion.current); + timerForDeletion.current = setTimeout(() => { + localStorage.removeItem(id); + }, 500); +}; + +/** + * Stale if server date is more recent + * @param {string} serverModifiedDate + * @param {string} autoSaveDate + * @returns {Boolean} + */ +const autoSaveFoundIsStale = (serverModifiedDate, autoSaveDate) => { + const result = !serverModifiedDate + ? false + : new Date(serverModifiedDate) > new Date(autoSaveDate); + return result; +}; + +const draftApi = (id, schema, timer, timerForDeletion, intl) => ({ + // - since Add Content Type will call componentDidMount twice, we will + // use the second call (using debounce)- the first will ignore any setState comands; + // - Delete local data only if user confirms Cancel + // - Will tell user that it has local stored data, even if its less recent than the server data + checkSavedDraft(state, updateCallback) { + if (!schema) return; + const saved = localStorage.getItem(id); + + if (saved) { + const formData = mapSchemaToData(schema, state); + // includes autoSaveDate + const foundSavedData = JSON.parse(saved); + // includes only form data found in schema (no autoSaveDate) + const foundSavedSchemaData = mapSchemaToData(schema, foundSavedData); + + if (!isEqual(formData, foundSavedSchemaData)) { + // eslint-disable-next-line no-alert + // cancel existing setTimeout to avoid using first call if + // successive calls are made + mountTime && clearTimeout(mountTime); + mountTime = setTimeout(() => { + toast.info( + updateCallback(foundSavedSchemaData)} + onClose={() => clearStorage(id, timerForDeletion)} + userMessage={ + autoSaveFoundIsStale( + state.modified, + foundSavedData.autoSaveDate, + ) + ? intl.formatMessage(messages.loadExpiredData) + : intl.formatMessage(messages.loadData) + } + /> + } + />, + ); + }, 300); + } + } + }, + // use debounce mode + onSaveDraft(state) { + if (!schema) return; + timer.current && clearTimeout(timer.current); + timer.current = setTimeout(() => { + const formData = mapSchemaToData(schema, state); + const saved = localStorage.getItem(id); + const newData = JSON.parse(saved); + + localStorage.setItem( + id, + JSON.stringify({ + ...newData, + ...formData, + autoSaveDate: new Date(), + }), + ); + }, 300); + }, + + onCancelDraft() { + if (!schema) return; + clearStorage(id, timerForDeletion); + }, +}); + +export default function withSaveAsDraft(options) { + const { forwardRef } = options; + + return (WrappedComponent) => { + function WithSaveAsDraft(props) { + const { schema } = props; + const intl = useIntl(); + const location = useLocation(); + const id = getFormId(props, location); + const ref = React.useRef(); + const ref2 = React.useRef(); + const api = React.useMemo( + () => draftApi(id, schema, ref, ref2, intl), + [id, schema, ref, ref2, intl], + ); + + return ( + + ); + } + + WithSaveAsDraft.displayName = `WithSaveAsDraft(${getDisplayName( + WrappedComponent, + )})`; + + if (forwardRef) { + return hoistNonReactStatics( + React.forwardRef((props, ref) => ( + + )), + WrappedComponent, + ); + } + + return hoistNonReactStatics(WithSaveAsDraft, WrappedComponent); + }; +} diff --git a/packages/volto/theme/themes/pastanaga/elements/input.overrides b/packages/volto/theme/themes/pastanaga/elements/input.overrides index 2637218f62..ef897adb09 100644 --- a/packages/volto/theme/themes/pastanaga/elements/input.overrides +++ b/packages/volto/theme/themes/pastanaga/elements/input.overrides @@ -98,9 +98,11 @@ of an error is present, it overrides a default from SemanticUI grid definitions. &.clear-search-button { //needed for focus margin-left: 0.1em; + svg.icon { margin: auto; } + &:focus, &:hover { -webkit-box-shadow: none; diff --git a/packages/volto/theme/themes/pastanaga/extras/main.less b/packages/volto/theme/themes/pastanaga/extras/main.less index 6b1bb30d5b..8cef323e1b 100644 --- a/packages/volto/theme/themes/pastanaga/extras/main.less +++ b/packages/volto/theme/themes/pastanaga/extras/main.less @@ -389,6 +389,21 @@ button { } } +.toast-box-center { + display: flex; + align-items: center; + justify-content: center; + + .save.toast-box { + background: transparent; + color: #007eb1; + + .circled.toast-box-blue-icon { + color: #007eb1; + } + } +} + .users-control-panel .table { overflow-x: scroll; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aed91fa239..c395f78e74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 5.2.2 vitest: specifier: ^1.3.1 - version: 1.3.1 + version: 1.3.1(jsdom@21.1.2) apps/nextjs: dependencies: @@ -846,7 +846,7 @@ importers: version: 5.2.2 vitest: specifier: ^1.3.1 - version: 1.3.1 + version: 1.3.1(jsdom@21.1.2) packages/client: dependencies: @@ -1223,7 +1223,7 @@ importers: version: 5.2.2 vitest: specifier: ^1.3.1 - version: 1.3.1 + version: 1.3.1(jsdom@21.1.2) packages/parcel-optimizer-react-client: dependencies: @@ -17758,7 +17758,7 @@ packages: jest: 26.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.3.1 + vitest: 1.3.1(jsdom@21.1.2) dev: true /@testing-library/jest-dom@6.4.2(vitest@1.3.1): @@ -43503,61 +43503,6 @@ packages: - terser dev: true - /vitest@1.3.1: - resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.3.1 - '@vitest/ui': 1.3.1 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@vitest/expect': 1.3.1 - '@vitest/runner': 1.3.1 - '@vitest/snapshot': 1.3.1 - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 - acorn-walk: 8.3.2 - chai: 4.3.10 - debug: 4.3.4(supports-color@8.1.1) - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.5 - pathe: 1.1.1 - picocolors: 1.0.0 - std-env: 3.5.0 - strip-literal: 2.0.0 - tinybench: 2.5.1 - tinypool: 0.8.2 - vite: 5.1.4(@types/node@20.9.0) - vite-node: 1.3.1 - why-is-node-running: 2.2.2 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true - /vitest@1.3.1(jsdom@21.1.2): resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/styles/Vocab/Plone/accept.txt b/styles/Vocab/Plone/accept.txt index 4161b50188..dd0113c70e 100644 --- a/styles/Vocab/Plone/accept.txt +++ b/styles/Vocab/Plone/accept.txt @@ -2,6 +2,7 @@ -{0,1}volto-{0,1} `plone.restapi` `plone.volto` +[Aa]utosave [Aa]sync [Bb]ackend JavaScript