From 5ff058b5955d71d51055b7073a3809e52ed531a5 Mon Sep 17 00:00:00 2001 From: Fred van Dijk Date: Wed, 29 Jan 2025 05:22:28 +0100 Subject: [PATCH 01/19] Fix some typos in NL translations (#6531) Co-authored-by: David Glick --- packages/volto/locales/nl/LC_MESSAGES/volto.po | 18 +++++++++--------- packages/volto/news/6531.bugfix | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 packages/volto/news/6531.bugfix diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index 281135a55c..597e954b7a 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -219,7 +219,7 @@ msgstr "Gebruikers aan groep toevoegen" #. Default: "Add term" #: components/manage/Widgets/VocabularyTermsWidget msgid "Add vocabulary term" -msgstr "Woordenboekterm toevoegen" +msgstr "Term toevoegen" #. Default: "Add {type}" #: components/manage/Add/Add @@ -867,7 +867,7 @@ msgstr "Maak of wis relaties naar doel" #. Default: "Create working copy" #: components/manage/Toolbar/More msgid "Create working copy" -msgstr "Maak werkkopij" +msgstr "Maak werkkopie" #. Default: "Created after" #: components/manage/Controlpanels/Aliases @@ -2172,7 +2172,7 @@ msgstr "Afmelden" #. Default: "Made by {creator} on {date}. This is not a working copy anymore, but the main content." #: components/manage/Toolbar/More msgid "Made by {creator} on {date}. This is not a working copy anymore, but the main content." -msgstr "Gemaakt door {creator} op {date}. Dit is geen werkkopij meer, maar de inhoud zelf." +msgstr "Gemaakt door {creator} op {date}. Dit is geen werkkopie meer, maar de inhoud zelf." #. Default: "Reduce cell padding" #: config/Blocks @@ -2941,7 +2941,7 @@ msgstr "Gebruikers uit groep verwijderen" #. Default: "Remove working copy" #: components/manage/Toolbar/More msgid "Remove working copy" -msgstr "Werkkopij verwijderen" +msgstr "werkkopie verwijderen" #. Default: "Rename" #: components/manage/Actions/Actions @@ -3781,7 +3781,7 @@ msgstr "De waarde komt niet overeen met het patroon {pattern}" #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" -msgstr "De werkkopij werd weggegooid" +msgstr "De werkkopie werd weggegooid" #. Default: "The {plonecms} is {copyright} 2000-{current_year} by the {plonefoundation} and friends." #: components/theme/Footer/Footer @@ -3838,7 +3838,7 @@ msgstr "Derde" #. Default: "This has an ongoing working copy in {title}" #: components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory msgid "This has an ongoing working copy in {title}" -msgstr "Deze heeft een lopende werkkopij in {title}" +msgstr "Deze heeft een lopende werkkopie in {title}" #. Default: "This is a reserved name and can't be used" #: components/manage/Widgets/IdWidget @@ -3848,7 +3848,7 @@ msgstr "Dit is een gereserveerde naam en kan niet gebruikt worden" #. Default: "This is a working copy of {title}" #: components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory msgid "This is a working copy of {title}" -msgstr "Dit is een werkkopij van {title}" +msgstr "Dit is een werkkopie van {title}" #. Default: "This item is also a folder. By deleting it you will delete {containedItemsToDelete} {variation} inside the folder." #: components/manage/Contents/ContentsDeleteModal @@ -3868,7 +3868,7 @@ msgstr "Deze naam wordt getoond in de URL." #. Default: "This page does not seem to exist…" #: components/theme/NotFound/NotFound msgid "This page does not seem to exist…" -msgstr "Deze pagina lijt niet te bestaan…" +msgstr "Deze pagina lijkt niet te bestaan…" #. Default: "This rule is assigned to the following locations:" #: components/manage/Controlpanels/Rules/ConfigureRule @@ -4272,7 +4272,7 @@ msgstr "Bekijk deze revisie" #. Default: "View working copy" #: components/manage/Toolbar/More msgid "View working copy" -msgstr "Bekijk werkkopij" +msgstr "Bekijk werkkopie" #. Default: "View" #: components/manage/Display/Display diff --git a/packages/volto/news/6531.bugfix b/packages/volto/news/6531.bugfix new file mode 100644 index 0000000000..d3106e0e3b --- /dev/null +++ b/packages/volto/news/6531.bugfix @@ -0,0 +1 @@ +Update Dutch translations @fredvd From b9344b19f6fbbda4aa6e58c29a0e046419f06791 Mon Sep 17 00:00:00 2001 From: Giulia Ghisini <51911425+giuliaghisini@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:47:11 +0100 Subject: [PATCH 02/19] fix broken releations control panel (#6629) Co-authored-by: Steve Piercy --- packages/volto/news/6629.bugfix | 1 + .../Relations/BrokenRelations.jsx | 32 ++++--- .../Controlpanels/Relations/Relations.jsx | 94 ++++++++++--------- 3 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 packages/volto/news/6629.bugfix diff --git a/packages/volto/news/6629.bugfix b/packages/volto/news/6629.bugfix new file mode 100644 index 0000000000..6c7cc3401b --- /dev/null +++ b/packages/volto/news/6629.bugfix @@ -0,0 +1 @@ +Correctly handle when one of the items is `null` in the Relations control panel. @giuliaghisini diff --git a/packages/volto/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx b/packages/volto/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx index 869df519fe..97bf42d43e 100644 --- a/packages/volto/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx @@ -60,22 +60,26 @@ const BrokenRelations = () => { }).map((el, index) => ( - - {flattenToAppURL(el[0])} - + {el[0] && ( + + {flattenToAppURL(el[0])} + + )} - - {flattenToAppURL(el[1])} - + {el[1] && ( + + {flattenToAppURL(el[1])} + + )} ))} diff --git a/packages/volto/src/components/manage/Controlpanels/Relations/Relations.jsx b/packages/volto/src/components/manage/Controlpanels/Relations/Relations.jsx index 3fc0494eeb..5d19f8e6f9 100644 --- a/packages/volto/src/components/manage/Controlpanels/Relations/Relations.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Relations/Relations.jsx @@ -47,53 +47,55 @@ const RelationsControlPanel = () => { return ( <> -
- - {can_edit ? ( - - - {brokenRelations && Object.keys(brokenRelations).length > 0 ? ( - - - - - - ) : null} -

+
+
+ + {can_edit ? ( + + + {brokenRelations && Object.keys(brokenRelations).length > 0 ? ( + + + + + + ) : null} +

+ +

+ {relations_stats?.error ? ( + + + ) : null} +
+ + + +
+ ) : ( + + -

- {relations_stats?.error ? ( - - - ) : null} -
- - - -
- ) : ( - - - - - - )} +
{isClient && From 12c187a0da163675decd171b49ae009bd159fbf1 Mon Sep 17 00:00:00 2001 From: Wesley Barroso Lopes Date: Wed, 29 Jan 2025 08:50:35 -0300 Subject: [PATCH 03/19] Show working copy links based on backend actions (#6393) Co-authored-by: Piero Nicolli Co-authored-by: David Glick Co-authored-by: Steve Piercy --- .../configuration/settings-reference.md | 11 +- docs/source/configuration/workingcopy.md | 28 ++- packages/coresandbox/src/index.ts | 6 - packages/seven/Makefile | 2 +- packages/seven/news/6393.internal | 1 + packages/types/news/6393.bugfix | 1 + packages/types/src/config/Settings.d.ts | 1 - packages/volto/Makefile | 2 +- .../volto/cypress/tests/workingCopy/create.js | 2 +- packages/volto/news/5284.feature | 1 + .../src/components/manage/Toolbar/More.jsx | 230 +++++++++--------- .../components/manage/Toolbar/More.test.jsx | 24 -- .../Toolbar/__snapshots__/More.test.jsx.snap | 135 ---------- .../WorkingCopyToastsFactory.jsx | 109 ++++----- packages/volto/src/config/index.js | 1 - 15 files changed, 199 insertions(+), 355 deletions(-) create mode 100644 packages/seven/news/6393.internal create mode 100644 packages/types/news/6393.bugfix create mode 100644 packages/volto/news/5284.feature diff --git a/docs/source/configuration/settings-reference.md b/docs/source/configuration/settings-reference.md index b8601cffef..56272835a2 100644 --- a/docs/source/configuration/settings-reference.md +++ b/docs/source/configuration/settings-reference.md @@ -177,7 +177,16 @@ contentMetadataTagsImageField The OpenGraph image that will represent this content item, will be used in the metadata HEAD tag as og:image for SEO purposes. Defaults to image. See the OpenGraph Protocol for more details. hasWorkingCopySupport - This setting will enable working copy support in your site. You need to install the `plone.app.iterate` add-on in your Plone site in order to make it working. + ```{versionremoved} Volto 18.8.0 + This setting is unnecessary since Volto 18.8.0. + Working copy support is now based on whether the `plone.app.iterate` add-on is installed in the backend. + ``` + + For Plone sites using a Volto version prior to 18.8.0, this setting enables working copy support. + + ```{seealso} + See {doc}`workingcopy` for configuration. + ``` controlpanels Register a component as control panel. diff --git a/docs/source/configuration/workingcopy.md b/docs/source/configuration/workingcopy.md index c559729c1a..c536c62cc0 100644 --- a/docs/source/configuration/workingcopy.md +++ b/docs/source/configuration/workingcopy.md @@ -9,17 +9,9 @@ myst: # Working copy support -Volto provide support for Plone's Working Copy feature. You need to install `plone.app.iterate` add-on in your Plone site that comes available by default. You can do that in Plone's control panel or using the `GenericSetup` facility. - -## Volto configuration - -You need to enable working copy support in Volto's configuration object: - -```js -import config from '@plone/volto/registry' - -config.settings.hasWorkingCopySupport = true; -``` +Volto supports Plone's working copy feature. +To enable it, you need to install the add-on `plone.app.iterate` in your Plone site. +You can do that either in Plone's {guilabel}`Add-ons` control panel or using the `GenericSetup` facility. ## Features @@ -29,3 +21,17 @@ Volto working copy support features include: - Work on the working copy - "Check in" the working copy by applying the changes into the original (baseline) object - Cancel the working copy if required + +## Volto configuration + +```{versionremoved} Volto 18.8.0 +This setting is no longer used. +``` + +If you have an older version of Volto, you also need to enable working copy support in Volto's configuration object as follows. + +```js +import config from '@plone/volto/registry' + +config.settings.hasWorkingCopySupport = true; +``` diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index ec4979d483..6bb16df1e8 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -180,12 +180,6 @@ export const multilingualFixture = (config: ConfigType) => { return config; }; -export const workingCopyFixture = (config: ConfigType) => { - config.settings.hasWorkingCopySupport = true; - - return config; -}; - // We extend the block types with the custom ones declare module '@plone/types' { export interface BlocksConfigData { diff --git a/packages/seven/Makefile b/packages/seven/Makefile index b012ee8899..cdaeadb163 100644 --- a/packages/seven/Makefile +++ b/packages/seven/Makefile @@ -259,7 +259,7 @@ working-copy-acceptance-backend-start: ## Start backend acceptance server for wo .PHONY: working-copy-acceptance-frontend-prod-start working-copy-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for working copy tests - ADDONS=@plone/volto-coresandbox:workingCopyFixture PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod .PHONY: working-copy-acceptance-test working-copy-acceptance-test: ## Start Cypress in interactive mode for working copy tests diff --git a/packages/seven/news/6393.internal b/packages/seven/news/6393.internal new file mode 100644 index 0000000000..2ebcfed3a1 --- /dev/null +++ b/packages/seven/news/6393.internal @@ -0,0 +1 @@ +Update Makefile. @davisagli diff --git a/packages/types/news/6393.bugfix b/packages/types/news/6393.bugfix new file mode 100644 index 0000000000..fffeba7f7e --- /dev/null +++ b/packages/types/news/6393.bugfix @@ -0,0 +1 @@ +Remove `hasWorkingCopySupport` setting. @davisagli diff --git a/packages/types/src/config/Settings.d.ts b/packages/types/src/config/Settings.d.ts index 2053fde0f5..4437f3d01e 100644 --- a/packages/types/src/config/Settings.d.ts +++ b/packages/types/src/config/Settings.d.ts @@ -87,7 +87,6 @@ export interface SettingsConfig { showSelfRegistration: boolean; contentMetadataTagsImageField: string; - hasWorkingCopySupport: boolean; maxUndoLevels: number; addonsInfo: unknown; workflowMapping: unknown; diff --git a/packages/volto/Makefile b/packages/volto/Makefile index ebeeec82d5..c6f2ec636b 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -252,7 +252,7 @@ working-copy-acceptance-backend-start: ## Start backend acceptance server for wo .PHONY: working-copy-acceptance-frontend-prod-start working-copy-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for working copy tests - ADDONS=@plone/volto-coresandbox:workingCopyFixture RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod .PHONY: working-copy-acceptance-test working-copy-acceptance-test: ## Start Cypress in interactive mode for working copy tests diff --git a/packages/volto/cypress/tests/workingCopy/create.js b/packages/volto/cypress/tests/workingCopy/create.js index 55b737e07f..b76d116862 100644 --- a/packages/volto/cypress/tests/workingCopy/create.js +++ b/packages/volto/cypress/tests/workingCopy/create.js @@ -59,7 +59,7 @@ describe('Working Copy Tests - Create', () => { cy.findByText('View working copy'); }); - it('Portal root does not have create option', function () { + it('Portal root has create option', function () { cy.visit('/'); cy.get('#toolbar-more').click(); cy.get('.menu-more').contains('Create working copy').should('not.exist'); diff --git a/packages/volto/news/5284.feature b/packages/volto/news/5284.feature new file mode 100644 index 0000000000..7c5cd61759 --- /dev/null +++ b/packages/volto/news/5284.feature @@ -0,0 +1 @@ +Show the working copy actions (checkin/checkout) based on whether they are enabled in the backend, instead of the `hasWorkingCopySupport` setting. @wesleybl, @davisagli diff --git a/packages/volto/src/components/manage/Toolbar/More.jsx b/packages/volto/src/components/manage/Toolbar/More.jsx index 07d8bc7c5d..c2864026b1 100644 --- a/packages/volto/src/components/manage/Toolbar/More.jsx +++ b/packages/volto/src/components/manage/Toolbar/More.jsx @@ -191,6 +191,13 @@ const More = (props) => { id: 'redirection', }); + const workingCopyCheckoutAction = find(actions.object_buttons, { + id: 'iterate_checkout', + }); + const workingCopyCheckinAction = find(actions.object_buttons, { + id: 'iterate_checkin', + }); + const dateOptions = { year: 'numeric', month: 'long', @@ -320,125 +327,114 @@ const More = (props) => { )} - {config.settings.hasWorkingCopySupport && - content['@type'] !== 'Plone Site' && ( - <> - {!content.working_copy && ( - -
  • - -
  • -
    - )} - {content.working_copy && content.working_copy_of && ( - -
  • - +
  • +
    + )} + {workingCopyCheckinAction && ( + +
  • + -
  • -
  • - -
  • -
    - )} - {content.working_copy && !content.working_copy_of && ( - -
  • - props.closeMenu()} - > - {intl.formatMessage(messages.viewWorkingCopy)} - - -
  • -
    - )} - - )} + + + +
  • + +
  • + + )} + {content.working_copy && !content.working_copy_of && ( + +
  • + props.closeMenu()} + > + {intl.formatMessage(messages.viewWorkingCopy)} + + +
  • +
    + )} {editAction && config.settings.isMultilingual && (
  • diff --git a/packages/volto/src/components/manage/Toolbar/More.test.jsx b/packages/volto/src/components/manage/Toolbar/More.test.jsx index 7ddb508e69..1befbf1894 100644 --- a/packages/volto/src/components/manage/Toolbar/More.test.jsx +++ b/packages/volto/src/components/manage/Toolbar/More.test.jsx @@ -1,10 +1,8 @@ -import React from 'react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter } from 'react-router-dom'; import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable'; import { waitFor, render } from '@testing-library/react'; -import config from '@plone/volto/registry'; import More from './More'; @@ -162,26 +160,4 @@ describe('Toolbar More component', () => { await waitFor(() => {}); expect(container).toMatchSnapshot(); }); - it('renders a Toolbar More component with manage content (working copy)', async () => { - config.settings.hasWorkingCopySupport = true; - - const { container } = render( - - - - {}} - theToolbar={{ - current: { getBoundingClientRect: () => ({ width: '320' }) }, - }} - closeMenu={() => {}} - /> - - - , - ); - await waitFor(() => {}); - expect(container).toMatchSnapshot(); - }); }); diff --git a/packages/volto/src/components/manage/Toolbar/__snapshots__/More.test.jsx.snap b/packages/volto/src/components/manage/Toolbar/__snapshots__/More.test.jsx.snap index da818c4054..42ca733d97 100644 --- a/packages/volto/src/components/manage/Toolbar/__snapshots__/More.test.jsx.snap +++ b/packages/volto/src/components/manage/Toolbar/__snapshots__/More.test.jsx.snap @@ -110,138 +110,3 @@ exports[`Toolbar More component renders a Toolbar More component 1`] = ` `; - -exports[`Toolbar More component renders a Toolbar More component with manage content (working copy) 1`] = ` -
    - -
    -`; diff --git a/packages/volto/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx b/packages/volto/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx index 07cc48d57d..50434ffe4d 100644 --- a/packages/volto/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx +++ b/packages/volto/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx @@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl'; import Toast from '@plone/volto/components/manage/Toast/Toast'; import { flattenToAppURL } from '@plone/volto/helpers/Url/Url'; import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate'; -import config from '@plone/volto/registry'; import useDeepCompareEffect from 'use-deep-compare-effect'; const messages = defineMessages({ @@ -39,48 +38,28 @@ const WorkingCopyToastsFactory = (props) => { }; useDeepCompareEffect(() => { - if (content && config.settings.hasWorkingCopySupport) { - if (working_copy) { - let toastMessage, toastTitle; - if (content.working_copy_of) { - // I'm a working copy - toastMessage = messages.thisIsAWorkingCopyOf; - toastTitle = ( - - {content.working_copy_of?.title} - - ); - } else { - // I'm a baseline - toastMessage = messages.workingCopyIs; - toastTitle = ( - - {working_copy?.title} - - ); - } - if (toast.isActive('workingcopyinfo')) { - toast.update('workingcopyinfo', { - render: ( - - ), - })} - /> - ), - }); - } else { - toast.info( + if (working_copy) { + let toastMessage, toastTitle; + if (content.working_copy_of) { + // I'm a working copy + toastMessage = messages.thisIsAWorkingCopyOf; + toastTitle = ( + + {content.working_copy_of?.title} + + ); + } else { + // I'm a baseline + toastMessage = messages.workingCopyIs; + toastTitle = ( + + {working_copy?.title} + + ); + } + if (toast.isActive('workingcopyinfo')) { + toast.update('workingcopyinfo', { + render: ( { /> ), })} - />, - { - toastId: 'workingcopyinfo', - autoClose: false, - closeButton: false, - transition: null, - }, - ); - } + /> + ), + }); + } else { + toast.info( + + ), + })} + />, + { + toastId: 'workingcopyinfo', + autoClose: false, + closeButton: false, + transition: null, + }, + ); } - if (!working_copy) { - if (toast.isActive('workingcopyinfo')) { - toast.dismiss('workingcopyinfo'); - } + } + if (!working_copy) { + if (toast.isActive('workingcopyinfo')) { + toast.dismiss('workingcopyinfo'); } } }, [pathname, content, title, working_copy, intl, lang, dateOptions]); diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js index 0c8f57d0b6..27fc225463 100644 --- a/packages/volto/src/config/index.js +++ b/packages/volto/src/config/index.js @@ -163,7 +163,6 @@ let config = { showSelfRegistration: false, contentMetadataTagsImageField: 'image', contentPropertiesSchemaEnhancer: null, - hasWorkingCopySupport: false, maxUndoLevels: 200, // undo history size for the main form addonsInfo: addonsInfo, workflowMapping, From b270d2569e57595ee51b0fa547b56437acface16 Mon Sep 17 00:00:00 2001 From: Ion Lizarazu Date: Wed, 29 Jan 2025 12:59:14 +0100 Subject: [PATCH 04/19] Ionlizarazu pattern validation message (#6632) Co-authored-by: Steve Piercy --- packages/volto/news/6631.bugfix | 1 + packages/volto/src/helpers/FormValidation/validators.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 packages/volto/news/6631.bugfix diff --git a/packages/volto/news/6631.bugfix b/packages/volto/news/6631.bugfix new file mode 100644 index 0000000000..051dd8aaba --- /dev/null +++ b/packages/volto/news/6631.bugfix @@ -0,0 +1 @@ +Added `field.pattern` as a parameter to the internationalized message. @ionlizarazu \ No newline at end of file diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index ad4f461e48..7abc954fb8 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -171,7 +171,9 @@ export const patternValidator = ({ } const regex = new RegExp(field.pattern); const isValid = regex.test(value); - return !isValid ? formatMessage(messages.pattern) : null; + return !isValid + ? formatMessage(messages.pattern, { pattern: field.pattern }) + : null; }; export const maxItemsValidator = ({ From c421af5b058dd7ca17abfa9ffcc7855ed9f414fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 30 Jan 2025 08:29:05 +0100 Subject: [PATCH 05/19] [registry] Addons styles loader (#6630) Co-authored-by: David Glick Co-authored-by: Steve Piercy --- apps/remix/.gitignore | 2 +- apps/rr7/.gitignore | 2 +- apps/rr7/.registry.loader.js | 64 ++++++++++++ .../__tests__/create-addons-loader.test.js | 10 +- .../docs/conceptual-guides/add-on-loader.md | 99 +++++++++++++++++++ .../conceptual-guides/add-on-styles-loader.md | 50 ++++++++++ packages/registry/docs/index.md | 2 + packages/registry/news/6630.feature | 1 + packages/registry/package.json | 4 + .../src/addon-registry/addon-registry.ts | 18 ++++ .../addon-registry/create-addons-loader.ts | 7 +- .../create-addons-styles-loader.ts | 27 +++++ packages/registry/vite-plugin.js | 3 + packages/seven/.gitignore | 3 +- packages/seven/app/routes.ts | 2 +- packages/seven/news/6630.feature | 1 + packages/volto/.gitignore | 2 +- packages/volto/.registry.loader.js | 36 +++++++ .../__tests__/create-addons-loader.test.js | 10 +- packages/volto/news/6630.feature | 1 + 20 files changed, 328 insertions(+), 16 deletions(-) create mode 100644 apps/rr7/.registry.loader.js create mode 100644 packages/registry/docs/conceptual-guides/add-on-loader.md create mode 100644 packages/registry/docs/conceptual-guides/add-on-styles-loader.md create mode 100644 packages/registry/news/6630.feature create mode 100644 packages/registry/src/addon-registry/create-addons-styles-loader.ts create mode 100644 packages/seven/news/6630.feature create mode 100644 packages/volto/.registry.loader.js create mode 100644 packages/volto/news/6630.feature diff --git a/apps/remix/.gitignore b/apps/remix/.gitignore index 8bf74e0cf1..94de5777a9 100644 --- a/apps/remix/.gitignore +++ b/apps/remix/.gitignore @@ -4,4 +4,4 @@ node_modules /build /public/build .env -.registry.loader.js +registry.loader.js diff --git a/apps/rr7/.gitignore b/apps/rr7/.gitignore index f1eb112b25..81fb5862b1 100644 --- a/apps/rr7/.gitignore +++ b/apps/rr7/.gitignore @@ -4,4 +4,4 @@ node_modules /build .env .react-router -.registry.loader.js +registry.loader.js diff --git a/apps/rr7/.registry.loader.js b/apps/rr7/.registry.loader.js new file mode 100644 index 0000000000..8068eeb7dc --- /dev/null +++ b/apps/rr7/.registry.loader.js @@ -0,0 +1,64 @@ +/* +This file is autogenerated. Don't change it directly. +Instead, change the "addons" setting in your package.json file. +*/ + +import ploneblocks from '@plone/blocks'; +import ploneslots from '@plone/slots'; + +const addonsInfo = [ + { + name: '@plone/blocks', + version: '1.0.0-alpha.1', + isPublishedPackage: true, + isRegisteredAddon: true, + modulePath: '/Users/sneridagh/Development/plone/volto/packages/blocks', + packageJson: + '/Users/sneridagh/Development/plone/volto/packages/blocks/package.json', + basePath: '/Users/sneridagh/Development/plone/volto/packages/blocks', + tsConfigPaths: ['', {}], + addons: [], + }, + { + name: '@plone/slots', + version: '1.0.0', + isPublishedPackage: true, + isRegisteredAddon: true, + modulePath: '/Users/sneridagh/Development/plone/volto/packages/slots', + packageJson: + '/Users/sneridagh/Development/plone/volto/packages/slots/package.json', + basePath: '/Users/sneridagh/Development/plone/volto/packages/slots', + tsConfigPaths: ['', {}], + addons: [], + }, +]; +export { addonsInfo }; + +const safeWrapper = (func) => (config) => { + const res = func(config); + if (typeof res === 'undefined') { + throw new Error("Configuration function doesn't return config"); + } + return res; +}; + +const projectConfigLoader = false; +const projectConfig = (config) => { + return projectConfigLoader && + typeof projectConfigLoader.default === 'function' + ? projectConfigLoader.default(config) + : config; +}; + +const load = (config) => { + const addonLoaders = [ploneblocks, ploneslots]; + if (!addonLoaders.every((el) => typeof el === 'function')) { + throw new TypeError( + 'Each addon has to provide a function applying its configuration to the projects configuration.', + ); + } + return projectConfig( + addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config), + ); +}; +export default load; diff --git a/packages/registry/__tests__/create-addons-loader.test.js b/packages/registry/__tests__/create-addons-loader.test.js index 60b611ad67..0ef6aa7b99 100644 --- a/packages/registry/__tests__/create-addons-loader.test.js +++ b/packages/registry/__tests__/create-addons-loader.test.js @@ -8,8 +8,9 @@ describe('create-addons-loader code generation', () => { test('no addon creates simple loader, default = no loadProjectConfig', () => { const code = getAddonsLoaderCode([]); expect(code).toBe(`/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. +Don't change this file manually. +It is autogenerated by @plone/registry. +Instead, change the "addons" registration in the app. */ @@ -45,8 +46,9 @@ export default load; test('no addon creates simple loader, loadProjectConfig set to true', () => { const code = getAddonsLoaderCode([], {}, true); expect(code).toBe(`/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. +Don't change this file manually. +It is autogenerated by @plone/registry. +Instead, change the "addons" registration in the app. */ const projectConfigLoader = require('@root/config'); diff --git a/packages/registry/docs/conceptual-guides/add-on-loader.md b/packages/registry/docs/conceptual-guides/add-on-loader.md new file mode 100644 index 0000000000..707fc6f4d0 --- /dev/null +++ b/packages/registry/docs/conceptual-guides/add-on-loader.md @@ -0,0 +1,99 @@ +--- +myst: + html_meta: + "description": "An explanation of the add-ons configuration loader in @plone/registry" + "property=og:description": "An explanation of the add-ons configuration loader in @plone/registry" + "property=og:title": "Add-ons configuration loader" + "keywords": "@plone/registry, registry, add-ons, loader" +--- + +# Add-ons configuration loader + +Add-ons that are compatible with `@plone/registry` can load configuration into the configuration registry. +`@plone/registry` reads the value of the `main` key entry point in the add-on {file}`package.json`, which specifies the source of the loader. +This should be a JavaScript or TypeScript file, such as {file}`index.ts` or {file}`index.js`, placed somewhere in your add-on, conventionally at its root. + +```json +{ + "main": "index.ts", +} +``` + +This file should contain a default export with a function with this signature: + +```ts +import type { ConfigType } from '@plone/registry'; + +export default function loadConfig(config: ConfigType) { + // You can mutate the configuration object in here + return config; +} +``` + +`@plone/registry` has a helper utility `createAddonsLoader` which generates the add-ons loader file. +That file contains the code needed to load the add-ons configuration of all the registered add-ons, keeping the order in which they were defined. + +This loader is a JavaScript file and it is placed in the root of your application. +By default, it's called {file}`registry.loader.js`. + +```{important} +This file is generated and maintained by `@plone/registry`. +You should neither modify it nor add your own styles in here. +It will be overwritten in the next bundler run. +``` + +The add-ons loader generator is meant to be run before bundling your app or by the bundler itself when it runs. +The `@plone/registry` Vite plugin generates this file, so the framework can load it during app bootstrap time, as shown below. + +```js + const projectRootPath = path.resolve('.'); + const { registry, shadowAliases } = AddonRegistry.init(projectRootPath); + + createAddonsLoader( + registry.getAddonDependencies(), + registry.getAddons(), + { tempInProject: true }, + ); +``` + +This will create {file}`registry.loader.js` in the root of your app. + +Afterwards, configure your app to load the code during the app bootstrap, as early as possible in both your client and server code, and as a module side-effect, as shown in the following example. + +```js +import config from '@plone/registry'; +import applyAddonConfiguration from './registry.loader'; + +applyAddonConfiguration(config); +``` + +```{note} +If you use a Vite-powered framework, use the `@plone/registry` Vite plugin. +If you use a non-Vite framework, you will have to build your own integration. +You can take the implementation of the Vite plugin as reference. +``` + +## Provide optional add-on configurations + +You can export additional configuration functions from your add-ons configuration loader file. +The default export is always loaded by `@plone/registry`, while the named exports are optional for loading as needed. + +To specify optional loaders as needed, in the add-on registration, use the name of your add-on, followed by a colon (`:`), followed by the names of the optional loaders as comma-separated values as shown in the following example. + +```{code-block} json +:emphasize-lines: 4 + +{ + "name": "my-nice-volto-project", + "addons": [ + "my-volto-add-on-name:loadOptionalBlocks,overrideSomeDefaultBlock", + "volto-another-add-on" + ], +} +``` + +```{note} +The additional comma-separated names should be exported from the add-on configuration loader file. +The main configuration function should be exported as the default. +An add-on's default configuration method will always be loaded. +``` diff --git a/packages/registry/docs/conceptual-guides/add-on-styles-loader.md b/packages/registry/docs/conceptual-guides/add-on-styles-loader.md new file mode 100644 index 0000000000..55c172d62e --- /dev/null +++ b/packages/registry/docs/conceptual-guides/add-on-styles-loader.md @@ -0,0 +1,50 @@ +--- +myst: + html_meta: + "description": "An explanation of the add-ons styles loader in @plone/registry" + "property=og:description": "An explanation of the add-ons styles loader in @plone/registry" + "property=og:title": "Add-ons styles loader" + "keywords": "@plone/registry, registry, add-ons, loader" +--- + +# Add-ons styles loader + +Add-ons that are compatible with the `@plone/registry` may declare styles that should be loaded by the app. +To do so, create a file {file}`styles/main.css` at the root of your project which serves as the entry point. +This file is a `.css` file containing the styles that you want your app to load. + +`@plone/registry` has a helper utility `createAddonsStyleLoader` which generates an add-ons loader file. +That file contains the aggregated files from all the registered add-ons, keeping the order in which they were defined. + +This loader is also a `.css` file and is placed in the root of your application. +By default, it's called {file}`addons.styles.css`. + +```{important} +This file is generated and maintained by `@plone/registry`. +You should neither modify it nor add your own styles in here. +It will be overwritten in the next bundler run. +``` + +The add-ons loader generator is meant to be run before bundling your app or by the bundler when it runs. +The `@plone/registry` Vite plugin generates this file, so the framework can load it during app bootstrap time. + +```js + const projectRootPath = path.resolve('.'); + const { registry, shadowAliases } = AddonRegistry.init(projectRootPath); + + createAddonsStyleLoader(registry.getAddonStyles()); +``` + +This will create {file}`addons.styles.css` in the root of your app. +Afterwards, configure your app to load the CSS according to the framework's convention, and in both a centralized and performant manner. + +```css +@import "tailwind"; +@import "./addons.styles.css" +``` + +```{note} +If you use a Vite-powered framework, use the `@plone/registry` Vite plugin. +If you use a non-Vite framework, you will have to build your own integration. +You can take the implementation of the Vite plugin as reference. +``` diff --git a/packages/registry/docs/index.md b/packages/registry/docs/index.md index d70b5ce51c..a300ae79a7 100644 --- a/packages/registry/docs/index.md +++ b/packages/registry/docs/index.md @@ -41,4 +41,6 @@ conceptual-guides/add-on-registry conceptual-guides/configuration-registry conceptual-guides/component-registry conceptual-guides/utility-registry +conceptual-guides/add-on-loader +conceptual-guides/add-on-styles-loader ``` diff --git a/packages/registry/news/6630.feature b/packages/registry/news/6630.feature new file mode 100644 index 0000000000..07fad44d0f --- /dev/null +++ b/packages/registry/news/6630.feature @@ -0,0 +1 @@ +Added add-ons styles loader. @sneridagh diff --git a/packages/registry/package.json b/packages/registry/package.json index 1f0f9047e0..c144a0a01c 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -52,6 +52,10 @@ "import": "./dist/addon-registry/create-theme-loader.js", "default": "./dist/addon-registry/create-theme-loader.cjs" }, + "./create-addons-styles-loader": { + "import": "./dist/addon-registry/create-addons-styles-loader.js", + "default": "./dist/addon-registry/create-addons-styles-loader.cjs" + }, "./vite-plugin": { "import": "./vite-plugin.js", "types": "./vite-plugin.d.ts" diff --git a/packages/registry/src/addon-registry/addon-registry.ts b/packages/registry/src/addon-registry/addon-registry.ts index e4eb233c89..87d50d8333 100644 --- a/packages/registry/src/addon-registry/addon-registry.ts +++ b/packages/registry/src/addon-registry/addon-registry.ts @@ -498,6 +498,24 @@ class AddonRegistry { .filter((e) => e); } + /** + * Returns the list of add-on style files (/styles/main.css) that contain styles + */ + getAddonStyles() { + const addonsStylesInfo: Array = []; + + this.getAddonDependencies().forEach((addon) => { + const normalizedAddonName = addon.split(':')[0] as string; + + const addonStyleFile = `${this.packages[normalizedAddonName]?.modulePath}/styles/main.css`; + if (fs.existsSync(addonStyleFile)) { + addonsStylesInfo.push(normalizedAddonName); + } + }); + + return addonsStylesInfo; + } + getCustomThemeAddons() { const customThemeAddonsInfo: { variables: string[]; diff --git a/packages/registry/src/addon-registry/create-addons-loader.ts b/packages/registry/src/addon-registry/create-addons-loader.ts index cd5b4f6989..ea68906c5d 100644 --- a/packages/registry/src/addon-registry/create-addons-loader.ts +++ b/packages/registry/src/addon-registry/create-addons-loader.ts @@ -30,8 +30,9 @@ function getAddonsLoaderCode( loadProjectConfig?: boolean, ) { let buf = `/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. +Don't change this file manually. +It is autogenerated by @plone/registry. +Instead, change the "addons" registration in the app. */ `; @@ -119,7 +120,7 @@ export function createAddonsLoader( // the `tempInProject` allows to place it inside let addonsLoaderPath: string; if (tempInProject) { - addonsLoaderPath = path.join(process.cwd(), '.registry.loader.js'); + addonsLoaderPath = path.join(process.cwd(), 'registry.loader.js'); } else { addonsLoaderPath = tmp.tmpNameSync({ postfix: '.js' }); } diff --git a/packages/registry/src/addon-registry/create-addons-styles-loader.ts b/packages/registry/src/addon-registry/create-addons-styles-loader.ts new file mode 100644 index 0000000000..ae55bf3ad9 --- /dev/null +++ b/packages/registry/src/addon-registry/create-addons-styles-loader.ts @@ -0,0 +1,27 @@ +import fs from 'fs'; +import path from 'path'; + +function buildLoaderCode(addonsStylesInfo: Array = []) { + let buf = `/* +Don't change this file manually. +It is autogenerated by @plone/registry. +Add a ./styles/main.css in your add-on to load your add-on styles in the app. +*/ + +`; + addonsStylesInfo.forEach((addon) => { + const customization = `${addon}/styles/main.css`; + const line = `@import '${customization}';\n`; + buf += line; + }); + + return buf; +} + +export function createAddonsStyleLoader(addonsStylesInfo: Array) { + const addonsLoaderPath = path.join(process.cwd(), 'addons.styles.css'); + + fs.writeFileSync(addonsLoaderPath, buildLoaderCode(addonsStylesInfo)); +} + +export { buildLoaderCode }; diff --git a/packages/registry/vite-plugin.js b/packages/registry/vite-plugin.js index 4326b1a88f..e340075407 100644 --- a/packages/registry/vite-plugin.js +++ b/packages/registry/vite-plugin.js @@ -2,6 +2,7 @@ import path from 'path'; import { AddonRegistry } from '@plone/registry/addon-registry'; import { createAddonsLoader } from '@plone/registry/create-addons-loader'; import { createThemeAddonsLoader } from '@plone/registry/create-theme-loader'; +import { createAddonsStyleLoader } from '@plone/registry/create-addons-styles-loader'; export const PloneRegistryVitePlugin = () => { const projectRootPath = path.resolve('.'); @@ -13,6 +14,8 @@ export const PloneRegistryVitePlugin = () => { { tempInProject: true }, ); + createAddonsStyleLoader(registry.getAddonStyles()); + const [addonsThemeLoaderVariablesPath, addonsThemeLoaderMainPath] = createThemeAddonsLoader(registry.getCustomThemeAddons()); diff --git a/packages/seven/.gitignore b/packages/seven/.gitignore index f1eb112b25..209abd6b56 100644 --- a/packages/seven/.gitignore +++ b/packages/seven/.gitignore @@ -4,4 +4,5 @@ node_modules /build .env .react-router -.registry.loader.js +registry.loader.js +addons.styles.css diff --git a/packages/seven/app/routes.ts b/packages/seven/app/routes.ts index 9f2ba5b06c..eef6c3fa14 100644 --- a/packages/seven/app/routes.ts +++ b/packages/seven/app/routes.ts @@ -3,7 +3,7 @@ import { index, route } from '@react-router/dev/routes'; import { getAddonRoutesConfig } from '@plone/react-router'; import config from '@plone/registry'; // eslint-disable-next-line import/no-unresolved -import applyAddonConfiguration, { addonsInfo } from '../.registry.loader'; +import applyAddonConfiguration, { addonsInfo } from '../registry.loader'; applyAddonConfiguration(config); diff --git a/packages/seven/news/6630.feature b/packages/seven/news/6630.feature new file mode 100644 index 0000000000..82d947c9cd --- /dev/null +++ b/packages/seven/news/6630.feature @@ -0,0 +1 @@ +Do not use the dotted notation for registry generated files. @sneridagh diff --git a/packages/volto/.gitignore b/packages/volto/.gitignore index b013f1deae..b8422498de 100644 --- a/packages/volto/.gitignore +++ b/packages/volto/.gitignore @@ -61,4 +61,4 @@ docs/_build/ /.tool-versions docs/source/news -.registry.loader.js +registry.loader.js diff --git a/packages/volto/.registry.loader.js b/packages/volto/.registry.loader.js new file mode 100644 index 0000000000..a02285fadb --- /dev/null +++ b/packages/volto/.registry.loader.js @@ -0,0 +1,36 @@ +/* +This file is autogenerated. Don't change it directly. +Instead, change the "addons" setting in your package.json file. +*/ + +const addonsInfo = []; +export { addonsInfo }; + +const safeWrapper = (func) => (config) => { + const res = func(config); + if (typeof res === 'undefined') { + throw new Error("Configuration function doesn't return config"); + } + return res; +}; + +const projectConfigLoader = false; +const projectConfig = (config) => { + return projectConfigLoader && + typeof projectConfigLoader.default === 'function' + ? projectConfigLoader.default(config) + : config; +}; + +const load = (config) => { + const addonLoaders = []; + if (!addonLoaders.every((el) => typeof el === 'function')) { + throw new TypeError( + 'Each addon has to provide a function applying its configuration to the projects configuration.', + ); + } + return projectConfig( + addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config), + ); +}; +export default load; diff --git a/packages/volto/__tests__/create-addons-loader.test.js b/packages/volto/__tests__/create-addons-loader.test.js index dfe9d174cb..84b4682e12 100644 --- a/packages/volto/__tests__/create-addons-loader.test.js +++ b/packages/volto/__tests__/create-addons-loader.test.js @@ -10,8 +10,9 @@ describe('create-addons-loader code generation', () => { test('no addon creates simple loader, default = no loadProjectConfig', () => { const code = getAddonsLoaderCode([]); expect(code).toBe(`/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. +Don't change this file manually. +It is autogenerated by @plone/registry. +Instead, change the "addons" registration in the app. */ @@ -47,8 +48,9 @@ export default load; test('no addon creates simple loader, loadProjectConfig set to true', () => { const code = getAddonsLoaderCode([], {}, true); expect(code).toBe(`/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. +Don't change this file manually. +It is autogenerated by @plone/registry. +Instead, change the "addons" registration in the app. */ const projectConfigLoader = require('@root/config'); diff --git a/packages/volto/news/6630.feature b/packages/volto/news/6630.feature new file mode 100644 index 0000000000..924d117a9a --- /dev/null +++ b/packages/volto/news/6630.feature @@ -0,0 +1 @@ +Update tests to match the new message in the add-ons loader. @sneridagh From fcdcdc54b30a4788b1aadd2bde55557fb5624e52 Mon Sep 17 00:00:00 2001 From: Giulia Ghisini <51911425+giuliaghisini@users.noreply.github.com> Date: Thu, 30 Jan 2025 18:38:48 +0100 Subject: [PATCH 06/19] Fix blurry images (#6634) Co-authored-by: Steve Piercy --- packages/volto/news/6634.bugfix | 1 + .../__snapshots__/LeadImageSidebar.test.jsx.snap | 2 +- packages/volto/src/components/theme/Image/Image.jsx | 9 ++++++++- .../theme/Image/__snapshots__/Image.test.jsx.snap | 10 +++++----- pnpm-lock.yaml | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 packages/volto/news/6634.bugfix diff --git a/packages/volto/news/6634.bugfix b/packages/volto/news/6634.bugfix new file mode 100644 index 0000000000..b559da23d4 --- /dev/null +++ b/packages/volto/news/6634.bugfix @@ -0,0 +1 @@ +Display the appropriately sized image to eliminate blurring from upsizing smaller images in the `srcSet` generation. @giuliaghisini diff --git a/packages/volto/src/components/manage/Blocks/LeadImage/__snapshots__/LeadImageSidebar.test.jsx.snap b/packages/volto/src/components/manage/Blocks/LeadImage/__snapshots__/LeadImageSidebar.test.jsx.snap index 07e5ee1607..19435bd868 100644 --- a/packages/volto/src/components/manage/Blocks/LeadImage/__snapshots__/LeadImageSidebar.test.jsx.snap +++ b/packages/volto/src/components/manage/Blocks/LeadImage/__snapshots__/LeadImageSidebar.test.jsx.snap @@ -21,7 +21,7 @@ exports[`renders a Lead Image block Sidebar component 1`] = ` height={400} sizes="188px" src="/image.png/@@images/image-1200.png" - srcSet="/image.png/@@images/image-400.png 400w" + srcSet="/image.png/@@images/image-400.png 400w, /image.png/@@images/image-1200.png 400w" width={400} /> diff --git a/packages/volto/src/components/theme/Image/Image.jsx b/packages/volto/src/components/theme/Image/Image.jsx index 30d2f2cbc4..8bd15225a2 100644 --- a/packages/volto/src/components/theme/Image/Image.jsx +++ b/packages/volto/src/components/theme/Image/Image.jsx @@ -54,7 +54,14 @@ export default function Image({ attrs.className = cx(className, { responsive }); if (!isSvg && image.scales && Object.keys(image.scales).length > 0) { - const sortedScales = Object.values(image.scales).sort((a, b) => { + const sortedScales = Object.values({ + ...image.scales, + original: { + download: `${image.download}`, + width: image.width, + height: image.height, + }, + }).sort((a, b) => { if (a.width > b.width) return 1; else if (a.width < b.width) return -1; else return 0; diff --git a/packages/volto/src/components/theme/Image/__snapshots__/Image.test.jsx.snap b/packages/volto/src/components/theme/Image/__snapshots__/Image.test.jsx.snap index 03773f1762..372034092d 100644 --- a/packages/volto/src/components/theme/Image/__snapshots__/Image.test.jsx.snap +++ b/packages/volto/src/components/theme/Image/__snapshots__/Image.test.jsx.snap @@ -7,7 +7,7 @@ exports[`renders an image component from a catalog brain 1`] = ` fetchpriority="high" height={400} src="/image/@@images/image.png" - srcSet="/image/@@images/image-400.png 400w" + srcSet="/image/@@images/image-400.png 400w, /image/@@images/image.png 400w" width={400} /> `; @@ -19,7 +19,7 @@ exports[`renders an image component from a catalog brain using \`preview_image_l fetchpriority="high" height={400} src="/image.png/@@images/image.png" - srcSet="/image.png/@@images/image-400.png 400w" + srcSet="/image.png/@@images/image-400.png 400w, /image.png/@@images/image.png 400w" width={400} /> `; @@ -40,7 +40,7 @@ exports[`renders an image component with fetchpriority high 1`] = ` fetchpriority="high" height={400} src="/image/@@images/image.png" - srcSet="/image/@@images/image-400.png 400w" + srcSet="/image/@@images/image-400.png 400w, /image/@@images/image.png 400w" width={400} /> `; @@ -53,7 +53,7 @@ exports[`renders an image component with lazy loading 1`] = ` height={400} loading="lazy" src="/image/@@images/image.png" - srcSet="/image/@@images/image-400.png 400w" + srcSet="/image/@@images/image-400.png 400w, /image/@@images/image.png 400w" width={400} /> `; @@ -65,7 +65,7 @@ exports[`renders an image component with responsive class 1`] = ` fetchpriority="high" height={400} src="/image/@@images/image-1200.png" - srcSet="/image/@@images/image-400.png 400w" + srcSet="/image/@@images/image-400.png 400w, /image/@@images/image-1200.png 400w" width={400} /> `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57e84a67d7..55c3a65625 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18546,7 +18546,7 @@ snapshots: '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.8)': dependencies: '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.4)': dependencies: From 3a8d8a464a17f974ab32a03d6bdaac7c0bac2b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katja=20S=C3=BCss?= Date: Thu, 30 Jan 2025 18:40:15 +0100 Subject: [PATCH 07/19] =?UTF-8?q?Add=20acceptance=20test=20for=20non-manag?= =?UTF-8?q?er=20user=20editing=20group=20memberships=20(c=E2=80=A6=20(#663?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user-group-membership-control-panel.js | 23 +++++++++++++++++++ packages/volto/news/5786.internal | 1 + 2 files changed, 24 insertions(+) create mode 100644 packages/volto/news/5786.internal diff --git a/packages/volto/cypress/tests/core/controlpanels/user-group-membership-control-panel.js b/packages/volto/cypress/tests/core/controlpanels/user-group-membership-control-panel.js index 44f2e76daa..dd71ada233 100644 --- a/packages/volto/cypress/tests/core/controlpanels/user-group-membership-control-panel.js +++ b/packages/volto/cypress/tests/core/controlpanels/user-group-membership-control-panel.js @@ -160,3 +160,26 @@ describe('User Group Membership Control Panel test for MANY users and MANY group }); }); }); + +describe('Checkboxes of group "Administrators" are disabled for non-manager users', () => { + beforeEach(() => { + init(); + cy.createUser({ + username: 'siteadmin', + fullname: 'Sven Siteadministrator', + roles: ['Site Administrator'], + }); + cy.autologin('siteadmin', 'password'); + }); + it('Non-manager is not allowed to edit managers', () => { + cy.visit('/controlpanel/usergroupmembership'); + cy.wait('@usergroup'); + + // Editing checkboxes for Administrators group are disabled. + cy.get('.usergroupmembership').then(() => { + cy.get('#source-row-max div.checkbox_Administrators input').should( + 'be.disabled', + ); + }); + }); +}); diff --git a/packages/volto/news/5786.internal b/packages/volto/news/5786.internal new file mode 100644 index 0000000000..541e252d48 --- /dev/null +++ b/packages/volto/news/5786.internal @@ -0,0 +1 @@ +Add acceptance test for non-manager user editing group memberships. @ksuess \ No newline at end of file From afcb3cfaa76048b37603175461be5e7a602ab30c Mon Sep 17 00:00:00 2001 From: Wesley Barroso Lopes Date: Thu, 30 Jan 2025 21:27:44 -0300 Subject: [PATCH 08/19] Pass intl object to initialValue function (#6538) --- packages/volto-slate/news/6529.bugfix | 1 + .../blocks/Text/DefaultTextBlockEditor.jsx | 4 +- .../src/blocks/Text/extensions/breakList.js | 9 ++-- .../src/blocks/Text/extensions/insertBreak.js | 5 +- .../src/blocks/Text/keyboard/joinBlocks.js | 10 ++-- .../volto-slate/src/utils/volto-blocks.js | 14 ++++-- packages/volto/news/6529.bugfix | 2 + .../manage/Blocks/Block/BlocksForm.jsx | 10 ++-- .../manage/Blocks/Block/EditBlockWrapper.jsx | 1 + packages/volto/src/helpers/Blocks/Blocks.js | 30 ++++++++++-- .../volto/src/helpers/Blocks/Blocks.test.js | 46 +++++++++++++++++++ .../volto/types/helpers/Blocks/Blocks.d.ts | 18 ++++++-- 12 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 packages/volto-slate/news/6529.bugfix create mode 100644 packages/volto/news/6529.bugfix diff --git a/packages/volto-slate/news/6529.bugfix b/packages/volto-slate/news/6529.bugfix new file mode 100644 index 0000000000..75d42324e0 --- /dev/null +++ b/packages/volto-slate/news/6529.bugfix @@ -0,0 +1 @@ +Pass `intl` object to `initialValue` function. @wesleybl diff --git a/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx b/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx index 385cc84a65..b0c7b8bbe0 100644 --- a/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx +++ b/packages/volto-slate/src/blocks/Text/DefaultTextBlockEditor.jsx @@ -142,10 +142,10 @@ export const DefaultTextBlockEditor = (props) => { const url = flattenToAppURL(imageId); setNewImageId(imageId); - createImageBlock(url, index, props); + createImageBlock(url, index, props, intl); } prevReq.current = loaded; - }, [props, loaded, loading, prevLoaded, imageId, newImageId, index]); + }, [props, loaded, loading, prevLoaded, imageId, newImageId, index, intl]); const handleUpdate = React.useCallback( (editor) => { diff --git a/packages/volto-slate/src/blocks/Text/extensions/breakList.js b/packages/volto-slate/src/blocks/Text/extensions/breakList.js index 515b8c4c67..a3249a1117 100644 --- a/packages/volto-slate/src/blocks/Text/extensions/breakList.js +++ b/packages/volto-slate/src/blocks/Text/extensions/breakList.js @@ -11,6 +11,7 @@ import { createEmptyParagraph } from '@plone/volto-slate/utils/blocks'; * Handles `Enter` key on empty and non-empty list items. * * @param {Editor} editor The editor which should be modified by this extension + * @param {Object} intl intl object. * with a new version of the `insertBreak` method of the Slate editor. * * @description If the selection does not exist or is expanded, handle with the @@ -20,7 +21,7 @@ import { createEmptyParagraph } from '@plone/volto-slate/utils/blocks'; * text cursor and then split the editor in two fragments, and convert them to * separate Slate Text blocks, based on the selection. */ -export const breakList = (editor) => { +export const breakList = (editor, intl) => { const { insertBreak } = editor; editor.insertBreak = () => { @@ -84,7 +85,7 @@ export const breakList = (editor) => { }); Transforms.select(editor, Editor.end(editor, [])); } else { - createAndSelectNewBlockAfter(editor, [createEmptyParagraph()]); + createAndSelectNewBlockAfter(editor, [createEmptyParagraph()], intl); Transforms.removeNodes(editor, { at: ref.current }); } return true; @@ -96,7 +97,7 @@ export const breakList = (editor) => { if (detached) { Editor.insertNode(editor, createEmptyParagraph()); } else { - createAndSelectNewBlockAfter(editor, [createEmptyParagraph()]); + createAndSelectNewBlockAfter(editor, [createEmptyParagraph()], intl); } return true; } @@ -104,7 +105,7 @@ export const breakList = (editor) => { if (!detached) { const [top, bottom] = splitEditorInTwoFragments(editor, ref.current); setEditorContent(editor, top); - createAndSelectNewBlockAfter(editor, bottom); + createAndSelectNewBlockAfter(editor, bottom, intl); } return true; }; diff --git a/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js b/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js index 91846daf3e..2836bba490 100644 --- a/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js +++ b/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js @@ -8,6 +8,7 @@ import { rangeIsInSplittableNode } from '@plone/volto-slate/utils/internals'; /** * @param {Editor} editor The Slate editor object to extend. + * @param {Object} intl intl object. * @description If the selection exists and touches with one of its edges a * closest-to-root `Text` node (`Path` with length `2`) * @@ -18,7 +19,7 @@ import { rangeIsInSplittableNode } from '@plone/volto-slate/utils/internals'; * and if the selection does not exist or does not touch with one of its edges a * closest-to-root `Text` node, call the default behavior. */ -export const withSplitBlocksOnBreak = (editor) => { +export const withSplitBlocksOnBreak = (editor, intl) => { const { insertBreak } = editor; editor.insertBreak = () => { @@ -40,7 +41,7 @@ export const withSplitBlocksOnBreak = (editor) => { ReactDOM.unstable_batchedUpdates(() => { const [top, bottom] = splitEditorInTwoFragments(editor); // ReactEditor.blur(editor); - createAndSelectNewBlockAfter(editor, bottom); + createAndSelectNewBlockAfter(editor, bottom, intl); setEditorContent(editor, top); }); } diff --git a/packages/volto-slate/src/blocks/Text/keyboard/joinBlocks.js b/packages/volto-slate/src/blocks/Text/keyboard/joinBlocks.js index 8c166f8c0f..d00097f8e0 100644 --- a/packages/volto-slate/src/blocks/Text/keyboard/joinBlocks.js +++ b/packages/volto-slate/src/blocks/Text/keyboard/joinBlocks.js @@ -26,7 +26,7 @@ import { * @param {Editor} editor * @param {KeyboardEvent} event */ -export function joinWithPreviousBlock({ editor, event }) { +export function joinWithPreviousBlock({ editor, event }, intl) { if (!isCursorAtBlockStart(editor)) return; const blockProps = editor.getBlockProps(); @@ -60,7 +60,7 @@ export function joinWithPreviousBlock({ editor, event }) { const text = Editor.string(editor, []); if (!text) { const cursor = getBlockEndAsRange(otherBlock); - const newFormData = deleteBlock(properties, block); + const newFormData = deleteBlock(properties, block, intl); ReactDOM.unstable_batchedUpdates(() => { saveSlateBlockSelection(otherBlockId, cursor); @@ -89,7 +89,7 @@ export function joinWithPreviousBlock({ editor, event }) { value: combined, plaintext: serializeNodesToText(combined || []), }); - const newFormData = deleteBlock(formData, block); + const newFormData = deleteBlock(formData, block, intl); ReactDOM.unstable_batchedUpdates(() => { saveSlateBlockSelection(otherBlockId, cursor); @@ -107,7 +107,7 @@ export function joinWithPreviousBlock({ editor, event }) { * @param {Editor} editor * @param {KeyboardEvent} event */ -export function joinWithNextBlock({ editor, event }) { +export function joinWithNextBlock({ editor, event }, intl) { if (!isCursorAtBlockEnd(editor)) return; const blockProps = editor.getBlockProps(); @@ -146,7 +146,7 @@ export function joinWithNextBlock({ editor, event }) { value: combined, plaintext: serializeNodesToText(combined || []), }); - const newFormData = deleteBlock(formData, block); + const newFormData = deleteBlock(formData, block, intl); ReactDOM.unstable_batchedUpdates(() => { // saveSlateBlockSelection(otherBlockId, cursor); diff --git a/packages/volto-slate/src/utils/volto-blocks.js b/packages/volto-slate/src/utils/volto-blocks.js index e01a40cc68..f4bba8b8de 100644 --- a/packages/volto-slate/src/utils/volto-blocks.js +++ b/packages/volto-slate/src/utils/volto-blocks.js @@ -118,7 +118,7 @@ export function syncCreateSlateBlock(value) { return [id, block]; } -export function createImageBlock(url, index, props) { +export function createImageBlock(url, index, props, intl) { const { properties, onChangeField, onSelectBlock } = props; const blocksFieldname = getBlocksFieldname(properties); const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); @@ -128,7 +128,7 @@ export function createImageBlock(url, index, props) { let id, newFormData; if (currBlockHasValue) { - [id, newFormData] = addBlock(properties, 'image', index + 1); + [id, newFormData] = addBlock(properties, 'image', index + 1, {}, intl); newFormData = changeBlock(newFormData, id, { '@type': 'image', url }); } else { [id, newFormData] = insertBlock(properties, currBlockId, { @@ -144,12 +144,18 @@ export function createImageBlock(url, index, props) { }); } -export const createAndSelectNewBlockAfter = (editor, blockValue) => { +export const createAndSelectNewBlockAfter = (editor, blockValue, intl) => { const blockProps = editor.getBlockProps(); const { onSelectBlock, properties, index, onChangeField } = blockProps; - const [blockId, formData] = addBlock(properties, 'slate', index + 1); + const [blockId, formData] = addBlock( + properties, + 'slate', + index + 1, + {}, + intl, + ); const options = { '@type': 'slate', diff --git a/packages/volto/news/6529.bugfix b/packages/volto/news/6529.bugfix new file mode 100644 index 0000000000..c9e1029a3f --- /dev/null +++ b/packages/volto/news/6529.bugfix @@ -0,0 +1,2 @@ +Pass `intl` object to `initialValue` function. @wesleybl + diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx index a8fd1e7c05..cba2390706 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx @@ -138,7 +138,7 @@ const BlocksForm = (props) => { }; const onMutateBlock = (id, value) => { - const newFormData = mutateBlock(properties, id, value); + const newFormData = mutateBlock(properties, id, value, {}, intl); onChangeFormData(newFormData); }; @@ -149,6 +149,8 @@ const BlocksForm = (props) => { value, current, config.experimental.addBlockButton.enabled ? 1 : 0, + {}, + intl, ); const blocksFieldname = getBlocksFieldname(newFormData); @@ -166,7 +168,7 @@ const BlocksForm = (props) => { const onAddBlock = (type, index) => { if (editable) { - const [id, newFormData] = addBlock(properties, type, index); + const [id, newFormData] = addBlock(properties, type, index, {}, intl); const blocksFieldname = getBlocksFieldname(newFormData); const blockData = newFormData[blocksFieldname][id]; newFormData[blocksFieldname][id] = applyBlockDefaults({ @@ -188,7 +190,7 @@ const BlocksForm = (props) => { const onDeleteBlock = (id, selectPrev) => { const previous = previousBlockId(properties, id); - const newFormData = deleteBlock(properties, id); + const newFormData = deleteBlock(properties, id, intl); onChangeFormData(newFormData); onSelectBlock(selectPrev ? previous : null); @@ -260,7 +262,7 @@ const BlocksForm = (props) => { for (const [n, v] of blockList) { if (!v) { - const newFormData = deleteBlock(properties, n); + const newFormData = deleteBlock(properties, n, intl); onChangeFormData(newFormData); } } diff --git a/packages/volto/src/components/manage/Blocks/Block/EditBlockWrapper.jsx b/packages/volto/src/components/manage/Blocks/Block/EditBlockWrapper.jsx index 3ef354708b..31351f8439 100644 --- a/packages/volto/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/EditBlockWrapper.jsx @@ -135,6 +135,7 @@ const EditBlockWrapper = (props) => { [id]: value || null, }, }, + intl, }); const newValue = newFormData[blocksFieldname][id]; onChangeBlock(id, newValue); diff --git a/packages/volto/src/helpers/Blocks/Blocks.js b/packages/volto/src/helpers/Blocks/Blocks.js index d5559828e5..bff936b4ca 100644 --- a/packages/volto/src/helpers/Blocks/Blocks.js +++ b/packages/volto/src/helpers/Blocks/Blocks.js @@ -120,9 +120,10 @@ export function moveBlock(formData, source, destination) { * @function deleteBlock * @param {Object} formData Form data * @param {string} blockId Block uid + * @param {Object} intl intl object. * @return {Object} New form data */ -export function deleteBlock(formData, blockId) { +export function deleteBlock(formData, blockId, intl) { const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); @@ -135,7 +136,13 @@ export function deleteBlock(formData, blockId) { }; if (newFormData[blocksLayoutFieldname].items.length === 0) { - newFormData = addBlock(newFormData, config.settings.defaultBlockType, 0); + newFormData = addBlock( + newFormData, + config.settings.defaultBlockType, + 0, + {}, + intl, + ); } return newFormData; @@ -147,9 +154,11 @@ export function deleteBlock(formData, blockId) { * @param {Object} formData Form data * @param {string} type Block type * @param {number} index Destination index + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Array} New block id, New form data */ -export function addBlock(formData, type, index, blocksConfig) { +export function addBlock(formData, type, index, blocksConfig, intl) { const { settings } = config; const id = uuid(); const idTrailingBlock = uuid(); @@ -192,6 +201,7 @@ export function addBlock(formData, type, index, blocksConfig) { }, selected: id, }, + intl, }), ]; } @@ -208,6 +218,7 @@ export const applyBlockInitialValue = ({ value, blocksConfig, formData, + intl, }) => { const type = value['@type']; blocksConfig = blocksConfig || config.blocks.blocksConfig; @@ -217,6 +228,7 @@ export const applyBlockInitialValue = ({ id, value, formData, + intl, }); const blocksFieldname = getBlocksFieldname(formData); formData[blocksFieldname][id] = value; @@ -231,9 +243,11 @@ export const applyBlockInitialValue = ({ * @param {Object} formData Form data * @param {string} id Block uid to mutate * @param {number} value Block's new value + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Object} New form data */ -export function mutateBlock(formData, id, value, blocksConfig) { +export function mutateBlock(formData, id, value, blocksConfig, intl) { const { settings } = config; const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); @@ -260,6 +274,7 @@ export function mutateBlock(formData, id, value, blocksConfig) { [id]: value || null, }, }, + intl, }); if (!blockHasValue(block)) { return newFormData; @@ -288,6 +303,7 @@ export function mutateBlock(formData, id, value, blocksConfig) { ], }, }, + intl, }); return newFormData; } @@ -298,6 +314,10 @@ export function mutateBlock(formData, id, value, blocksConfig) { * @param {Object} formData Form data * @param {string} id Insert new block before the block with this id * @param {number} value New block's value + * @param {Object} current Current block + * @param {number} offset offset position + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Array} New block id, New form data */ export function insertBlock( @@ -307,6 +327,7 @@ export function insertBlock( current = {}, offset = 0, blocksConfig, + intl, ) { const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); @@ -340,6 +361,7 @@ export function insertBlock( ], }, }, + intl, }); return [newBlockId, newFormData]; diff --git a/packages/volto/src/helpers/Blocks/Blocks.test.js b/packages/volto/src/helpers/Blocks/Blocks.test.js index 659b3c9b50..2887d8d0a3 100644 --- a/packages/volto/src/helpers/Blocks/Blocks.test.js +++ b/packages/volto/src/helpers/Blocks/Blocks.test.js @@ -568,6 +568,52 @@ describe('Blocks', () => { marker: true, }); }); + + it('initialValue with intl', () => { + // Mock intl with formatMessage function + const intl = { + formatMessage: jest.fn(({ id }) => id), + }; + + const messages = { + intl: { + id: 'intl', + defaultMessage: 'intl', + }, + }; + + config.blocks.blocksConfig.text.initialValue = ({ + id, + value, + formData, + intl, + }) => { + return { + ...formData.blocks[id], + intl: intl.formatMessage(messages.intl), + }; + }; + const [newId, form] = addBlock( + { + blocks: { a: { value: 1 }, b: { value: 2 } }, + blocks_layout: { items: ['a', 'b'] }, + }, + 'text', + 1, + config.blocks.blocksConfig, + intl, + ); + + delete config.blocks.blocksConfig.text.initialValue; + + expect(form.blocks[newId]).toStrictEqual({ + '@type': 'text', + booleanField: false, + description: 'Default description', + title: 'Default title', + intl: 'intl', + }); + }); }); describe('moveBlock', () => { diff --git a/packages/volto/types/helpers/Blocks/Blocks.d.ts b/packages/volto/types/helpers/Blocks/Blocks.d.ts index 42c7973f94..f21c5fd7bd 100644 --- a/packages/volto/types/helpers/Blocks/Blocks.d.ts +++ b/packages/volto/types/helpers/Blocks/Blocks.d.ts @@ -40,36 +40,43 @@ export function moveBlock(formData: any, source: number, destination: number): a * @function deleteBlock * @param {Object} formData Form data * @param {string} blockId Block uid + * @param {Object} intl intl object. * @return {Object} New form data */ -export function deleteBlock(formData: any, blockId: string): any; +export function deleteBlock(formData: any, blockId: string, intl: Object): any; /** * Adds a block to the blocks form * @function addBlock * @param {Object} formData Form data * @param {string} type Block type * @param {number} index Destination index + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Array} New block id, New form data */ -export function addBlock(formData: any, type: string, index: number, blocksConfig: any): any[]; +export function addBlock(formData: any, type: string, index: number, blocksConfig: any, intl: Object): any[]; /** * Mutate block, changes the block @type * @function mutateBlock * @param {Object} formData Form data * @param {string} id Block uid to mutate * @param {number} value Block's new value + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Object} New form data */ -export function mutateBlock(formData: any, id: string, value: number, blocksConfig: any): any; +export function mutateBlock(formData: any, id: string, value: number, blocksConfig: any, intl: Object): any; /** * Insert new block before another block * @function insertBlock * @param {Object} formData Form data * @param {string} id Insert new block before the block with this id * @param {number} value New block's value + * @param {Object} blocksConfig Blocks configuration. + * @param {Object} intl intl object. * @return {Array} New block id, New form data */ -export function insertBlock(formData: any, id: string, value: number, current: {}, offset: number, blocksConfig: any): any[]; +export function insertBlock(formData: any, id: string, value: number, current: {}, offset: number, blocksConfig: any, intl: Object): any[]; /** * Change block * @function changeBlock @@ -167,11 +174,12 @@ export function findBlocks(blocks: {}, types: any, result?: any[]): any[]; */ export function moveBlockEnhanced(formData: any, { source, destination }: number): any; export function getBlocks(properties: any): any[]; -export function applyBlockInitialValue({ id, value, blocksConfig, formData, }: { +export function applyBlockInitialValue({ id, value, blocksConfig, formData, intl}: { id: any; value: any; blocksConfig: any; formData: any; + intl: Object; }): any; export function styleToClassName(key: any, value: any, prefix?: string): any; export function buildStyleClassNamesFromData(obj?: {}, prefix?: string): any; From f000a749bfb97708e682fa49f6eedb44dc3865ab Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 31 Jan 2025 08:57:33 +0100 Subject: [PATCH 09/19] Remove dangling file .registry.loader.js --- packages/volto/.registry.loader.js | 36 ------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 packages/volto/.registry.loader.js diff --git a/packages/volto/.registry.loader.js b/packages/volto/.registry.loader.js deleted file mode 100644 index a02285fadb..0000000000 --- a/packages/volto/.registry.loader.js +++ /dev/null @@ -1,36 +0,0 @@ -/* -This file is autogenerated. Don't change it directly. -Instead, change the "addons" setting in your package.json file. -*/ - -const addonsInfo = []; -export { addonsInfo }; - -const safeWrapper = (func) => (config) => { - const res = func(config); - if (typeof res === 'undefined') { - throw new Error("Configuration function doesn't return config"); - } - return res; -}; - -const projectConfigLoader = false; -const projectConfig = (config) => { - return projectConfigLoader && - typeof projectConfigLoader.default === 'function' - ? projectConfigLoader.default(config) - : config; -}; - -const load = (config) => { - const addonLoaders = []; - if (!addonLoaders.every((el) => typeof el === 'function')) { - throw new TypeError( - 'Each addon has to provide a function applying its configuration to the projects configuration.', - ); - } - return projectConfig( - addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config), - ); -}; -export default load; From 08b4499e559c607d075484851975f3c67da80dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Fri, 31 Jan 2025 12:48:16 +0100 Subject: [PATCH 10/19] Simplify Seven data fetching, add addons styles loader (#6636) --- .stylelintignore | 1 + apps/remix/app/root.tsx | 2 +- apps/rr7/app/root.tsx | 2 +- packages/seven/.stylelintrc | 14 + packages/seven/app/config.server.ts | 14 +- packages/seven/app/config.ts | 4 +- packages/seven/app/content.tsx | 53 +- packages/seven/app/loaders/content.ts | 64 + packages/seven/app/root.tsx | 87 +- packages/seven/app/utils.ts | 5 +- packages/seven/news/6636.feature | 2 + packages/seven/package.json | 3 +- packages/seven/public/favicon.ico | Bin 15086 -> 15086 bytes packages/seven/public/icon.svg | 13 + packages/seven/registry.config.ts | 2 +- packages/slots/.storybook/preview.ts | 2 +- packages/slots/news/6636.breaking | 1 + packages/slots/{ => styles}/main.css | 0 pnpm-lock.yaml | 3257 +++++++++++++++++++++++-- 19 files changed, 3202 insertions(+), 324 deletions(-) create mode 100644 packages/seven/.stylelintrc create mode 100644 packages/seven/app/loaders/content.ts create mode 100644 packages/seven/news/6636.feature create mode 100644 packages/seven/public/icon.svg create mode 100644 packages/slots/news/6636.breaking rename packages/slots/{ => styles}/main.css (100%) diff --git a/.stylelintignore b/.stylelintignore index 4b2be235a0..f52d70d0db 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -2,3 +2,4 @@ dist docs node_modules packages/registry/lib +build diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 5d49a06f99..670d6eada0 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -17,7 +17,7 @@ import PloneClient from '@plone/client'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import '@plone/components/dist/basic.css'; -import '@plone/slots/main.css'; +import '@plone/slots/styles/main.css'; import { flattenToAppURL } from './utils'; import { PloneProvider } from '@plone/providers'; diff --git a/apps/rr7/app/root.tsx b/apps/rr7/app/root.tsx index 7340c7627a..2373ad2d81 100644 --- a/apps/rr7/app/root.tsx +++ b/apps/rr7/app/root.tsx @@ -27,7 +27,7 @@ import installSSR from './config.server'; install(); import themingMain from '@plone/theming/styles/main.css?url'; -import slotsMain from '@plone/slots/main.css?url'; +import slotsMain from '@plone/slots/styles/main.css?url'; function useNavigate() { const navigate = useRRNavigate(); diff --git a/packages/seven/.stylelintrc b/packages/seven/.stylelintrc new file mode 100644 index 0000000000..8ac62f8d0f --- /dev/null +++ b/packages/seven/.stylelintrc @@ -0,0 +1,14 @@ +{ + "extends": ["stylelint-config-idiomatic-order"], + "plugins": ["stylelint-prettier"], + "overrides": [ + { + "files": ["**/*.scss"], + "customSyntax": "postcss-scss" + } + ], + "rules": { + "prettier/prettier": true, + "order/properties-alphabetical-order": null + } +} diff --git a/packages/seven/app/config.server.ts b/packages/seven/app/config.server.ts index 2d7c951f7d..81aebed5c3 100644 --- a/packages/seven/app/config.server.ts +++ b/packages/seven/app/config.server.ts @@ -3,18 +3,17 @@ */ import config from '@plone/registry'; import ploneClient from '@plone/client'; -import applyAddonConfiguration from '@plone/registry/addons-loader'; +// eslint-disable-next-line import/no-unresolved +import applyAddonConfiguration from '../registry.loader'; export default function install() { applyAddonConfiguration(config); config.settings.apiPath = - process.env.PLONE_API_PATH || 'http://localhost:3000'; - config.settings.internalApiPath = - process.env.PLONE_INTERNAL_API_PATH || undefined; + process.env.PLONE_API_PATH || 'http://localhost:8080/Plone'; const cli = ploneClient.initialize({ - apiPath: config.settings.internalApiPath || config.settings.apiPath, + apiPath: config.settings.apiPath, }); config.registerUtility({ @@ -24,9 +23,6 @@ export default function install() { }); console.log('API_PATH is:', config.settings.apiPath); - console.log( - 'INTERNAL_API_PATH is:', - config.settings.internalApiPath || 'not set', - ); + return config; } diff --git a/packages/seven/app/config.ts b/packages/seven/app/config.ts index 7f2026e538..3877c17e91 100644 --- a/packages/seven/app/config.ts +++ b/packages/seven/app/config.ts @@ -2,10 +2,10 @@ * This is the client side config entry point */ import config from '@plone/registry'; -import applyAddonConfiguration from '@plone/registry/addons-loader'; +// eslint-disable-next-line import/no-unresolved +import applyAddonConfiguration from '../registry.loader'; export default function install() { applyAddonConfiguration(config); - config.settings.apiPath = 'http://localhost:3000'; return config; } diff --git a/packages/seven/app/content.tsx b/packages/seven/app/content.tsx index 8358a163e0..86c803550a 100644 --- a/packages/seven/app/content.tsx +++ b/packages/seven/app/content.tsx @@ -1,55 +1,10 @@ -import type { Route } from './+types/content'; -import { data, useLoaderData, useLocation } from 'react-router'; -import PloneClient from '@plone/client'; +// import type { Route } from './+types/content'; +import { useRouteLoaderData, useLocation } from 'react-router'; +import type { Content } from '@plone/types'; import App from '@plone/slots/components/App'; -import config from '@plone/registry'; - -export const meta: Route.MetaFunction = ({ data }) => { - return [ - { title: data?.title }, - { name: 'description', content: data?.description }, - ]; -}; - -const expand = ['navroot', 'breadcrumbs', 'navigation']; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function loader({ params, request }: Route.LoaderArgs) { - const ploneClient = config - .getUtility({ - name: 'ploneClient', - type: 'client', - }) - .method(); - - const { getContent } = ploneClient as PloneClient; - - const path = new URL(request.url).pathname; - - if ( - !( - /^https?:\/\//.test(path) || - /^favicon.ico\/\//.test(path) || - /expand/.test(path) || - /\/@@images\//.test(path) || - /\/@@download\//.test(path) || - /^\/assets/.test(path) || - /\.(css|css\.map)$/.test(path) - ) - ) { - try { - return await getContent({ path, expand }); - } catch (error) { - throw data('Content Not Found', { status: 404 }); - } - } else { - console.log('matched path not fetched', path); - throw data('Content Not Found', { status: 404 }); - } -} export default function Content() { - const data = useLoaderData(); + const data = useRouteLoaderData('root') as Content; const pathname = useLocation().pathname; return ; diff --git a/packages/seven/app/loaders/content.ts b/packages/seven/app/loaders/content.ts new file mode 100644 index 0000000000..34ce9c9d92 --- /dev/null +++ b/packages/seven/app/loaders/content.ts @@ -0,0 +1,64 @@ +import type { Route } from '../+types/root'; +import { data } from 'react-router'; +import PloneClient from '@plone/client'; +import config from '@plone/registry'; +import type { Content } from '@plone/types'; + +const expand = ['navroot', 'breadcrumbs', 'navigation']; + +/** + * The definitive flattenToAppURL function + * Flattens all the URLs in the response to the current app URL + * This could be a potential use case for the upcoming RR7 middleware + */ +function flattenToAppURL(data: Content, request: Request) { + const currentUrl = new URL(request.url); + const baseUrl = `${currentUrl.protocol}//${currentUrl.host}`; + + // Convert data to string to perform replacements + let stringData = JSON.stringify(data); + + // Replace all occurrences of backend URLs + stringData = stringData.replace( + new RegExp(config.settings.apiPath, 'g'), + baseUrl, + ); + + // Parse back to object + return JSON.parse(stringData); +} + +export default async function loader({ params, request }: Route.LoaderArgs) { + const ploneClient = config + .getUtility({ + name: 'ploneClient', + type: 'client', + }) + .method(); + + const { getContent } = ploneClient as PloneClient; + + const path = `/${params['*'] || ''}`; + + if ( + !( + /^https?:\/\//.test(path) || + /^favicon.ico\/\//.test(path) || + /expand/.test(path) || + /\/@@images\//.test(path) || + /\/@@download\//.test(path) || + /^\/assets/.test(path) || + /\.(css|css\.map)$/.test(path) + ) + ) { + try { + return flattenToAppURL(await getContent({ path, expand }), request); + } catch (error) { + console.log(error); + throw data('Content Not Found', { status: 404 }); + } + } else { + console.log('matched path not fetched', path); + throw data('Content Not Found', { status: 404 }); + } +} diff --git a/packages/seven/app/root.tsx b/packages/seven/app/root.tsx index ad5ae56a73..067ccb2996 100644 --- a/packages/seven/app/root.tsx +++ b/packages/seven/app/root.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { isRouteErrorResponse, Links, @@ -11,23 +10,19 @@ import { useNavigate as useRRNavigate, useParams, useLoaderData, - useRouteLoaderData, } from 'react-router'; import type { Route } from './+types/root'; +import contentLoader from './loaders/content'; -import { QueryClient } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import PloneClient from '@plone/client'; -import { PloneProvider } from '@plone/providers'; +import { AppRouterProvider } from '@plone/providers'; import { flattenToAppURL } from './utils'; -import config from '@plone/registry'; import install from './config'; -import installSSR from './config.server'; +import installServer from './config.server'; install(); -import themingMain from '@plone/theming/styles/main.css?url'; -import slotsMain from '@plone/slots/main.css?url'; +// eslint-disable-next-line import/no-unresolved +import stylesheet from '../addons.styles.css?url'; function useNavigate() { const navigate = useRRNavigate(); @@ -38,15 +33,17 @@ function useHrefLocal(to: string) { return useHref(flattenToAppURL(to) || ''); } -export const meta: Route.MetaFunction = () => [ +export const meta: Route.MetaFunction = ({ data }) => [ + { title: data?.title }, + { name: 'description', content: data?.description }, { name: 'generator', content: 'Plone 7 - https://plone.org' }, ]; export const links: Route.LinksFunction = () => [ { rel: 'icon', - href: '/favicon.png', - type: 'image/png', + href: '/favicon.ico', + type: 'image/x-icon', sizes: 'any', }, { @@ -54,8 +51,7 @@ export const links: Route.LinksFunction = () => [ href: '/icon.svg', type: 'image/svg+xml', }, - { rel: 'stylesheet', href: themingMain }, - { rel: 'stylesheet', href: slotsMain }, + { rel: 'stylesheet', href: stylesheet }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', @@ -68,25 +64,17 @@ export const links: Route.LinksFunction = () => [ }, ]; -export async function loader() { - const ssrConfig = installSSR(); +export async function loader({ params, request }: Route.LoaderArgs) { + installServer(); - return { - env: { - PLONE_API_PATH: ssrConfig.settings.apiPath, - PLONE_INTERNAL_API_PATH: ssrConfig.settings.internalApiPath, - }, - }; + return await contentLoader({ params, request }); } export function Layout({ children }: { children: React.ReactNode }) { const data = useLoaderData(); - const indexLoaderData = useRouteLoaderData('index'); - const contentLoaderData = useRouteLoaderData('content'); - const contentData = indexLoaderData || contentLoaderData; return ( - + @@ -99,11 +87,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
    {children}