From d8b1eb5d817365a94e91d61b159faf14e45f228b Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 16 Feb 2024 22:39:25 +0100 Subject: [PATCH 01/68] API support for slots in registry --- packages/registry/src/index.ts | 74 +++++++++++++++++++++++++ packages/registry/src/registry.test.jsx | 23 ++++++++ packages/types/src/config/index.d.ts | 8 ++- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 6630ad5ff2..aca3a98a2d 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -5,6 +5,7 @@ import type { ComponentsConfig, ExperimentalConfig, SettingsConfig, + Slot, SlotsConfig, ViewsConfig, WidgetsConfig, @@ -179,6 +180,79 @@ class Config { } } } + + getSlot( + options: { name: string; dependencies: string[] | string } | string, + ): Slot | undefined { + if (typeof options === 'object') { + const { name, dependencies = '' } = options; + let depsString: string = ''; + if (dependencies && Array.isArray(dependencies)) { + depsString = dependencies.join('+'); + } else if (typeof dependencies === 'string') { + depsString = dependencies; + } + const slotComponentName = `${name}${depsString ? `|${depsString}` : ''}`; + const currentSlot = this._data.slots[name]; + + return currentSlot.get(slotComponentName); + } else { + // Shortcut notation, accepting a lonely string as argument + const name = options; + const currentSlot = this._data.slots[name]; + return currentSlot.get(name); + } + } + + registerSlot(options: { + name: string; + dependencies: string[] | string; + component: React.ComponentType; + route: string; + }) { + const { name, component, dependencies = '', route } = options; + let depsString: string = ''; + if (!component) { + throw new Error('No component provided'); + } else { + if (dependencies && Array.isArray(dependencies)) { + depsString = dependencies.join('+'); + } else if (typeof dependencies === 'string') { + depsString = dependencies; + } + const slotComponentName = `${name}${depsString ? `|${depsString}` : ''}`; + + let currentSlot = this._data.slots[name]; + if (!currentSlot) { + this._data.slots[name] = new Map(); + currentSlot = this._data.slots[name]; + } + + currentSlot.set(slotComponentName, { + component, + dependencies, + route, + }); + // Try to set a displayName (useful for React dev tools) for the registered component + // Only if it's a function and it's not set previously + try { + const displayName = + currentSlot.get(slotComponentName)!.component.displayName; + + if ( + !displayName && + typeof currentSlot.get(slotComponentName)?.component === 'function' + ) { + currentSlot.get(slotComponentName)!.component.displayName = name; + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + `Not setting the slot component displayName because ${error}`, + ); + } + } + } } const instance = new Config(); diff --git a/packages/registry/src/registry.test.jsx b/packages/registry/src/registry.test.jsx index 861d26701f..b56b8fe575 100644 --- a/packages/registry/src/registry.test.jsx +++ b/packages/registry/src/registry.test.jsx @@ -7,6 +7,8 @@ config.set('components', { 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, }); +config.set('slots', {}); + describe('registry', () => { it('get components', () => { expect(config.getComponent('Toolbar').component).toEqual( @@ -109,4 +111,25 @@ describe('registry', () => { }).component, ).toEqual('this is a Bar component'); }); + + it('registers a slot', () => { + config.registerSlot({ + name: 'aboveContent', + component: 'this is a Bar component', + dependencies: ['News Item'], + route: '/folder/path', + }); + expect( + config.getSlot({ + name: 'aboveContent', + dependencies: 'News Item', + }).component, + ).toEqual('this is a Bar component'); + expect( + config.getSlot({ + name: 'aboveContent', + dependencies: 'News Item', + }).route, + ).toEqual('/folder/path'); + }); }); diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 02df192e63..26b30edfb7 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -11,7 +11,13 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; -export type SlotsConfig = Record; +export type Slot = { + component: React.ComponentType; + route: string; + dependencies: string[] | string; +}; + +export type SlotsConfig = Record>; export type ComponentsConfig = Record< string, From fa3978254d37d695865c85393de3d992f7280044 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 16 Feb 2024 22:39:49 +0100 Subject: [PATCH 02/68] WIP SlotRenderer --- .../theme/SlotRenderer/SlotRenderer.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx new file mode 100644 index 0000000000..a3cb310275 --- /dev/null +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -0,0 +1,40 @@ +import { matchPath, useLocation } from 'react-router-dom'; +import { v4 as uuid } from 'uuid'; +import config from '@plone/volto/registry'; +import type { Content, Slot } from '@plone/types'; + +const SlotRenderer = ({ + name, + content = {}, +}: { + name: string; + content: Content; +}) => { + const pathname = useLocation().pathname; + + // First I ask for the slots registered per dependencies + let slots = config.getSlot({ + name, + dependencies: content['@type'], + }); + + if (!slots) { + slots = config.getSlot({ name }); + } + + if (!slots) { + return null; + } + + const active = slots.filter((slot: Slot) => + matchPath(pathname, { path: slot.route, exact: slot.exact }), + ); + + return active.map(({ component, props }: Slot) => { + const id = uuid(); + const SlotComponent = component; + return ; + }); +}; + +export default SlotRenderer; From 24babd7b34c9cfc306c25e5c2ead800f6d05c65d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 20 Feb 2024 13:07:21 +0100 Subject: [PATCH 03/68] First working implementation --- packages/registry/src/index.ts | 91 ++++---- packages/registry/src/registry.test.jsx | 197 ++++++++++++++++-- packages/types/src/config/index.d.ts | 14 +- .../theme/SlotRenderer/SlotRenderer.tsx | 29 +-- 4 files changed, 243 insertions(+), 88 deletions(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index aca3a98a2d..273429b4c2 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -5,7 +5,7 @@ import type { ComponentsConfig, ExperimentalConfig, SettingsConfig, - Slot, + SlotComponent, SlotsConfig, ViewsConfig, WidgetsConfig, @@ -181,69 +181,72 @@ class Config { } } - getSlot( - options: { name: string; dependencies: string[] | string } | string, - ): Slot | undefined { - if (typeof options === 'object') { - const { name, dependencies = '' } = options; - let depsString: string = ''; - if (dependencies && Array.isArray(dependencies)) { - depsString = dependencies.join('+'); - } else if (typeof dependencies === 'string') { - depsString = dependencies; + getSlot(name: string, args: T): SlotComponent['component'][] | undefined { + const { slots, data } = this._data.slots[name]; + const slotComponents = []; + // For all enabled slots + for (const slotName of slots) { + // For all registered components for that slot, inversed, since the last one registered wins + // TODO: Cover ZCA use case, where if more predicates, more specificity wins if all true. + // Let's keep it simple here and stick to the registered order. + for (const slotComponent of data[slotName].toReversed()) { + const isPredicateTrueFound = slotComponent.predicates.reduce( + (acc, value) => acc && value(args), + true, + ); + // If all the predicates are truthy + if (isPredicateTrueFound) { + slotComponents.push(slotComponent.component); + break; + } } - const slotComponentName = `${name}${depsString ? `|${depsString}` : ''}`; - const currentSlot = this._data.slots[name]; - - return currentSlot.get(slotComponentName); - } else { - // Shortcut notation, accepting a lonely string as argument - const name = options; - const currentSlot = this._data.slots[name]; - return currentSlot.get(name); } + + return slotComponents; } - registerSlot(options: { + registerSlotComponent(options: { + slot: string; name: string; - dependencies: string[] | string; + predicates: ((...args: unknown[]) => boolean)[]; component: React.ComponentType; - route: string; }) { - const { name, component, dependencies = '', route } = options; - let depsString: string = ''; + const { name, component, predicates, slot } = options; + if (!component) { throw new Error('No component provided'); } else { - if (dependencies && Array.isArray(dependencies)) { - depsString = dependencies.join('+'); - } else if (typeof dependencies === 'string') { - depsString = dependencies; - } - const slotComponentName = `${name}${depsString ? `|${depsString}` : ''}`; - - let currentSlot = this._data.slots[name]; + let currentSlot = this._data.slots[slot]; if (!currentSlot) { - this._data.slots[name] = new Map(); - currentSlot = this._data.slots[name]; + this._data.slots[slot] = { + slots: [], + data: {}, + }; + currentSlot = this._data.slots[slot]; + } + if (!currentSlot.data[name]) { + currentSlot.data[name] = []; } - currentSlot.set(slotComponentName, { + const currentSlotComponent = currentSlot.data[name]; + if (!currentSlot.slots.includes(name)) { + currentSlot.slots.push(name); + } + const slotComponentData = { component, - dependencies, - route, - }); + predicates, + }; + // Try to set a displayName (useful for React dev tools) for the registered component // Only if it's a function and it's not set previously try { - const displayName = - currentSlot.get(slotComponentName)!.component.displayName; + const displayName = slotComponentData.component.displayName; if ( !displayName && - typeof currentSlot.get(slotComponentName)?.component === 'function' + typeof slotComponentData?.component === 'function' ) { - currentSlot.get(slotComponentName)!.component.displayName = name; + slotComponentData.component.displayName = name; } } catch (error) { // eslint-disable-next-line no-console @@ -251,6 +254,8 @@ class Config { `Not setting the slot component displayName because ${error}`, ); } + + currentSlotComponent.push(slotComponentData); } } } diff --git a/packages/registry/src/registry.test.jsx b/packages/registry/src/registry.test.jsx index b56b8fe575..e57eb7aed4 100644 --- a/packages/registry/src/registry.test.jsx +++ b/packages/registry/src/registry.test.jsx @@ -1,5 +1,5 @@ import config from './index'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, afterEach } from 'vitest'; config.set('components', { Toolbar: { component: 'this is the Toolbar component' }, @@ -9,7 +9,7 @@ config.set('components', { config.set('slots', {}); -describe('registry', () => { +describe('Component registry', () => { it('get components', () => { expect(config.getComponent('Toolbar').component).toEqual( 'this is the Toolbar component', @@ -111,25 +111,182 @@ describe('registry', () => { }).component, ).toEqual('this is a Bar component'); }); +}); - it('registers a slot', () => { - config.registerSlot({ - name: 'aboveContent', - component: 'this is a Bar component', - dependencies: ['News Item'], - route: '/folder/path', +describe.only('Slots registry', () => { + // config.slots.toolbar = [ // viewlets.xml + // 'save', + // 'edit', + // ] + + // config.slots.toolbar.save = [ + // { + // predicates: [RouteCondition('/de')], + // component: 'this is a Bar component', + // }, + // { + // predicates: [RouteCondition('/de'), ContentTypeCondition(['News Item'])], + // component: 'this is a Bar component', + // }, + // { + // predicates: [RouteCondition('/de/path/folder'), ContentTypeCondition(['News Item']), TrueCondition()], + // component: 'this is a Bar component', + // }, + // ] + + afterEach(() => { + config.set('slots', {}); + }); + + // type Predicate = (predicateValues: unknown) = (predicateValues, args) => boolean + const RouteConditionTrue = () => () => true; + const RouteConditionFalse = () => () => false; + const ContentTypeConditionTrue = () => () => true; + const ContentTypeConditionFalse = () => () => false; + + it('registers two slot components with predicates - registered components order is respected', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: + 'this is a toolbar component with only a truth-ish route condition', + predicates: [RouteConditionTrue('/de')], }); - expect( - config.getSlot({ - name: 'aboveContent', - dependencies: 'News Item', - }).component, - ).toEqual('this is a Bar component'); - expect( - config.getSlot({ - name: 'aboveContent', - dependencies: 'News Item', - }).route, - ).toEqual('/folder/path'); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a Bar component with a false predicate and one true', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar')).toEqual([ + 'this is a toolbar component with only a truth-ish route condition', + ]); + }); + + it('registers two slot components with predicates - All registered components predicates are truthy, the last one registered wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: + 'this is a toolbar component with only a truth-ish route condition', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar')).toEqual([ + 'this is a toolbar component with two truth-ish predicates', + ]); + }); + + it('registers two slot components with predicates - No registered component have a truthy predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + expect(config.getSlot('toolbar')).toEqual([]); + }); + + it('registers 2 + 2 slot components with predicates - No registered component have a truthy predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + expect(config.getSlot('toolbar')).toEqual([]); + }); + + it('registers 2 + 2 slot components with predicates - One truthy predicate per set', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlot('toolbar')).toEqual([ + 'this is a toolbar save component with a true predicate', + 'this is a toolbar edit component with true predicate', + ]); }); }); diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 26b30edfb7..a4c5365487 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -11,13 +11,17 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; -export type Slot = { - component: React.ComponentType; - route: string; - dependencies: string[] | string; +export type SlotComponent = { + component: React.ComponentType; + predicates: ((...args: any[]) => boolean)[]; +}; + +export type SlotManager = { + slots: string[]; + data: Record; }; -export type SlotsConfig = Record>; +export type SlotsConfig = Record; export type ComponentsConfig = Record< string, diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index a3cb310275..dc0c5315f1 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -1,39 +1,28 @@ -import { matchPath, useLocation } from 'react-router-dom'; import { v4 as uuid } from 'uuid'; import config from '@plone/volto/registry'; -import type { Content, Slot } from '@plone/types'; +import type { Content, SlotComponent } from '@plone/types'; + +``` + +```; const SlotRenderer = ({ name, - content = {}, + content, }: { name: string; content: Content; }) => { - const pathname = useLocation().pathname; - - // First I ask for the slots registered per dependencies - let slots = config.getSlot({ - name, - dependencies: content['@type'], - }); - - if (!slots) { - slots = config.getSlot({ name }); - } + let slots = config.getSlot<{ content: Content }>(name) as SlotComponent[]; if (!slots) { return null; } - const active = slots.filter((slot: Slot) => - matchPath(pathname, { path: slot.route, exact: slot.exact }), - ); - - return active.map(({ component, props }: Slot) => { + return slots.map(({ component }: SlotComponent) => { const id = uuid(); const SlotComponent = component; - return ; + return ; }); }; From b58a9777490504d0f41a081f7060fa028632f583 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 20 Feb 2024 14:13:00 +0100 Subject: [PATCH 04/68] Improve SlotRenderer --- .../src/components/theme/SlotRenderer/SlotRenderer.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index dc0c5315f1..43cfa197f2 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -1,10 +1,10 @@ import { v4 as uuid } from 'uuid'; import config from '@plone/volto/registry'; -import type { Content, SlotComponent } from '@plone/types'; +import type { Content } from '@plone/types'; -``` +/* -```; +*/ const SlotRenderer = ({ name, @@ -13,13 +13,13 @@ const SlotRenderer = ({ name: string; content: Content; }) => { - let slots = config.getSlot<{ content: Content }>(name) as SlotComponent[]; + let slots = config.getSlot<{ content: Content }>(name, { content }); if (!slots) { return null; } - return slots.map(({ component }: SlotComponent) => { + return slots.map((component) => { const id = uuid(); const SlotComponent = component; return ; From 9f5e56c12890b13c0deae79a281b9d3c0fa8dd18 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 20 Feb 2024 14:13:10 +0100 Subject: [PATCH 05/68] Changelog --- packages/registry/news/5775.feature | 1 + packages/types/news/5775.feature | 1 + packages/volto/news/5775.feature | 1 + 3 files changed, 3 insertions(+) create mode 100644 packages/registry/news/5775.feature create mode 100644 packages/types/news/5775.feature create mode 100644 packages/volto/news/5775.feature diff --git a/packages/registry/news/5775.feature b/packages/registry/news/5775.feature new file mode 100644 index 0000000000..d3b2b6f4be --- /dev/null +++ b/packages/registry/news/5775.feature @@ -0,0 +1 @@ +Support for slots @tiberiuichim @sneridagh diff --git a/packages/types/news/5775.feature b/packages/types/news/5775.feature new file mode 100644 index 0000000000..58fafd2629 --- /dev/null +++ b/packages/types/news/5775.feature @@ -0,0 +1 @@ +Support for slots @sneridagh diff --git a/packages/volto/news/5775.feature b/packages/volto/news/5775.feature new file mode 100644 index 0000000000..58fafd2629 --- /dev/null +++ b/packages/volto/news/5775.feature @@ -0,0 +1 @@ +Support for slots @sneridagh From 81dfe6121196cd7ec279c246158b4c0adad8eda2 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 20 Feb 2024 15:49:37 +0100 Subject: [PATCH 06/68] Improve and test as TS --- packages/registry/src/index.ts | 9 ++++----- .../{registry.test.jsx => registry.test.tsx} | 20 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) rename packages/registry/src/{registry.test.jsx => registry.test.tsx} (92%) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index cd6cb82a90..5898a2af8f 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -126,7 +126,7 @@ class Config { } getComponent( - options: { name: string; dependencies: string[] | string } | string, + options: { name: string; dependencies?: string[] | string } | string, ): GetComponentResult { if (typeof options === 'object') { const { name, dependencies = '' } = options; @@ -148,7 +148,7 @@ class Config { registerComponent(options: { name: string; - dependencies: string[] | string; + dependencies?: string[] | string; component: React.ComponentType; }) { const { name, component, dependencies = '' } = options; @@ -192,9 +192,8 @@ class Config { // TODO: Cover ZCA use case, where if more predicates, more specificity wins if all true. // Let's keep it simple here and stick to the registered order. for (const slotComponent of data[slotName].toReversed()) { - const isPredicateTrueFound = slotComponent.predicates.reduce( - (acc, value) => acc && value(args), - true, + const isPredicateTrueFound = slotComponent.predicates.every( + (predicate) => predicate(args), ); // If all the predicates are truthy if (isPredicateTrueFound) { diff --git a/packages/registry/src/registry.test.jsx b/packages/registry/src/registry.test.tsx similarity index 92% rename from packages/registry/src/registry.test.jsx rename to packages/registry/src/registry.test.tsx index e57eb7aed4..cb6e38689c 100644 --- a/packages/registry/src/registry.test.jsx +++ b/packages/registry/src/registry.test.tsx @@ -139,10 +139,14 @@ describe.only('Slots registry', () => { }); // type Predicate = (predicateValues: unknown) = (predicateValues, args) => boolean - const RouteConditionTrue = () => () => true; - const RouteConditionFalse = () => () => false; - const ContentTypeConditionTrue = () => () => true; - const ContentTypeConditionFalse = () => () => false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const RouteConditionTrue = (route) => () => true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const RouteConditionFalse = (route) => () => false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ContentTypeConditionTrue = (contentType) => () => true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ContentTypeConditionFalse = (contentType) => () => false; it('registers two slot components with predicates - registered components order is respected', () => { config.registerSlotComponent({ @@ -187,7 +191,7 @@ describe.only('Slots registry', () => { ], }); - expect(config.getSlot('toolbar')).toEqual([ + expect(config.getSlot('toolbar', {})).toEqual([ 'this is a toolbar component with two truth-ish predicates', ]); }); @@ -210,7 +214,7 @@ describe.only('Slots registry', () => { ], }); - expect(config.getSlot('toolbar')).toEqual([]); + expect(config.getSlot('toolbar', {})).toEqual([]); }); it('registers 2 + 2 slot components with predicates - No registered component have a truthy predicate', () => { @@ -247,7 +251,7 @@ describe.only('Slots registry', () => { ContentTypeConditionFalse(['News Item']), ], }); - expect(config.getSlot('toolbar')).toEqual([]); + expect(config.getSlot('toolbar', {})).toEqual([]); }); it('registers 2 + 2 slot components with predicates - One truthy predicate per set', () => { @@ -284,7 +288,7 @@ describe.only('Slots registry', () => { ContentTypeConditionTrue(['News Item']), ], }); - expect(config.getSlot('toolbar')).toEqual([ + expect(config.getSlot('toolbar', {})).toEqual([ 'this is a toolbar save component with a true predicate', 'this is a toolbar edit component with true predicate', ]); From c2d859f91df3e36e5c9db5f2d30c78d14ff5792b Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 20 Feb 2024 16:53:06 +0100 Subject: [PATCH 07/68] Better implementation, improve tests --- packages/registry/src/index.ts | 103 +++++++++++++++--------- packages/registry/src/registry.test.tsx | 82 ++++++++++++++++++- packages/types/src/config/index.d.ts | 2 +- 3 files changed, 144 insertions(+), 43 deletions(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 5898a2af8f..7e99fae1aa 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -191,16 +191,30 @@ class Config { // For all registered components for that slot, inversed, since the last one registered wins // TODO: Cover ZCA use case, where if more predicates, more specificity wins if all true. // Let's keep it simple here and stick to the registered order. + let noPredicateComponent: SlotComponent | undefined; for (const slotComponent of data[slotName].toReversed()) { - const isPredicateTrueFound = slotComponent.predicates.every( - (predicate) => predicate(args), - ); + let isPredicateTrueFound: boolean = false; + if (slotComponent.predicates) { + isPredicateTrueFound = slotComponent.predicates.every((predicate) => + predicate(args), + ); + } else { + // We mark the one with no predicates + noPredicateComponent = slotComponent; + } + // If all the predicates are truthy if (isPredicateTrueFound) { slotComponents.push(slotComponent.component); + // We "reset" the marker, we already found a candidate + noPredicateComponent = undefined; break; } } + + if (noPredicateComponent) { + slotComponents.push(noPredicateComponent.component); + } } return slotComponents; @@ -209,55 +223,64 @@ class Config { registerSlotComponent(options: { slot: string; name: string; - predicates: ((...args: unknown[]) => boolean)[]; + predicates?: ((...args: unknown[]) => boolean)[]; component: React.ComponentType; }) { const { name, component, predicates, slot } = options; if (!component) { throw new Error('No component provided'); - } else { - let currentSlot = this._data.slots[slot]; - if (!currentSlot) { - this._data.slots[slot] = { - slots: [], - data: {}, - }; - currentSlot = this._data.slots[slot]; - } - if (!currentSlot.data[name]) { - currentSlot.data[name] = []; + } + if (!predicates) { + // Test if there's already one registered, we only support one + const hasRegisteredNoPredicatesComponent = this._data.slots?.[ + slot + ]?.data?.[name]?.find(({ predicates }) => !predicates); + console.log(hasRegisteredNoPredicatesComponent); + if (hasRegisteredNoPredicatesComponent) { + throw new Error( + `There is already registered a component ${name} for the slot ${slot}. You can only register one slot component with no predicates per slot.`, + ); } + } - const currentSlotComponent = currentSlot.data[name]; - if (!currentSlot.slots.includes(name)) { - currentSlot.slots.push(name); - } - const slotComponentData = { - component, - predicates, + let currentSlot = this._data.slots[slot]; + if (!currentSlot) { + this._data.slots[slot] = { + slots: [], + data: {}, }; + currentSlot = this._data.slots[slot]; + } + if (!currentSlot.data[name]) { + currentSlot.data[name] = []; + } - // Try to set a displayName (useful for React dev tools) for the registered component - // Only if it's a function and it's not set previously - try { - const displayName = slotComponentData.component.displayName; - - if ( - !displayName && - typeof slotComponentData?.component === 'function' - ) { - slotComponentData.component.displayName = name; - } - } catch (error) { - // eslint-disable-next-line no-console - console.warn( - `Not setting the slot component displayName because ${error}`, - ); + const currentSlotComponent = currentSlot.data[name]; + if (!currentSlot.slots.includes(name)) { + currentSlot.slots.push(name); + } + const slotComponentData = { + component, + predicates, + }; + + // Try to set a displayName (useful for React dev tools) for the registered component + // Only if it's a function and it's not set previously + try { + const displayName = slotComponentData.component.displayName; + + if (!displayName && typeof slotComponentData?.component === 'function') { + slotComponentData.component.displayName = name; } - - currentSlotComponent.push(slotComponentData); + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + `Not setting the slot component displayName because ${error}`, + ); } + + currentSlotComponent.push(slotComponentData); } } diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index cb6e38689c..14daabb6a6 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -113,7 +113,7 @@ describe('Component registry', () => { }); }); -describe.only('Slots registry', () => { +describe('Slots registry', () => { // config.slots.toolbar = [ // viewlets.xml // 'save', // 'edit', @@ -148,6 +148,18 @@ describe.only('Slots registry', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const ContentTypeConditionFalse = (contentType) => () => false; + it('registers a single slot component with no predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + expect(config.getSlot('toolbar', {})).toEqual([ + 'this is a toolbar component with no predicate', + ]); + }); + it('registers two slot components with predicates - registered components order is respected', () => { config.registerSlotComponent({ slot: 'toolbar', @@ -167,7 +179,7 @@ describe.only('Slots registry', () => { ], }); - expect(config.getSlot('toolbar')).toEqual([ + expect(config.getSlot('toolbar', {})).toEqual([ 'this is a toolbar component with only a truth-ish route condition', ]); }); @@ -217,6 +229,72 @@ describe.only('Slots registry', () => { expect(config.getSlot('toolbar', {})).toEqual([]); }); + it('registers two slot components one without predicates - registered component with predicates are truthy, the last one registered wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})).toEqual([ + 'this is a toolbar component with two truth-ish predicates', + ]); + }); + + it('registers two slot components one without predicates - registered components predicates are falsy, the one with no predicates wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})).toEqual([ + 'this is a toolbar component with no predicate', + ]); + }); + + it('registers two slot components one without predicates - registered components predicates are truthy, the one with predicates wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + expect(config.getSlot('toolbar', {})).toEqual([ + 'this is a toolbar component with two truth-ish predicates', + ]); + }); + it('registers 2 + 2 slot components with predicates - No registered component have a truthy predicate', () => { config.registerSlotComponent({ slot: 'toolbar', diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index a4c5365487..c77c510261 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -13,7 +13,7 @@ export type AddonRoutesConfig = { export type SlotComponent = { component: React.ComponentType; - predicates: ((...args: any[]) => boolean)[]; + predicates?: ((...args: any[]) => boolean)[]; }; export type SlotManager = { From 3a5d56bf3022543323d68982bffd1959f40b7fa8 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 21 Feb 2024 18:29:42 +0100 Subject: [PATCH 08/68] Add coresandbox example, adjust here and there --- .../src/components/Slots/SlotTest.tsx | 12 ++++++++++ packages/coresandbox/src/index.ts | 10 +++++++++ packages/registry/src/index.ts | 11 ++++++---- packages/types/src/config/index.d.ts | 4 +++- packages/volto/package.json | 1 + .../theme/SlotRenderer/SlotRenderer.tsx | 22 ++++++++++++++----- .../volto/src/components/theme/View/View.jsx | 3 +++ packages/volto/src/config/index.js | 2 ++ packages/volto/src/helpers/Slots/index.tsx | 12 ++++++++++ packages/volto/src/helpers/index.js | 1 + pnpm-lock.yaml | 22 +++++++++++++++++++ 11 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 packages/coresandbox/src/components/Slots/SlotTest.tsx create mode 100644 packages/volto/src/helpers/Slots/index.tsx diff --git a/packages/coresandbox/src/components/Slots/SlotTest.tsx b/packages/coresandbox/src/components/Slots/SlotTest.tsx new file mode 100644 index 0000000000..affaf5b019 --- /dev/null +++ b/packages/coresandbox/src/components/Slots/SlotTest.tsx @@ -0,0 +1,12 @@ +import { Container } from 'semantic-ui-react'; + +const SlotComponentTest = () => { + return ( + +

This is a test slot component

+

It should appear above the Content

+
+ ); +}; + +export default SlotComponentTest; diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index 1f4527a27c..760bd1983c 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -9,6 +9,9 @@ import { conditionalVariationsSchemaEnhancer } from './components/Blocks/schemaE import codeSVG from '@plone/volto/icons/code.svg'; import type { BlockConfigBase } from '@plone/types'; import type { ConfigType } from '@plone/registry'; +import SlotComponentTest from './components/Slots/SlotTest'; +import { ContentTypeCondition } from '@plone/volto/helpers'; +import { RouteCondition } from '@plone/volto/helpers/Slots'; const testBlock: BlockConfigBase = { id: 'testBlock', @@ -171,6 +174,13 @@ const applyConfig = (config: ConfigType) => { config.blocks.blocksConfig.listing = listing(config); config.views.contentTypesViews.Folder = NewsAndEvents; + config.registerSlotComponent({ + slot: 'aboveContent', + name: 'hello.aboveFooter', + component: SlotComponentTest, + predicates: [ContentTypeCondition(['Document']), RouteCondition('/hello')], + }); + return config; }; diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 7e99fae1aa..f367c018b5 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -6,6 +6,7 @@ import type { ExperimentalConfig, SettingsConfig, SlotComponent, + SlotPredicate, SlotsConfig, ViewsConfig, WidgetsConfig, @@ -184,6 +185,9 @@ class Config { } getSlot(name: string, args: T): SlotComponent['component'][] | undefined { + if (!this._data.slots[name]) { + return; + } const { slots, data } = this._data.slots[name]; const slotComponents = []; // For all enabled slots @@ -223,9 +227,9 @@ class Config { registerSlotComponent(options: { slot: string; name: string; - predicates?: ((...args: unknown[]) => boolean)[]; - component: React.ComponentType; - }) { + predicates?: SlotPredicate[]; + component: SlotComponent['component']; + }): void { const { name, component, predicates, slot } = options; if (!component) { @@ -236,7 +240,6 @@ class Config { const hasRegisteredNoPredicatesComponent = this._data.slots?.[ slot ]?.data?.[name]?.find(({ predicates }) => !predicates); - console.log(hasRegisteredNoPredicatesComponent); if (hasRegisteredNoPredicatesComponent) { throw new Error( `There is already registered a component ${name} for the slot ${slot}. You can only register one slot component with no predicates per slot.`, diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index c77c510261..6a0dc0e0fc 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -11,9 +11,11 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; +export type SlotPredicate = (args: any) => boolean; + export type SlotComponent = { component: React.ComponentType; - predicates?: ((...args: any[]) => boolean)[]; + predicates?: SlotPredicate[]; }; export type SlotManager = { diff --git a/packages/volto/package.json b/packages/volto/package.json index ac60b897ff..55a414518a 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -367,6 +367,7 @@ "@types/lodash": "^4.14.201", "@types/react": "^17.0.52", "@types/react-dom": "^17", + "@types/react-router-dom": "^5.3.3", "@types/react-test-renderer": "18.0.1", "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "6.7.0", diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index 43cfa197f2..fda6366739 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -1,5 +1,6 @@ import { v4 as uuid } from 'uuid'; import config from '@plone/volto/registry'; +import { useLocation } from 'react-router-dom'; import type { Content } from '@plone/types'; /* @@ -13,17 +14,26 @@ const SlotRenderer = ({ name: string; content: Content; }) => { - let slots = config.getSlot<{ content: Content }>(name, { content }); + const pathname = useLocation().pathname; + + let slots = config.getSlot<{ content: Content; pathname: string }>(name, { + content, + pathname, + }); if (!slots) { return null; } - return slots.map((component) => { - const id = uuid(); - const SlotComponent = component; - return ; - }); + return ( + <> + {slots.map((component) => { + const id = uuid(); + const SlotComponent = component as React.ElementType; + return ; + })} + + ); }; export default SlotRenderer; diff --git a/packages/volto/src/components/theme/View/View.jsx b/packages/volto/src/components/theme/View/View.jsx index e836e452ab..ae9d97b53b 100644 --- a/packages/volto/src/components/theme/View/View.jsx +++ b/packages/volto/src/components/theme/View/View.jsx @@ -28,6 +28,7 @@ import { } from '@plone/volto/helpers'; import config from '@plone/volto/registry'; +import SlotRenderer from '../SlotRenderer/SlotRenderer'; /** * View container class. @@ -244,6 +245,7 @@ class View extends Component { : null } /> + + {config.settings.showTags && this.props.content.subjects && this.props.content.subjects.length > 0 && ( diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js index 8c295e5a1c..b417abc27d 100644 --- a/packages/volto/src/config/index.js +++ b/packages/volto/src/config/index.js @@ -223,6 +223,7 @@ let config = { }, addonRoutes: [], addonReducers: {}, + slots: {}, components, }; @@ -250,5 +251,6 @@ ConfigRegistry.widgets = config.widgets; ConfigRegistry.addonRoutes = config.addonRoutes; ConfigRegistry.addonReducers = config.addonReducers; ConfigRegistry.components = config.components; +ConfigRegistry.slots = config.slots; applyAddonConfiguration(ConfigRegistry); diff --git a/packages/volto/src/helpers/Slots/index.tsx b/packages/volto/src/helpers/Slots/index.tsx new file mode 100644 index 0000000000..9021485123 --- /dev/null +++ b/packages/volto/src/helpers/Slots/index.tsx @@ -0,0 +1,12 @@ +import type { Content } from '@plone/types'; +import { matchPath } from 'react-router-dom'; + +export function RouteCondition(path: string, exact?: boolean) { + return ({ pathname }: { pathname: string }) => + Boolean(matchPath(pathname, { path, exact })); +} + +export function ContentTypeCondition(contentType: string[]) { + return ({ content }: { content: Content }) => + contentType.includes(content['@type']); +} diff --git a/packages/volto/src/helpers/index.js b/packages/volto/src/helpers/index.js index e67c383e4e..99cf5aa21e 100644 --- a/packages/volto/src/helpers/index.js +++ b/packages/volto/src/helpers/index.js @@ -129,3 +129,4 @@ export { getWorkflowOptions, } from './Workflows/Workflows'; export { getSiteAsyncPropExtender } from './Site'; +export { ContentTypeCondition } from './Slots'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a037a4f84f..a1ee1a272b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1873,6 +1873,9 @@ importers: '@types/react-dom': specifier: ^17 version: 17.0.23 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@types/react-test-renderer': specifier: 18.0.1 version: 18.0.1 @@ -17158,6 +17161,10 @@ packages: '@types/unist': 2.0.10 dev: true + /@types/history@4.7.11: + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + dev: true + /@types/hoist-non-react-statics@3.3.5: resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} dependencies: @@ -17391,6 +17398,21 @@ packages: redux: 4.1.0 dev: true + /@types/react-router-dom@5.3.3: + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.55 + '@types/react-router': 5.1.20 + dev: true + + /@types/react-router@5.1.20: + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.2.55 + dev: true + /@types/react-syntax-highlighter@11.0.5: resolution: {integrity: sha512-VIOi9i2Oj5XsmWWoB72p3KlZoEbdRAcechJa8Ztebw7bDl2YmR+odxIqhtJGp1q2EozHs02US+gzxJ9nuf56qg==} dependencies: From efc4805909a210926a12e630ab5e989bac781751 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 21 Feb 2024 18:37:22 +0100 Subject: [PATCH 09/68] Add ignore --- .../volto/src/components/theme/SlotRenderer/SlotRenderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index fda6366739..6d4a884957 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -30,6 +30,7 @@ const SlotRenderer = ({ {slots.map((component) => { const id = uuid(); const SlotComponent = component as React.ElementType; + //@ts-ignore - Probably related to an old @types/react dep :( return ; })} From 6bf883ee49f4feb6552d6f66ecbba39aa78c6c45 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 21 Feb 2024 20:35:54 +0100 Subject: [PATCH 10/68] Use latest @types/react and @types/react-dom --- packages/volto/package.json | 4 +- .../theme/SlotRenderer/SlotRenderer.tsx | 3 +- pnpm-lock.yaml | 70 ++++++++++--------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/packages/volto/package.json b/packages/volto/package.json index 55a414518a..a053583b8b 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -189,6 +189,8 @@ "@plone/registry": "workspace:*", "@plone/scripts": "workspace:*", "@plone/volto-slate": "workspace:*", + "@types/react": "^18.2.57", + "@types/react-dom": "^18.2.19", "autoprefixer": "10.4.8", "axe-core": "4.4.2", "babel-plugin-add-module-exports": "0.2.1", @@ -365,8 +367,6 @@ "@testing-library/react-hooks": "8.0.1", "@types/jest": "^29.5.8", "@types/lodash": "^4.14.201", - "@types/react": "^17.0.52", - "@types/react-dom": "^17", "@types/react-router-dom": "^5.3.3", "@types/react-test-renderer": "18.0.1", "@types/uuid": "^9.0.2", diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index 6d4a884957..b99e294c8c 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -29,8 +29,7 @@ const SlotRenderer = ({ <> {slots.map((component) => { const id = uuid(); - const SlotComponent = component as React.ElementType; - //@ts-ignore - Probably related to an old @types/react dep :( + const SlotComponent = component; return ; })} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1ee1a272b..8ac33998a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -308,7 +308,7 @@ importers: version: 2.4.3(react-router-dom@5.2.0)(react@17.0.2) react-select: specifier: 4.3.1 - version: 4.3.1(@types/react@17.0.70)(react-dom@17.0.2)(react@17.0.2) + version: 4.3.1(@types/react@18.2.57)(react-dom@17.0.2)(react@17.0.2) react-select-async-paginate: specifier: 0.5.3 version: 0.5.3(react-dom@17.0.2)(react-select@4.3.1)(react@17.0.2) @@ -1344,6 +1344,12 @@ importers: '@plone/volto-slate': specifier: workspace:* version: link:../volto-slate + '@types/react': + specifier: ^18.2.57 + version: 18.2.57 + '@types/react-dom': + specifier: ^18.2.19 + version: 18.2.19 autoprefixer: specifier: 10.4.8 version: 10.4.8(postcss@8.4.31) @@ -1673,7 +1679,7 @@ importers: version: 2.4.3(react-router-dom@5.2.0)(react@17.0.2) react-select: specifier: 4.3.1 - version: 4.3.1(@types/react@17.0.70)(react-dom@17.0.2)(react@17.0.2) + version: 4.3.1(@types/react@18.2.57)(react-dom@17.0.2)(react@17.0.2) react-select-async-paginate: specifier: 0.5.3 version: 0.5.3(react-dom@17.0.2)(react-select@4.3.1)(react@17.0.2) @@ -1860,19 +1866,13 @@ importers: version: 12.1.5(react-dom@17.0.2)(react@17.0.2) '@testing-library/react-hooks': specifier: 8.0.1 - version: 8.0.1(@types/react@17.0.70)(react-dom@17.0.2)(react-test-renderer@17.0.2)(react@17.0.2) + version: 8.0.1(@types/react@18.2.57)(react-dom@17.0.2)(react-test-renderer@17.0.2)(react@17.0.2) '@types/jest': specifier: ^29.5.8 version: 29.5.8 '@types/lodash': specifier: ^4.14.201 version: 4.14.201 - '@types/react': - specifier: ^17.0.52 - version: 17.0.70 - '@types/react-dom': - specifier: ^17 - version: 17.0.23 '@types/react-router-dom': specifier: ^5.3.3 version: 5.3.3 @@ -3004,7 +3004,6 @@ packages: dependencies: '@babel/core': 7.23.3 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-flow@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} @@ -3102,7 +3101,6 @@ packages: dependencies: '@babel/core': 7.23.3 '@babel/helper-plugin-utils': 7.22.5 - dev: true /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} @@ -4212,7 +4210,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.3) '@babel/types': 7.23.3 - dev: true /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.23.9): resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} @@ -5071,7 +5068,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.1(@types/react@17.0.70)(react@17.0.2): + /@emotion/react@11.11.1(@types/react@18.2.57)(react@17.0.2): resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} peerDependencies: '@types/react': '*' @@ -5087,7 +5084,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@17.0.2) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 17.0.70 + '@types/react': 18.2.57 hoist-non-react-statics: 3.3.2 react: 17.0.2 dev: false @@ -6781,7 +6778,7 @@ packages: react: '>=16' dependencies: '@types/mdx': 2.0.10 - '@types/react': 18.2.55 + '@types/react': 18.2.57 react: 18.2.0 dev: true @@ -16846,7 +16843,7 @@ packages: vitest: 0.34.6(jsdom@22.1.0)(lightningcss@1.23.0) dev: true - /@testing-library/react-hooks@8.0.1(@types/react@17.0.70)(react-dom@17.0.2)(react-test-renderer@17.0.2)(react@17.0.2): + /@testing-library/react-hooks@8.0.1(@types/react@18.2.57)(react-dom@17.0.2)(react-test-renderer@17.0.2)(react@17.0.2): resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} engines: {node: '>=12'} peerDependencies: @@ -16863,7 +16860,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.20.6 - '@types/react': 17.0.70 + '@types/react': 18.2.57 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-error-boundary: 3.1.4(react@17.0.2) @@ -17168,7 +17165,7 @@ packages: /@types/hoist-non-react-statics@3.3.5: resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.57 hoist-non-react-statics: 3.3.2 /@types/html-minifier-terser@5.1.2: @@ -17360,7 +17357,7 @@ packages: /@types/reach__router@1.3.14: resolution: {integrity: sha512-2iOQZbwfw1ZYwYK+dRp7D1b8kU6GlFPJ/iEt33zDYxfId5CAKT7vX3lN/XmJ+FaMZ3FyB99tPgfajcmZnTqdtg==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.57 dev: true /@types/react-dom@17.0.23: @@ -17371,20 +17368,19 @@ packages: /@types/react-dom@18.2.12: resolution: {integrity: sha512-QWZuiA/7J/hPIGocXreCRbx7wyoeet9ooxfbSA+zbIWqyQEE7GMtRn4A37BdYyksnN+/NDnWgfxZH9UVGDw1hg==} dependencies: - '@types/react': 18.2.27 + '@types/react': 18.2.57 dev: true /@types/react-dom@18.2.19: resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.55 - dev: true + '@types/react': 18.2.57 /@types/react-redux@7.1.30: resolution: {integrity: sha512-i2kqM6YaUwFKduamV6QM/uHbb0eCP8f8ZQ/0yWf+BsAVVsZPRYJ9eeGWZ3uxLfWwwA0SrPRMTPTqsPFkY3HZdA==} dependencies: '@types/hoist-non-react-statics': 3.3.5 - '@types/react': 18.2.55 + '@types/react': 18.2.57 hoist-non-react-statics: 3.3.2 redux: 4.1.0 dev: false @@ -17393,7 +17389,7 @@ packages: resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} dependencies: '@types/hoist-non-react-statics': 3.3.5 - '@types/react': 18.2.55 + '@types/react': 18.2.57 hoist-non-react-statics: 3.3.2 redux: 4.1.0 dev: true @@ -17402,7 +17398,7 @@ packages: resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.55 + '@types/react': 18.2.57 '@types/react-router': 5.1.20 dev: true @@ -17410,19 +17406,19 @@ packages: resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.55 + '@types/react': 18.2.57 dev: true /@types/react-syntax-highlighter@11.0.5: resolution: {integrity: sha512-VIOi9i2Oj5XsmWWoB72p3KlZoEbdRAcechJa8Ztebw7bDl2YmR+odxIqhtJGp1q2EozHs02US+gzxJ9nuf56qg==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.57 dev: true /@types/react-test-renderer@18.0.1: resolution: {integrity: sha512-LjEF+jTUCjzd+Qq4eWqsmZvEWPA/l4L0my+YWN5US8Fo3wZOMiyrpBshHDFbkO8usjdO1B430mEWNU/i1MF7Qg==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.57 dev: true /@types/react@17.0.70: @@ -17446,6 +17442,14 @@ packages: '@types/prop-types': 15.7.10 '@types/scheduler': 0.16.6 csstype: 3.1.2 + dev: true + + /@types/react@18.2.57: + resolution: {integrity: sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==} + dependencies: + '@types/prop-types': 15.7.10 + '@types/scheduler': 0.16.6 + csstype: 3.1.2 /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -23766,8 +23770,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.9) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.9) + '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.3) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.3) eslint: 8.49.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -35321,13 +35325,13 @@ packages: '@seznam/compose-react-refs': 1.0.6 react: 17.0.2 react-is-mounted-hook: 1.1.2(react-dom@17.0.2)(react@17.0.2) - react-select: 4.3.1(@types/react@17.0.70)(react-dom@17.0.2)(react@17.0.2) + react-select: 4.3.1(@types/react@18.2.57)(react-dom@17.0.2)(react@17.0.2) sleep-promise: 9.1.0 transitivePeerDependencies: - react-dom dev: false - /react-select@4.3.1(@types/react@17.0.70)(react-dom@17.0.2)(react@17.0.2): + /react-select@4.3.1(@types/react@18.2.57)(react-dom@17.0.2)(react@17.0.2): resolution: {integrity: sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 @@ -35335,7 +35339,7 @@ packages: dependencies: '@babel/runtime': 7.20.6 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.1(@types/react@17.0.70)(react@17.0.2) + '@emotion/react': 11.11.1(@types/react@18.2.57)(react@17.0.2) memoize-one: 5.2.1 prop-types: 15.7.2 react: 17.0.2 From 70c93f34fd2cbce06b55a977642823b0cce97122 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 21 Feb 2024 23:03:35 +0100 Subject: [PATCH 11/68] Temptative docs --- docs/source/configuration/slots.md | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/source/configuration/slots.md diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md new file mode 100644 index 0000000000..f95f9c33b1 --- /dev/null +++ b/docs/source/configuration/slots.md @@ -0,0 +1,89 @@ +# Slots + +Slots are insertion points in the Volto rendering tree structure. +This concept is inherited from Plone ClassicUI artifact `Viewlets`. + +## Anatomy + +Slots are named, and they can contain a list of different slot components. +Slots components are also named, and they are registered in the configuration registry using a specific API for slots. +The main trait of a slot component is that its render are controlled by a list of conditions (predicates). +Multiple slot components can be registered under the same name, as long as they have different predicates. + +Slot (eg. `toolbar`) + - SlotComponent (eg. `edit`) + - predicates (eg. only appear in `/de/about`) + - predicates (eg. only appear if content type is one of `Document` or `News Item`) + - no predicates (eg. always appear) + - SlotComponent (eg. `contents`) + - SlotComponent (eg. `more`) + +The order in which the components should render is governed by the order that they were registered. +You can change the order of the defined slot components for a different slot using the API. (pending) +You can even delete the rendering of a registered slot component using the API (pending) + +Slot (eg. `toolbar`) + - `edit` + - `contents` + - `more` + +Volto renders the slots using the `SlotRenderer` component. +You can add more insertion points in your code as needed. + +```tsx + +``` + +## Registering a slot component + +You register a slot component using the configuration registry: + +```ts + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); +``` + +`slot`: The name of the slot, where the slot components are stored +`name`: The name of the slot component that we are registering +`component`: The component that we want to render in the slot +`predicates`: A list of functions that return a function with this signature: + +```ts +export type SlotPredicate = (args: any) => boolean; +``` + +There are two predicate helpers available in the Volto helpers. + +### RouteCondition + +```ts +export function RouteCondition(path: string, exact?: boolean) { + return ({ pathname }: { pathname: string }) => + Boolean(matchPath(pathname, { path, exact })); +} +``` + +It allows to make a slot render if a certain route is matched. +It takes the route and if the route match should be exact or not. + +### ContentTypeCondition + +```ts +export function ContentTypeCondition(contentType: string[]) { + return ({ content }: { content: Content }) => + contentType.includes(content['@type']); +} +``` + +This helper predicate allows you to make a slot render if the current content type is match. +It takes a list of possible content types. + +### Custom predicates + +You can provide your own predicate helpers to determine if your slot component has to render or not. +The `SlotRenderer` will pass down the current `content` and the `pathname`. +If that is not enough you could tailor your own `SlotRenderer`s or shadow the original to match your requirements. From 3f6ed67050e463c39b804f72975c385bb5dc3c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 22 Feb 2024 08:49:27 +0100 Subject: [PATCH 12/68] Apply suggestions from code review Co-authored-by: Steve Piercy --- docs/source/configuration/slots.md | 33 +++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index f95f9c33b1..406d6d670c 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -1,13 +1,22 @@ +--- +myst: + html_meta: + "description": "Slots are insertion points in the Volto rendering tree structure." + "property=og:description": "Slots are insertion points in the Volto rendering tree structure." + "property=og:title": "Slots" + "keywords": "Volto, Plone, frontend, React, configuration, slots, viewlets" +--- + # Slots Slots are insertion points in the Volto rendering tree structure. -This concept is inherited from Plone ClassicUI artifact `Viewlets`. +This concept is inherited from the Plone Classic UI {doc}`plone:classic-ui/viewlets`. ## Anatomy Slots are named, and they can contain a list of different slot components. -Slots components are also named, and they are registered in the configuration registry using a specific API for slots. -The main trait of a slot component is that its render are controlled by a list of conditions (predicates). +Slot components are also named, and they are registered in the configuration registry using a specific API for slots. +The main trait of a slot component is that its renderer is controlled by a list of conditions called {term}`predicates`. Multiple slot components can be registered under the same name, as long as they have different predicates. Slot (eg. `toolbar`) @@ -18,7 +27,7 @@ Slot (eg. `toolbar`) - SlotComponent (eg. `contents`) - SlotComponent (eg. `more`) -The order in which the components should render is governed by the order that they were registered. +The order in which the components render is governed by the order in which they were registered. You can change the order of the defined slot components for a different slot using the API. (pending) You can even delete the rendering of a registered slot component using the API (pending) @@ -28,13 +37,13 @@ Slot (eg. `toolbar`) - `more` Volto renders the slots using the `SlotRenderer` component. -You can add more insertion points in your code as needed. +You can add insertion points in your code, as shown in the following example. ```tsx ``` -## Registering a slot component +## Register a slot component You register a slot component using the configuration registry: @@ -58,7 +67,7 @@ export type SlotPredicate = (args: any) => boolean; There are two predicate helpers available in the Volto helpers. -### RouteCondition +### `RouteCondition` ```ts export function RouteCondition(path: string, exact?: boolean) { @@ -67,10 +76,10 @@ export function RouteCondition(path: string, exact?: boolean) { } ``` -It allows to make a slot render if a certain route is matched. +It renders a slot if the specified route matches. It takes the route and if the route match should be exact or not. -### ContentTypeCondition +### `ContentTypeCondition` ```ts export function ContentTypeCondition(contentType: string[]) { @@ -79,11 +88,11 @@ export function ContentTypeCondition(contentType: string[]) { } ``` -This helper predicate allows you to make a slot render if the current content type is match. +The `ContentTypeCondition` helper predicate allows you to render a slot when the given content type matches the current content type. It takes a list of possible content types. ### Custom predicates -You can provide your own predicate helpers to determine if your slot component has to render or not. -The `SlotRenderer` will pass down the current `content` and the `pathname`. +You can create your own predicate helpers to determine whether your slot component should render. +The `SlotRenderer` will pass down the current `content` and the `pathname` into your custom predicate helper. If that is not enough you could tailor your own `SlotRenderer`s or shadow the original to match your requirements. From 829bea1a2ab5f2e9f629c2b57e5108f3cfdceae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 22 Feb 2024 08:49:58 +0100 Subject: [PATCH 13/68] Update docs/source/configuration/slots.md Co-authored-by: Steve Piercy --- docs/source/configuration/slots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 406d6d670c..a4c6397d53 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -89,7 +89,7 @@ export function ContentTypeCondition(contentType: string[]) { ``` The `ContentTypeCondition` helper predicate allows you to render a slot when the given content type matches the current content type. -It takes a list of possible content types. +It accepts a list of possible content types. ### Custom predicates From 4aba432b4fc8401e5f7d91d96d8b730b1e86d90a Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 09:30:28 +0100 Subject: [PATCH 14/68] F*ck ts-jest, cannot wait to say bye to Jest --- packages/volto/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/volto/package.json b/packages/volto/package.json index d00a906022..66fa82ad9e 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -75,7 +75,12 @@ "jest": { "transform": { "^.+\\.js(x)?$": "babel-jest", - "^.+\\.ts(x)?$": "ts-jest", + "^.+\\.ts(x)?$": [ + "ts-jest", + { + "diagnostics": false + } + ], "^.+\\.(png)$": "jest-file", "^.+\\.(jpg)$": "jest-file", "^.+\\.(svg)$": "./jest-svgsystem-transform.js" From 3fbe33ce577762c75cc59da64b8a72e0923960de Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 09:59:54 +0100 Subject: [PATCH 15/68] Change name of the --- packages/coresandbox/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index 760bd1983c..b5c04d775b 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -176,7 +176,7 @@ const applyConfig = (config: ConfigType) => { config.registerSlotComponent({ slot: 'aboveContent', - name: 'hello.aboveFooter', + name: 'testSlotComponent', component: SlotComponentTest, predicates: [ContentTypeCondition(['Document']), RouteCondition('/hello')], }); From 0f013fc2eaf5885068ce6f870385f579003d0f16 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 10:05:51 +0100 Subject: [PATCH 16/68] Remove Semantic Container from coresandbox example --- packages/coresandbox/src/components/Slots/SlotTest.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/coresandbox/src/components/Slots/SlotTest.tsx b/packages/coresandbox/src/components/Slots/SlotTest.tsx index affaf5b019..2c273d5f4b 100644 --- a/packages/coresandbox/src/components/Slots/SlotTest.tsx +++ b/packages/coresandbox/src/components/Slots/SlotTest.tsx @@ -1,11 +1,9 @@ -import { Container } from 'semantic-ui-react'; - const SlotComponentTest = () => { return ( - +

This is a test slot component

It should appear above the Content

- +
); }; From 3241e764325a2e93f4a5409d6d1c421b1875b019 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 01:35:13 -0800 Subject: [PATCH 17/68] Add slots to toctree --- docs/source/configuration/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/configuration/index.md b/docs/source/configuration/index.md index fde6916ff3..3375c41f9c 100644 --- a/docs/source/configuration/index.md +++ b/docs/source/configuration/index.md @@ -26,4 +26,5 @@ workingcopy environmentvariables expanders locking +slots ``` From 8662ee4b1129f98f3976774371433915fb866071 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 02:05:16 -0800 Subject: [PATCH 18/68] Add https://stackoverflow.com to ignore in linkcheck, and sort entries --- docs/source/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b60d6eabac..6360e35dae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -91,10 +91,11 @@ # Ignore github.com pages with anchors r"https://github.com/.*#.*", # Ignore other specific anchors - r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue", - r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0", r"https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects". r"https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects". + r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue", + r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0", + r"https://stackoverflow.com", # volto and documentation # TODO retest with latest Sphinx. ] linkcheck_anchors = True linkcheck_timeout = 10 From c4cfe86ce3aacfd829cde31ea868f7f09026d855 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 02:35:31 -0800 Subject: [PATCH 19/68] Expound upon the slot tree structure and how it works with an ASCII tree --- docs/source/configuration/slots.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index a4c6397d53..3acb563afc 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -19,13 +19,24 @@ Slot components are also named, and they are registered in the configuration reg The main trait of a slot component is that its renderer is controlled by a list of conditions called {term}`predicates`. Multiple slot components can be registered under the same name, as long as they have different predicates. -Slot (eg. `toolbar`) - - SlotComponent (eg. `edit`) - - predicates (eg. only appear in `/de/about`) - - predicates (eg. only appear if content type is one of `Document` or `News Item`) - - no predicates (eg. always appear) - - SlotComponent (eg. `contents`) - - SlotComponent (eg. `more`) +The following tree structure diagram illustrates these concepts. + +```text +Slot (`toolbar`) +├── SlotComponent (`edit`) +│   ├── predicates (only appear in `/de/about`) +│   ├── predicates (only appear if the content type is either a `Document` or `News Item`) +│   └── no predicates (default when all predicates return `false`) +├── SlotComponent (`contents`) +└── SlotComponent (`more`) +``` + +At the root of the tree, there is a slot named `toolbar`. +It contains three slot components, named `edit`, `contents`, and `more`. +`edit` contains two predicates and a default for when all of its predicates return `false`. + +Thus, when either the route is `/de/about` or the content type is either a `Document` or `News Item`, then the `edit` slot component would appear in the slot `about`. +It would not display elsewhere. The order in which the components render is governed by the order in which they were registered. You can change the order of the defined slot components for a different slot using the API. (pending) From 7e30c9db1cc25b9021480eb6dd83656210dff9a8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 02:40:21 -0800 Subject: [PATCH 20/68] Put the pending stuff into a todo, so that it sticks out like a sore thumb. --- docs/source/configuration/slots.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 3acb563afc..8c34c2a4f9 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -39,13 +39,16 @@ Thus, when either the route is `/de/about` or the content type is either a `Docu It would not display elsewhere. The order in which the components render is governed by the order in which they were registered. + +```{todo} You can change the order of the defined slot components for a different slot using the API. (pending) -You can even delete the rendering of a registered slot component using the API (pending) +You can even delete the rendering of a registered slot component using the API. (pending) Slot (eg. `toolbar`) - `edit` - `contents` - `more` +``` Volto renders the slots using the `SlotRenderer` component. You can add insertion points in your code, as shown in the following example. From 520fc605c45145562c9f796eee3e05952c19aa95 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 02:41:24 -0800 Subject: [PATCH 21/68] Put the predicates into a definition list (I changed my mind from a glossary) --- docs/source/configuration/slots.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 8c34c2a4f9..0369005008 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -70,14 +70,21 @@ You register a slot component using the configuration registry: }); ``` -`slot`: The name of the slot, where the slot components are stored -`name`: The name of the slot component that we are registering -`component`: The component that we want to render in the slot -`predicates`: A list of functions that return a function with this signature: +`slot` +: The name of the slot, where the slot components are stored. + +`name` +: The name of the slot component that we are registering. -```ts -export type SlotPredicate = (args: any) => boolean; -``` +`component` +: The component that we want to render in the slot. + +`predicates` +: A list of functions that return a function with this signature. + + ```ts + export type SlotPredicate = (args: any) => boolean; + ``` There are two predicate helpers available in the Volto helpers. From 1a9cb4331bf0bf99176ed47708e27f58f416f9d8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 02:49:36 -0800 Subject: [PATCH 22/68] Create a new parent subsection for predicate helpers --- docs/source/configuration/slots.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 0369005008..a763877fa0 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -86,8 +86,12 @@ You register a slot component using the configuration registry: export type SlotPredicate = (args: any) => boolean; ``` + +## Predicate helpers + There are two predicate helpers available in the Volto helpers. + ### `RouteCondition` ```ts From a8f6de3a946d44bd2b6c8a047037a38e28c73b70 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 03:12:48 -0800 Subject: [PATCH 23/68] Tidy up `RouteCondition` --- docs/source/configuration/slots.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index a763877fa0..496698cfa1 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -101,8 +101,15 @@ export function RouteCondition(path: string, exact?: boolean) { } ``` -It renders a slot if the specified route matches. -It takes the route and if the route match should be exact or not. +The `RouteCondition` predicate helper renders a slot if the specified route matches. +It accepts the following parameters. + +`path` +: Required. String. The route. + +`exact` +: Optional. Boolean. If `true`, then the match will be exact, else matches "begins with", for the given string from `path`. + ### `ContentTypeCondition` From ffced49ec06189be4a27557ac7c2e55bf84e9d2d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 03:15:17 -0800 Subject: [PATCH 24/68] Final pass, nitpicky stuff --- docs/source/configuration/slots.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 496698cfa1..67b9d738ad 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -12,10 +12,12 @@ myst: Slots are insertion points in the Volto rendering tree structure. This concept is inherited from the Plone Classic UI {doc}`plone:classic-ui/viewlets`. + ## Anatomy Slots are named, and they can contain a list of different slot components. Slot components are also named, and they are registered in the configuration registry using a specific API for slots. + The main trait of a slot component is that its renderer is controlled by a list of conditions called {term}`predicates`. Multiple slot components can be registered under the same name, as long as they have different predicates. @@ -50,13 +52,14 @@ Slot (eg. `toolbar`) - `more` ``` -Volto renders the slots using the `SlotRenderer` component. +Volto renders slots using the `SlotRenderer` component. You can add insertion points in your code, as shown in the following example. -```tsx +```ts ``` + ## Register a slot component You register a slot component using the configuration registry: @@ -70,6 +73,8 @@ You register a slot component using the configuration registry: }); ``` +A slot component must have the following parameters. + `slot` : The name of the slot, where the slot components are stored. @@ -123,8 +128,9 @@ export function ContentTypeCondition(contentType: string[]) { The `ContentTypeCondition` helper predicate allows you to render a slot when the given content type matches the current content type. It accepts a list of possible content types. + ### Custom predicates You can create your own predicate helpers to determine whether your slot component should render. The `SlotRenderer` will pass down the current `content` and the `pathname` into your custom predicate helper. -If that is not enough you could tailor your own `SlotRenderer`s or shadow the original to match your requirements. +You can also tailor your own `SlotRenderer`s, or shadow the original `SlotRenderer`, to satisfy your requirements. From f17ecbe9f5b5f25588b81d01a22b2fb8c8d71356 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 12:21:15 +0100 Subject: [PATCH 25/68] Fix TS tests --- packages/volto/package.json | 7 +------ .../src/components/theme/View/View.test.jsx | 3 +++ .../View/__snapshots__/View.test.jsx.snap | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/volto/package.json b/packages/volto/package.json index 66fa82ad9e..d00a906022 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -75,12 +75,7 @@ "jest": { "transform": { "^.+\\.js(x)?$": "babel-jest", - "^.+\\.ts(x)?$": [ - "ts-jest", - { - "diagnostics": false - } - ], + "^.+\\.ts(x)?$": "ts-jest", "^.+\\.(png)$": "jest-file", "^.+\\.(jpg)$": "jest-file", "^.+\\.(svg)$": "./jest-svgsystem-transform.js" diff --git a/packages/volto/src/components/theme/View/View.test.jsx b/packages/volto/src/components/theme/View/View.test.jsx index 66d02014c6..0fd4580099 100644 --- a/packages/volto/src/components/theme/View/View.test.jsx +++ b/packages/volto/src/components/theme/View/View.test.jsx @@ -33,6 +33,9 @@ jest.mock('../SocialSharing/SocialSharing', () => ); jest.mock('../Comments/Comments', () => jest.fn(() =>
)); jest.mock('../Tags/Tags', () => jest.fn(() =>
)); +jest.mock('../SlotRenderer/SlotRenderer', () => + jest.fn(() =>
), +); jest.mock('../ContentMetadataTags/ContentMetadataTags', () => jest.fn(() =>
), ); diff --git a/packages/volto/src/components/theme/View/__snapshots__/View.test.jsx.snap b/packages/volto/src/components/theme/View/__snapshots__/View.test.jsx.snap index 544bbf3fa3..49a539fd4e 100644 --- a/packages/volto/src/components/theme/View/__snapshots__/View.test.jsx.snap +++ b/packages/volto/src/components/theme/View/__snapshots__/View.test.jsx.snap @@ -7,9 +7,15 @@ exports[`View renders a document view 1`] = `
+
+
@@ -23,9 +29,15 @@ exports[`View renders a summary view 1`] = `
+
+
@@ -39,9 +51,15 @@ exports[`View renders a tabular view 1`] = `
+
+
From 6b98d6c5ed596d4a9f0fcfa4aa7418ff8d74e7ae Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 12:29:35 +0100 Subject: [PATCH 26/68] push locks --- pnpm-lock.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ac33998a3..7d032298dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3004,6 +3004,7 @@ packages: dependencies: '@babel/core': 7.23.3 '@babel/helper-plugin-utils': 7.22.5 + dev: true /@babel/plugin-syntax-flow@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} @@ -3101,6 +3102,7 @@ packages: dependencies: '@babel/core': 7.23.3 '@babel/helper-plugin-utils': 7.22.5 + dev: true /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} @@ -4210,6 +4212,7 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.3) '@babel/types': 7.23.3 + dev: true /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.23.9): resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} @@ -23770,8 +23773,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.3) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.3) + '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.9) eslint: 8.49.0 lodash: 4.17.21 string-natural-compare: 3.0.1 From 675e0af8402328988b6d4d758272491f94984364 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 13:44:39 +0100 Subject: [PATCH 27/68] Finish the missing docs --- docs/source/configuration/slots.md | 92 +++++++++++++- packages/registry/src/index.ts | 48 ++++++- packages/registry/src/registry.test.tsx | 162 ++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 67b9d738ad..f2e7f1f570 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -77,7 +77,7 @@ A slot component must have the following parameters. `slot` : The name of the slot, where the slot components are stored. - + `name` : The name of the slot component that we are registering. @@ -108,7 +108,7 @@ export function RouteCondition(path: string, exact?: boolean) { The `RouteCondition` predicate helper renders a slot if the specified route matches. It accepts the following parameters. - + `path` : Required. String. The route. @@ -134,3 +134,91 @@ It accepts a list of possible content types. You can create your own predicate helpers to determine whether your slot component should render. The `SlotRenderer` will pass down the current `content` and the `pathname` into your custom predicate helper. You can also tailor your own `SlotRenderer`s, or shadow the original `SlotRenderer`, to satisfy your requirements. + +## Manage registered slots and slot components + +### `getSlotComponents` + +It returns the list of components registered per slot. +This is useful to debug what is registered an in what order, and later change the order, if needed. +This is the signature: + +```ts +config.getSlotComponents(slot: string): string[] + +``` + +`slot` +: The name of the slot, where the slot components are stored. + +### `reorderSlotComponent` + +It reorders the list of components registered per slot. +This is the signature: + +```ts +config.reorderSlotComponent(slot: string, name: string, position: number): void +``` + +`slot` +: The name of the slot, where the slot components are stored. + +`name` +: The name of the slot component we want to reorder. + +`position` +: The destination position in the registered list of slot components that we want to move the slot component. + + +### `getSlotComponent` + +It returns the list of registered components per slot component name. +This is useful to debug what is registered an in what order, and later remove a registration, if needed. +This is the signature: + +```ts +config.getSlotComponent(slot: string, name: string): SlotComponent[] +``` + +`slot` +: The name of the slot, where the slot components are stored. + +`name` +: The name of the slot component we want to retrieve. + +### `unRegisterSlotComponent` + +It removes a registration for a specific component, given its registration position. +This is the signature: + +```ts +config.unRegisterSlotComponent(slot: string, name: string, position: number): void +``` + +`slot` +: The name of the slot, where the slot components are stored. + +`name` +: The name of the slot component inside it's the component we want to unregister. + +`position` +: The component position that we want to remove in the slot component registration. Use `getSlotComponent` to find out the position of the registered component that you want to remove. + +### `getSlot` + +It returns the components that should be rendered per named slot. +You should use this method in case you are building you own slot renderer, or customizing the existing one (`SlotRenderer`). +You can take the implementation of `SlotRenderer` as template. +This is the signature: + +```ts +config.getSlot(name: string, args: T): SlotComponent['component'][] | undefined +``` + +It must have the following parameters. + +`name` +: The name of the slot we want to render. + +`options` +: An object containing the arguments that you want to pass to the predicates. diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index f367c018b5..52bf8fb4fa 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -259,7 +259,7 @@ class Config { currentSlot.data[name] = []; } - const currentSlotComponent = currentSlot.data[name]; + const currentSlotComponents = currentSlot.data[name]; if (!currentSlot.slots.includes(name)) { currentSlot.slots.push(name); } @@ -283,7 +283,51 @@ class Config { ); } - currentSlotComponent.push(slotComponentData); + currentSlotComponents.push(slotComponentData); + } + + getSlotComponent(slot: string, name: string) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + const currentSlotComponents = currentSlot.data[name]; + if (!currentSlotComponents) { + throw new Error(`No slot component ${name} in slot ${slot} found`); + } + return currentSlotComponents; + } + + getSlotComponents(slot: string) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + return currentSlot.slots; + } + + reorderSlotComponent(slot: string, name: string, position: number) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + + const origin = currentSlot.slots.indexOf(name); + currentSlot.slots = currentSlot.slots + .toSpliced(origin, 1) + .toSpliced(position, 0, name); + } + + unRegisterSlotComponent(slot: string, name: string, position: number) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + const currentSlotComponents = currentSlot.data[name]; + if (!currentSlotComponents) { + throw new Error(`No slot component ${name} in slot ${slot} found`); + } + currentSlot.data[name] = currentSlotComponents.toSpliced(position, 1); } } diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index 14daabb6a6..c953cf5a76 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -371,4 +371,166 @@ describe('Slots registry', () => { 'this is a toolbar edit component with true predicate', ]); }); + + it('getSlotComponents - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponents('toolbar').length).toEqual(2); + expect(config.getSlotComponents('toolbar')).toEqual(['save', 'edit']); + }); + + it('getSlotComponent - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + }); + + it('reorderSlotComponent - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + config.reorderSlotComponent('toolbar', 'save', 1); + expect(config.getSlotComponents('toolbar')).toEqual(['edit', 'save']); + }); + + it('unRegisterSlotComponent - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + config.unRegisterSlotComponent('toolbar', 'save', 1); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(1); + }); }); From 187e21e8a4eb48483bade09b61d748cf1f47846a Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 14:15:34 +0100 Subject: [PATCH 28/68] Make ts-jest happy --- .../volto/src/components/theme/SlotRenderer/SlotRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index b99e294c8c..00731b9758 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -27,7 +27,7 @@ const SlotRenderer = ({ return ( <> - {slots.map((component) => { + {slots.map((component: React.ComponentType) => { const id = uuid(); const SlotComponent = component; return ; From f189d8536ca360bdd24fef4605f9cd3fb793c0ac Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 05:38:34 -0800 Subject: [PATCH 29/68] Use a pattern of Type, required/optional, description for signatures. --- docs/source/configuration/slots.md | 59 +++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index f2e7f1f570..d83b3a4bd3 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -110,10 +110,14 @@ The `RouteCondition` predicate helper renders a slot if the specified route matc It accepts the following parameters. `path` -: Required. String. The route. +: String. + Required. + The route. `exact` -: Optional. Boolean. If `true`, then the match will be exact, else matches "begins with", for the given string from `path`. +: Boolean. + Optional. + If `true`, then the match will be exact, else matches "begins with", for the given string from `path`. ### `ContentTypeCondition` @@ -149,7 +153,10 @@ config.getSlotComponents(slot: string): string[] ``` `slot` -: The name of the slot, where the slot components are stored. +: String. + Required. + The name of the slot, where the slot components are stored. + ### `reorderSlotComponent` @@ -161,14 +168,23 @@ config.reorderSlotComponent(slot: string, name: string, position: number): void ``` `slot` -: The name of the slot, where the slot components are stored. +: String. + Required. + The name of the slot where the slot components are stored. `name` -: The name of the slot component we want to reorder. +: String. + Required. + The name of the slot component to reposition in the list of slot components. `position` -: The destination position in the registered list of slot components that we want to move the slot component. +: Number. + Required. + The destination position in the registered list of slot components. + The position is zero-indexed. + +(slots-getSlotComponent-label)= ### `getSlotComponent` @@ -181,10 +197,15 @@ config.getSlotComponent(slot: string, name: string): SlotComponent[] ``` `slot` -: The name of the slot, where the slot components are stored. +: String. + Required. + The name of the slot where the slot components are stored. `name` -: The name of the slot component we want to retrieve. +: String. + Required. + The name of the slot component to retrieve. + ### `unRegisterSlotComponent` @@ -196,13 +217,21 @@ config.unRegisterSlotComponent(slot: string, name: string, position: number): vo ``` `slot` -: The name of the slot, where the slot components are stored. +: String. + Required. + The name of the slot that contains the slot component to unregister. `name` -: The name of the slot component inside it's the component we want to unregister. +: String. + Required. + The name of the slot component to unregister inside the component. `position` -: The component position that we want to remove in the slot component registration. Use `getSlotComponent` to find out the position of the registered component that you want to remove. +: Number. + Required. + The component position to remove in the slot component registration. + Use {ref}`slots-getSlotComponent-label` to find the position of the registered component to remove. + ### `getSlot` @@ -218,7 +247,11 @@ config.getSlot(name: string, args: T): SlotComponent['component'][] | undefin It must have the following parameters. `name` -: The name of the slot we want to render. +: String. + Required. + The name of the slot we want to render. `options` -: An object containing the arguments that you want to pass to the predicates. +: Object. + Required. + An object containing the arguments to pass to the predicates. From e234d630a7136cb99e13719fda23703cae771c80 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 05:39:33 -0800 Subject: [PATCH 30/68] Grammar, no empty headings, tidy up --- docs/source/configuration/slots.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index d83b3a4bd3..032d05214a 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -139,17 +139,20 @@ You can create your own predicate helpers to determine whether your slot compone The `SlotRenderer` will pass down the current `content` and the `pathname` into your custom predicate helper. You can also tailor your own `SlotRenderer`s, or shadow the original `SlotRenderer`, to satisfy your requirements. + ## Manage registered slots and slot components +You can manage registered slots and slot components through the slots API. + + ### `getSlotComponents` -It returns the list of components registered per slot. -This is useful to debug what is registered an in what order, and later change the order, if needed. +`getSlotComponents` returns the list of components registered per slot. +This is useful to debug what is registered and in what order, informing you whether you need to change their order. This is the signature: ```ts config.getSlotComponents(slot: string): string[] - ``` `slot` @@ -160,7 +163,7 @@ config.getSlotComponents(slot: string): string[] ### `reorderSlotComponent` -It reorders the list of components registered per slot. +`reorderSlotComponent` reorders the list of slot components registered per slot. This is the signature: ```ts @@ -188,8 +191,8 @@ config.reorderSlotComponent(slot: string, name: string, position: number): void ### `getSlotComponent` -It returns the list of registered components per slot component name. -This is useful to debug what is registered an in what order, and later remove a registration, if needed. +`getSlotComponent` returns the list of registered components under the given slot component name. +This is useful to debug what is registered and in what order, and later remove a component's registration, if needed. This is the signature: ```ts @@ -235,16 +238,16 @@ config.unRegisterSlotComponent(slot: string, name: string, position: number): vo ### `getSlot` -It returns the components that should be rendered per named slot. -You should use this method in case you are building you own slot renderer, or customizing the existing one (`SlotRenderer`). -You can take the implementation of `SlotRenderer` as template. +`getSlot` returns the components to be rendered for the given named slot. +You should use this method while building you own slot renderer or customizing the existing `SlotRenderer`. +You can use the implementation of `SlotRenderer` as a template. This is the signature: ```ts config.getSlot(name: string, args: T): SlotComponent['component'][] | undefined ``` -It must have the following parameters. +It has the following parameters. `name` : String. From b23bd9ad38eec01d2f0342a11c178e8833e4ab1a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 22 Feb 2024 05:41:13 -0800 Subject: [PATCH 31/68] zero-indexed --- docs/source/configuration/slots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 032d05214a..545df2903e 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -233,7 +233,7 @@ config.unRegisterSlotComponent(slot: string, name: string, position: number): vo : Number. Required. The component position to remove in the slot component registration. - Use {ref}`slots-getSlotComponent-label` to find the position of the registered component to remove. + Use {ref}`slots-getSlotComponent-label` to find the zero-indexed position of the registered component to remove. ### `getSlot` From 2ad12dc4a07ed9870c849d6adc8c23123060a635 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 14:27:07 +0100 Subject: [PATCH 32/68] Add Cypress test --- .../src/components/Slots/SlotTest.tsx | 2 +- .../volto/cypress/tests/coresandbox/slots.js | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/volto/cypress/tests/coresandbox/slots.js diff --git a/packages/coresandbox/src/components/Slots/SlotTest.tsx b/packages/coresandbox/src/components/Slots/SlotTest.tsx index 2c273d5f4b..66f095c552 100644 --- a/packages/coresandbox/src/components/Slots/SlotTest.tsx +++ b/packages/coresandbox/src/components/Slots/SlotTest.tsx @@ -1,6 +1,6 @@ const SlotComponentTest = () => { return ( -
+

This is a test slot component

It should appear above the Content

diff --git a/packages/volto/cypress/tests/coresandbox/slots.js b/packages/volto/cypress/tests/coresandbox/slots.js new file mode 100644 index 0000000000..4d8abe6e48 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/slots.js @@ -0,0 +1,43 @@ +context('Slots', () => { + describe('Block Default View / Edit', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'hello', + contentTitle: 'Test document Hello', + }); + + cy.visit('/'); + cy.wait('@content'); + }); + + it('[ContentTypeCondition(["Document"]), RouteCondition("/hello")] only renders when the predicates are true', function () { + cy.get('body').should( + 'not.include.text', + 'This is a test slot component', + ); + + cy.navigate('/document'); + cy.wait('@content'); + + cy.get('body').should( + 'not.include.text', + 'This is a test slot component', + ); + + cy.navigate('/hello'); + cy.wait('@content'); + + cy.get('body').should('include.text', 'This is a test slot component'); + }); + }); +}); From 659812724c73e6d23df5c840945555612eb14d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 22 Feb 2024 15:52:52 +0100 Subject: [PATCH 33/68] Update docs/source/configuration/slots.md Co-authored-by: Steve Piercy --- docs/source/configuration/slots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 545df2903e..c1573cec51 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -210,7 +210,7 @@ config.getSlotComponent(slot: string, name: string): SlotComponent[] The name of the slot component to retrieve. -### `unRegisterSlotComponent` +### `unregisterSlotComponent` It removes a registration for a specific component, given its registration position. This is the signature: From 9bec47ff46ca9fbd2662cd5c868c8e732dd79811 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 15:56:15 +0100 Subject: [PATCH 34/68] Force fresh build always --- packages/registry/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/registry/package.json b/packages/registry/package.json index c8a2fa1c1f..83351c88c2 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -50,7 +50,7 @@ }, "scripts": { "watch": "parcel watch", - "build": "parcel build", + "build": "parcel build --no-cache", "test": "vitest", "dry-release": "release-it --dry-run", "release": "release-it", From 5fde3590acb618995092c4cfbbe0f61536a0d8f2 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 16:12:03 +0100 Subject: [PATCH 35/68] Force build on coresandbox --- packages/volto/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/volto/Makefile b/packages/volto/Makefile index fbb5a192fe..cc03502731 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -156,6 +156,7 @@ start-test-acceptance-server-coresandbox test-acceptance-server-coresandbox: ## .PHONY: start-test-acceptance-frontend-coresandbox start-test-acceptance-frontend-coresandbox: build-deps ## Start the CoreSandbox Acceptance Frontend Fixture + pnpm --filter @plone/registry build ADDONS=@plone/volto-coresandbox RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod .PHONY: start-test-acceptance-frontend-coresandbox-dev From c93d1465ba23d20245eb4e985e8e2c3e03310d19 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 16:58:00 +0100 Subject: [PATCH 36/68] Revert "Force build on coresandbox" This reverts commit 5fde3590acb618995092c4cfbbe0f61536a0d8f2. --- packages/volto/Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/volto/Makefile b/packages/volto/Makefile index cc03502731..fbb5a192fe 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -156,7 +156,6 @@ start-test-acceptance-server-coresandbox test-acceptance-server-coresandbox: ## .PHONY: start-test-acceptance-frontend-coresandbox start-test-acceptance-frontend-coresandbox: build-deps ## Start the CoreSandbox Acceptance Frontend Fixture - pnpm --filter @plone/registry build ADDONS=@plone/volto-coresandbox RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod .PHONY: start-test-acceptance-frontend-coresandbox-dev From 37e6b929b72e961adf37ad0de398a6e7e0768178 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 17:05:56 +0100 Subject: [PATCH 37/68] Remove methods that are not 18.x compatible :( (toReversed, toSplice) --- packages/registry/src/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 52bf8fb4fa..c1c8de83e5 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -196,7 +196,8 @@ class Config { // TODO: Cover ZCA use case, where if more predicates, more specificity wins if all true. // Let's keep it simple here and stick to the registered order. let noPredicateComponent: SlotComponent | undefined; - for (const slotComponent of data[slotName].toReversed()) { + const reversedSlotComponents = data[slotName].slice().reverse(); // Immutable reversed copy + for (const slotComponent of reversedSlotComponents) { let isPredicateTrueFound: boolean = false; if (slotComponent.predicates) { isPredicateTrueFound = slotComponent.predicates.every((predicate) => @@ -311,11 +312,12 @@ class Config { if (!slot || !currentSlot) { throw new Error(`No slot ${slot} found`); } - const origin = currentSlot.slots.indexOf(name); - currentSlot.slots = currentSlot.slots - .toSpliced(origin, 1) - .toSpliced(position, 0, name); + const result = Array.from(currentSlot.slots); + const [removed] = result.splice(origin, 1); + result.splice(position, 0, removed); + + currentSlot.slots = result; } unRegisterSlotComponent(slot: string, name: string, position: number) { @@ -327,7 +329,8 @@ class Config { if (!currentSlotComponents) { throw new Error(`No slot component ${name} in slot ${slot} found`); } - currentSlot.data[name] = currentSlotComponents.toSpliced(position, 1); + const result = currentSlotComponents.slice(); + currentSlot.data[name] = result.splice(position, 1); } } From 26d4dda017fc311abc8702c268490d9ca9fefc44 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 17:07:00 +0100 Subject: [PATCH 38/68] Add a small comment --- .../volto/src/components/theme/SlotRenderer/SlotRenderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index 00731b9758..96474b80c2 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -28,6 +28,7 @@ const SlotRenderer = ({ return ( <> {slots.map((component: React.ComponentType) => { + // Weird compilation issue, ^^ that forced to re-declare the type above const id = uuid(); const SlotComponent = component; return ; From ac09167c6b3747f875e0f5ebf9366f39e5991292 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Thu, 22 Feb 2024 17:08:09 +0100 Subject: [PATCH 39/68] Remove comments --- packages/registry/src/registry.test.tsx | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index c953cf5a76..f1ead84ed2 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -114,26 +114,6 @@ describe('Component registry', () => { }); describe('Slots registry', () => { - // config.slots.toolbar = [ // viewlets.xml - // 'save', - // 'edit', - // ] - - // config.slots.toolbar.save = [ - // { - // predicates: [RouteCondition('/de')], - // component: 'this is a Bar component', - // }, - // { - // predicates: [RouteCondition('/de'), ContentTypeCondition(['News Item'])], - // component: 'this is a Bar component', - // }, - // { - // predicates: [RouteCondition('/de/path/folder'), ContentTypeCondition(['News Item']), TrueCondition()], - // component: 'this is a Bar component', - // }, - // ] - afterEach(() => { config.set('slots', {}); }); From adec91df973c44b0a66b16090387fcf1c44e24a1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 24 Feb 2024 11:53:35 -0800 Subject: [PATCH 40/68] Update docs/source/configuration/slots.md s/about/toolbar Co-authored-by: David Glick --- docs/source/configuration/slots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index c1573cec51..1196259a38 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -37,7 +37,7 @@ At the root of the tree, there is a slot named `toolbar`. It contains three slot components, named `edit`, `contents`, and `more`. `edit` contains two predicates and a default for when all of its predicates return `false`. -Thus, when either the route is `/de/about` or the content type is either a `Document` or `News Item`, then the `edit` slot component would appear in the slot `about`. +Thus, when either the route is `/de/about` or the content type is either a `Document` or `News Item`, then the `edit` slot component would appear in the slot `toolbar`. It would not display elsewhere. The order in which the components render is governed by the order in which they were registered. From 049e16a846405392473e99ea08e245e407391443 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 24 Feb 2024 12:23:07 -0800 Subject: [PATCH 41/68] Link to API --- docs/source/configuration/slots.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 1196259a38..8a1847f15f 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -16,7 +16,7 @@ This concept is inherited from the Plone Classic UI {doc}`plone:classic-ui/viewl ## Anatomy Slots are named, and they can contain a list of different slot components. -Slot components are also named, and they are registered in the configuration registry using a specific API for slots. +Slot components are also named, and they are registered in the {ref}`configuration registry using a specific API for slots `. The main trait of a slot component is that its renderer is controlled by a list of conditions called {term}`predicates`. Multiple slot components can be registered under the same name, as long as they have different predicates. @@ -60,7 +60,9 @@ You can add insertion points in your code, as shown in the following example. ``` -## Register a slot component +(configuration-registry-for-slot-components)= + +## Configuration registry for slot components You register a slot component using the configuration registry: From 547a6773e1616bc3d48326745cc9c1cebfda5cb7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 24 Feb 2024 12:25:04 -0800 Subject: [PATCH 42/68] Enhance intro sentence Co-authored-by: David Glick --- docs/source/configuration/slots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 8a1847f15f..8ff81de351 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -9,7 +9,7 @@ myst: # Slots -Slots are insertion points in the Volto rendering tree structure. +Slots provide a way for Volto add-ons to insert their own components at predefined locations in the rendered page. This concept is inherited from the Plone Classic UI {doc}`plone:classic-ui/viewlets`. From 3496942d1b1fbb41a7b3e093641b40ec0f030ba0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 24 Feb 2024 12:26:29 -0800 Subject: [PATCH 43/68] s/inherited from/inspired by Co-authored-by: David Glick --- docs/source/configuration/slots.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index 8ff81de351..bc9c22e85a 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -10,7 +10,10 @@ myst: # Slots Slots provide a way for Volto add-ons to insert their own components at predefined locations in the rendered page. -This concept is inherited from the Plone Classic UI {doc}`plone:classic-ui/viewlets`. + +```{note} +This concept is inspired by the Plone Classic UI {doc}`plone:classic-ui/viewlets`. +``` ## Anatomy From 394622dd42de7e683580044ec4560d820bef7fd8 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 26 Feb 2024 12:35:11 +0100 Subject: [PATCH 44/68] David's suggestions --- docs/source/configuration/slots.md | 13 +++-------- packages/registry/src/index.ts | 15 +++++++++--- .../theme/SlotRenderer/SlotRenderer.tsx | 23 ++++++++++++------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md index bc9c22e85a..32b339730e 100644 --- a/docs/source/configuration/slots.md +++ b/docs/source/configuration/slots.md @@ -45,15 +45,8 @@ It would not display elsewhere. The order in which the components render is governed by the order in which they were registered. -```{todo} -You can change the order of the defined slot components for a different slot using the API. (pending) -You can even delete the rendering of a registered slot component using the API. (pending) - -Slot (eg. `toolbar`) - - `edit` - - `contents` - - `more` -``` +You can change the order of the defined slot components for a different slot using the API. +You can even delete the rendering of a registered slot component using the API. Volto renders slots using the `SlotRenderer` component. You can add insertion points in your code, as shown in the following example. @@ -74,7 +67,7 @@ You register a slot component using the configuration registry: slot: 'toolbar', name: 'save', component: 'this is a toolbar save component with a true predicate', - predicates: [RouteConditionTrue('/de')], + predicates: [RouteCondition('/de')], }); ``` diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index c1c8de83e5..83dc0d5ba4 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -184,7 +184,10 @@ class Config { } } - getSlot(name: string, args: T): SlotComponent['component'][] | undefined { + getSlot( + name: string, + args: T, + ): { component: SlotComponent['component']; name: string }[] | undefined { if (!this._data.slots[name]) { return; } @@ -210,7 +213,10 @@ class Config { // If all the predicates are truthy if (isPredicateTrueFound) { - slotComponents.push(slotComponent.component); + slotComponents.push({ + component: slotComponent.component, + name: slotName, + }); // We "reset" the marker, we already found a candidate noPredicateComponent = undefined; break; @@ -218,7 +224,10 @@ class Config { } if (noPredicateComponent) { - slotComponents.push(noPredicateComponent.component); + slotComponents.push({ + component: noPredicateComponent.component, + name: slotName, + }); } } diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx index 96474b80c2..7386453974 100644 --- a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.tsx @@ -1,6 +1,6 @@ -import { v4 as uuid } from 'uuid'; -import config from '@plone/volto/registry'; import { useLocation } from 'react-router-dom'; +import config from '@plone/volto/registry'; + import type { Content } from '@plone/types'; /* @@ -27,12 +27,19 @@ const SlotRenderer = ({ return ( <> - {slots.map((component: React.ComponentType) => { - // Weird compilation issue, ^^ that forced to re-declare the type above - const id = uuid(); - const SlotComponent = component; - return ; - })} + {slots.map( + ({ + component, + name, + }: { + component: React.ComponentType; + name: string; + }) => { + // ^^ Weird compilation issue for Jest tests, that forced to re-declare the type above + const SlotComponent = component; + return ; + }, + )} ); }; From 829ea91dd6e4fbf062458618f9b52f061f4bfd6e Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 26 Feb 2024 12:40:04 +0100 Subject: [PATCH 45/68] Fix tests --- packages/registry/src/registry.test.tsx | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index f1ead84ed2..f325d8b237 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -135,9 +135,9 @@ describe('Slots registry', () => { component: 'this is a toolbar component with no predicate', }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with no predicate', - ]); + ); }); it('registers two slot components with predicates - registered components order is respected', () => { @@ -159,9 +159,9 @@ describe('Slots registry', () => { ], }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with only a truth-ish route condition', - ]); + ); }); it('registers two slot components with predicates - All registered components predicates are truthy, the last one registered wins', () => { @@ -183,9 +183,9 @@ describe('Slots registry', () => { ], }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with two truth-ish predicates', - ]); + ); }); it('registers two slot components with predicates - No registered component have a truthy predicate', () => { @@ -226,9 +226,9 @@ describe('Slots registry', () => { ], }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with two truth-ish predicates', - ]); + ); }); it('registers two slot components one without predicates - registered components predicates are falsy, the one with no predicates wins', () => { @@ -248,9 +248,9 @@ describe('Slots registry', () => { ], }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with no predicate', - ]); + ); }); it('registers two slot components one without predicates - registered components predicates are truthy, the one with predicates wins', () => { @@ -270,9 +270,9 @@ describe('Slots registry', () => { component: 'this is a toolbar component with no predicate', }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar component with two truth-ish predicates', - ]); + ); }); it('registers 2 + 2 slot components with predicates - No registered component have a truthy predicate', () => { @@ -346,10 +346,12 @@ describe('Slots registry', () => { ContentTypeConditionTrue(['News Item']), ], }); - expect(config.getSlot('toolbar', {})).toEqual([ + expect(config.getSlot('toolbar', {})![0].component).toEqual( 'this is a toolbar save component with a true predicate', + ); + expect(config.getSlot('toolbar', {})![1].component).toEqual( 'this is a toolbar edit component with true predicate', - ]); + ); }); it('getSlotComponents - registers 2 + 2 slot components with predicates', () => { From d73cb36b1e6c4d0f96d14a139ba752bf0574f458 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 26 Feb 2024 12:58:37 +0100 Subject: [PATCH 46/68] Add unit test --- packages/registry/src/registry.test.tsx | 8 ++++---- packages/volto/test-setup-config.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index f325d8b237..183727569d 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -120,13 +120,13 @@ describe('Slots registry', () => { // type Predicate = (predicateValues: unknown) = (predicateValues, args) => boolean // eslint-disable-next-line @typescript-eslint/no-unused-vars - const RouteConditionTrue = (route) => () => true; + const RouteConditionTrue = (route: string) => () => true; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const RouteConditionFalse = (route) => () => false; + const RouteConditionFalse = (route: string) => () => false; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const ContentTypeConditionTrue = (contentType) => () => true; + const ContentTypeConditionTrue = (contentType: string[]) => () => true; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const ContentTypeConditionFalse = (contentType) => () => false; + const ContentTypeConditionFalse = (contentType: string[]) => () => false; it('registers a single slot component with no predicate', () => { config.registerSlotComponent({ diff --git a/packages/volto/test-setup-config.js b/packages/volto/test-setup-config.js index 7ae9bae43b..18a121888e 100644 --- a/packages/volto/test-setup-config.js +++ b/packages/volto/test-setup-config.js @@ -188,3 +188,4 @@ config.set('experimental', { enabled: false, }, }); +config.set('slots', {}); From ba356c3878298df5b1412bde76a8f415e4aa1af6 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 26 Feb 2024 15:42:36 +0100 Subject: [PATCH 47/68] Add navRoot to the equation, this enables Add form correct checks, and Slots under that route. --- packages/registry/src/index.ts | 7 +- packages/types/src/config/Slots.d.ts | 25 ++ packages/types/src/config/index.d.ts | 16 +- packages/types/src/content/common.d.ts | 2 + .../volto/src/components/manage/Form/Form.jsx | 258 ++++++++++-------- .../theme/SlotRenderer/SlotRenderer.test.jsx | 56 ++++ .../theme/SlotRenderer/SlotRenderer.tsx | 9 +- packages/volto/src/config/index.js | 2 +- packages/volto/src/helpers/Slots/index.tsx | 2 +- 9 files changed, 236 insertions(+), 141 deletions(-) create mode 100644 packages/types/src/config/Slots.d.ts create mode 100644 packages/volto/src/components/theme/SlotRenderer/SlotRenderer.test.jsx diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 83dc0d5ba4..9592bc5a06 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -5,6 +5,8 @@ import type { ComponentsConfig, ExperimentalConfig, SettingsConfig, + GetSlotArgs, + GetSlotReturn, SlotComponent, SlotPredicate, SlotsConfig, @@ -184,10 +186,7 @@ class Config { } } - getSlot( - name: string, - args: T, - ): { component: SlotComponent['component']; name: string }[] | undefined { + getSlot(name: string, args: GetSlotArgs): GetSlotReturn { if (!this._data.slots[name]) { return; } diff --git a/packages/types/src/config/Slots.d.ts b/packages/types/src/config/Slots.d.ts new file mode 100644 index 0000000000..87952c3f7d --- /dev/null +++ b/packages/types/src/config/Slots.d.ts @@ -0,0 +1,25 @@ +import type { Content } from '../content'; + +export type SlotPredicate = (args: any) => boolean; + +export type GetSlotArgs = { + content: Content; + pathname: string; + navRoot?: Content; +}; + +export type GetSlotReturn = + | { component: SlotComponent['component']; name: string }[] + | undefined; + +export type SlotComponent = { + component: React.ComponentType; + predicates?: SlotPredicate[]; +}; + +export type SlotManager = { + slots: string[]; + data: Record; +}; + +export type SlotsConfig = Record; diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 6a0dc0e0fc..a62e18c96d 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -2,6 +2,7 @@ import type { SettingsConfig } from './Settings'; import type { BlocksConfig } from './Blocks'; import type { ViewsConfig } from './Views'; import type { WidgetsConfig } from './Widgets'; +import type { SlotsConfig } from './Slots'; export type AddonReducersConfig = Record; @@ -11,20 +12,6 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; -export type SlotPredicate = (args: any) => boolean; - -export type SlotComponent = { - component: React.ComponentType; - predicates?: SlotPredicate[]; -}; - -export type SlotManager = { - slots: string[]; - data: Record; -}; - -export type SlotsConfig = Record; - export type ComponentsConfig = Record< string, { component: React.ComponentType } @@ -46,3 +33,4 @@ export type ConfigData = { export { SettingsConfig, BlocksConfig, ViewsConfig, WidgetsConfig }; export * from './Blocks'; +export * from './Slots'; diff --git a/packages/types/src/content/common.d.ts b/packages/types/src/content/common.d.ts index b1b91edf84..de8922c49f 100644 --- a/packages/types/src/content/common.d.ts +++ b/packages/types/src/content/common.d.ts @@ -2,6 +2,7 @@ import type { BreadcrumbsResponse } from '../services/breadcrumbs'; import type { NavigationResponse } from '../services/navigation'; import type { ActionsResponse } from '../services/actions'; import type { GetTypesResponse } from '../services/types'; +import type { GetNavrootResponse } from '../services/navroot'; import type { GetAliasesResponse } from '../services/aliases'; import type { ContextNavigationResponse } from '../services/contextnavigation'; import type { WorkflowResponse } from '../services/workflow'; @@ -13,6 +14,7 @@ export interface Expanders { breadcrumbs: BreadcrumbsResponse; contextnavigation: ContextNavigationResponse; navigation: NavigationResponse; + navroot: GetNavrootResponse; types: GetTypesResponse; workflow: WorkflowResponse; } diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 2e7c4ff4e6..6ffc4020b0 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -53,6 +53,7 @@ import { } from '@plone/volto/actions'; import { compose } from 'redux'; import config from '@plone/volto/registry'; +import SlotRenderer from '../../theme/SlotRenderer/SlotRenderer'; /** * Form container class. @@ -641,131 +642,147 @@ class Form extends Component { // Removing this from SSR is important, since react-beautiful-dnd supports SSR, // but draftJS don't like it much and the hydration gets messed up this.state.isClient && ( - - { - const newFormData = { - ...formData, - ...newBlockData, - }; - this.setState({ - formData: newFormData, - }); - if (this.props.global) { - this.props.setFormData(newFormData); - } - }} - onSetSelectedBlocks={(blockIds) => - this.setState({ multiSelected: blockIds }) - } - onSelectBlock={this.onSelectBlock} - /> - { - if (this.props.global) { - this.props.setFormData(state.formData); - } - return this.setState(state); - }} - /> - { - const newFormData = { - ...formData, - ...newData, - }; - this.setState({ - formData: newFormData, - }); - if (this.props.global) { - this.props.setFormData(newFormData); - } - }} - onChangeField={this.onChangeField} - onSelectBlock={this.onSelectBlock} - properties={formData} + <> + - {this.state.isClient && this.props.editable && ( - - 0} + + + { + const newFormData = { + ...formData, + ...newBlockData, + }; + this.setState({ + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} + onSetSelectedBlocks={(blockIds) => + this.setState({ multiSelected: blockIds }) + } + onSelectBlock={this.onSelectBlock} + /> + { + if (this.props.global) { + this.props.setFormData(state.formData); + } + return this.setState(state); + }} + /> + { + const newFormData = { + ...formData, + ...newData, + }; + this.setState({ + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} + onChangeField={this.onChangeField} + onSelectBlock={this.onSelectBlock} + properties={formData} + navRoot={navRoot} + type={type} + pathname={this.props.pathname} + selectedBlock={this.state.selected} + multiSelected={this.state.multiSelected} + manage={this.props.isAdminForm} + allowedBlocks={this.props.allowedBlocks} + showRestricted={this.props.showRestricted} + editable={this.props.editable} + isMainForm={this.props.editable} + /> + {this.state.isClient && this.props.editable && ( + - {schema && - map(schema.fieldsets, (fieldset) => ( - -
0} + > + {schema && + map(schema.fieldsets, (fieldset) => ( + - - {fieldset.title} - {metadataFieldsets.includes(fieldset.id) ? ( - - ) : ( - - )} - - - - {map(fieldset.fields, (field, index) => ( - - ))} - - -
-
- ))} -
-
- )} -
+ + {fieldset.title} + {metadataFieldsets.includes(fieldset.id) ? ( + + ) : ( + + )} + + + + {map(fieldset.fields, (field, index) => ( + + ))} + + +
+ + ))} + + + )} + + + + ) ) : ( @@ -931,6 +948,7 @@ const FormIntl = injectIntl(Form, { forwardRef: true }); export default compose( connect( (state, props) => ({ + content: state.content.data, globalData: state.form?.global, metadataFieldsets: state.sidebar?.metadataFieldsets, metadataFieldFocus: state.sidebar?.metadataFieldFocus, diff --git a/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.test.jsx b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.test.jsx new file mode 100644 index 0000000000..0f09535e5a --- /dev/null +++ b/packages/volto/src/components/theme/SlotRenderer/SlotRenderer.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import SlotRenderer from './SlotRenderer'; +import config from '@plone/volto/registry'; + +describe('SlotRenderer Component', () => { + const RouteConditionTrue = () => () => true; + const RouteConditionFalse = () => () => false; + const ContentTypeConditionTrue = () => () => true; + const ContentTypeConditionFalse = () => () => false; + + test('renders a SlotRenderer component for the aboveContentTitle with two slots in the root', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: (props) =>
, + predicates: [RouteConditionTrue()], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: (props) =>
, + predicates: [RouteConditionFalse(), ContentTypeConditionFalse()], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: (props) =>
, + predicates: [RouteConditionFalse()], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: (props) => ( +