+
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..b5c04d775b 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: 'testSlotComponent',
+ component: SlotComponentTest,
+ predicates: [ContentTypeCondition(['Document']), RouteCondition('/hello')],
+ });
+
return config;
};
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/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",
diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts
index dc81917a15..56740160ed 100644
--- a/packages/registry/src/index.ts
+++ b/packages/registry/src/index.ts
@@ -5,6 +5,10 @@ import type {
ComponentsConfig,
ExperimentalConfig,
SettingsConfig,
+ GetSlotArgs,
+ GetSlotReturn,
+ SlotComponent,
+ SlotPredicate,
SlotsConfig,
ViewsConfig,
WidgetsConfig,
@@ -125,7 +129,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;
@@ -147,7 +151,7 @@ class Config {
registerComponent(options: {
name: string;
- dependencies: string[] | string;
+ dependencies?: string[] | string;
component: React.ComponentType;
}) {
const { name, component, dependencies = '' } = options;
@@ -181,6 +185,214 @@ class Config {
}
}
}
+
+ getSlot(name: string, args: GetSlotArgs): GetSlotReturn {
+ if (!this._data.slots[name]) {
+ return;
+ }
+ 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.
+ let noPredicateComponent: SlotComponent | undefined;
+ 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) =>
+ predicate(args),
+ );
+ } else {
+ // We mark the one with no predicates
+ noPredicateComponent = slotComponent;
+ }
+
+ // If all the predicates are truthy
+ if (isPredicateTrueFound) {
+ slotComponents.push({
+ component: slotComponent.component,
+ name: slotName,
+ });
+ // We "reset" the marker, we already found a candidate
+ noPredicateComponent = undefined;
+ break;
+ }
+ }
+
+ if (noPredicateComponent) {
+ slotComponents.push({
+ component: noPredicateComponent.component,
+ name: slotName,
+ });
+ }
+ }
+
+ return slotComponents;
+ }
+
+ registerSlotComponent(options: {
+ slot: string;
+ name: string;
+ predicates?: SlotPredicate[];
+ component: SlotComponent['component'];
+ }): void {
+ const { name, component, predicates, slot } = options;
+
+ if (!component) {
+ throw new Error('No component provided');
+ }
+ if (!predicates) {
+ // Test if there's already one registered, we only support one
+ const hasRegisteredNoPredicatesComponent = this._data.slots?.[
+ slot
+ ]?.data?.[name]?.find(({ predicates }) => !predicates);
+ 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.`,
+ );
+ }
+ }
+
+ 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] = [];
+ }
+
+ const currentSlotComponents = 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;
+ }
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Not setting the slot component displayName because ${error}`,
+ );
+ }
+
+ 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,
+ name,
+ position,
+ action,
+ target,
+ }: {
+ slot: string;
+ name: string;
+ position?: number;
+ action?: 'after' | 'before' | 'first' | 'last';
+ target?: string;
+ }) {
+ if (!position && !action) {
+ throw new Error(`At least a position or action is required as argument`);
+ }
+ if (position && action) {
+ throw new Error(
+ `You should provide only one of position or action as arguments`,
+ );
+ }
+ if ((action == 'after' || action == 'before') && !target) {
+ throw new Error(
+ `No action target set. You should provide the name of a slot component as target when action is 'after' or 'before'.`,
+ );
+ }
+
+ const currentSlot = this._data.slots[slot];
+ if (!slot || !currentSlot) {
+ throw new Error(`No slot ${slot} found`);
+ }
+ const origin = currentSlot.slots.indexOf(name);
+ const result = Array.from(currentSlot.slots);
+ const [removed] = result.splice(origin, 1);
+
+ if (action) {
+ let targetIdx = 0;
+ if (target) {
+ targetIdx = currentSlot.slots.indexOf(target);
+ }
+ switch (action) {
+ case 'after':
+ result.splice(targetIdx, 0, removed);
+ break;
+ case 'before':
+ result.splice(targetIdx - 1, 0, removed);
+ break;
+ case 'last':
+ result.push(removed);
+ break;
+ case 'first':
+ result.unshift(removed);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ if (position) {
+ result.splice(position, 0, removed);
+ }
+
+ currentSlot.slots = result;
+ }
+
+ 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`);
+ }
+ const result = currentSlotComponents.slice();
+ currentSlot.data[name] = result.splice(position, 1);
+ }
}
const instance = new Config();
diff --git a/packages/registry/src/registry.test.jsx b/packages/registry/src/registry.test.jsx
deleted file mode 100644
index 861d26701f..0000000000
--- a/packages/registry/src/registry.test.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import config from './index';
-import { describe, expect, it } from 'vitest';
-
-config.set('components', {
- Toolbar: { component: 'this is the Toolbar component' },
- 'Toolbar.Types': { component: 'this is the Types component' },
- 'Teaser|News Item': { component: 'This is the News Item Teaser component' },
-});
-
-describe('registry', () => {
- it('get components', () => {
- expect(config.getComponent('Toolbar').component).toEqual(
- 'this is the Toolbar component',
- );
- });
- it('get components with context', () => {
- expect(
- config.getComponent({ name: 'Teaser', dependencies: 'News Item' })
- .component,
- ).toEqual('This is the News Item Teaser component');
- });
- it('get components with dots (as an object)', () => {
- expect(config.getComponent({ name: 'Toolbar.Types' }).component).toEqual(
- 'this is the Types component',
- );
- });
- it('get components with | and spaces (as a string)', () => {
- expect(config.getComponent('Teaser|News Item').component).toEqual(
- 'This is the News Item Teaser component',
- );
- });
- it('resolves unexistent component (as a string)', () => {
- expect(config.getComponent('Toolbar.Doh').component).toEqual(undefined);
- expect(config.getComponent('Toolbar.Doh')).toEqual({});
- });
- it('registers and gets a component by name (as string)', () => {
- config.registerComponent({
- name: 'Toolbar.Bar',
- component: 'this is a Bar component',
- });
- expect(config.getComponent('Toolbar.Bar').component).toEqual(
- 'this is a Bar component',
- );
- });
- it('registers and gets a component by name (as an object)', () => {
- config.registerComponent({
- name: 'Toolbar.Bar',
- component: 'this is a Bar component',
- });
- expect(config.getComponent({ name: 'Toolbar.Bar' }).component).toEqual(
- 'this is a Bar component',
- );
- });
- it('registers and gets a component by name (as an object) - check displayName', () => {
- config.registerComponent({
- name: 'Toolbar.Bar',
- component: () =>