diff --git a/src/libs/extension-api/registry/extension.registry.test.ts b/src/libs/extension-api/registry/extension.registry.test.ts index 83ee09d37b..caed039f60 100644 --- a/src/libs/extension-api/registry/extension.registry.test.ts +++ b/src/libs/extension-api/registry/extension.registry.test.ts @@ -1,4 +1,11 @@ -import type { ManifestElementWithElementName, ManifestKind, ManifestBase } from '../types/index.js'; +import type { WorkspaceAliasConditionConfig } from '@umbraco-cms/backoffice/workspace'; +import type { + ManifestElementWithElementName, + ManifestKind, + ManifestBase, + ManifestWithDynamicConditions, + UmbConditionConfigBase, +} from '../types/index.js'; import { UmbExtensionRegistry } from './extension.registry.js'; import { expect } from '@open-wc/testing'; @@ -453,3 +460,224 @@ describe('UmbExtensionRegistry with exclusions', () => { expect(extensionRegistry.isRegistered('Umb.Test.Section.Late')).to.be.false; }); }); + +describe('Add Conditions', () => { + let extensionRegistry: UmbExtensionRegistry; + let manifests: Array; + + beforeEach(() => { + extensionRegistry = new UmbExtensionRegistry(); + manifests = [ + { + type: 'section', + name: 'test-section-1', + alias: 'Umb.Test.Section.1', + weight: 1, + conditions: [ + { + alias: 'Umb.Test.Condition.Invalid', + }, + ], + }, + { + type: 'section', + name: 'test-section-2', + alias: 'Umb.Test.Section.2', + weight: 200, + }, + ]; + + manifests.forEach((manifest) => extensionRegistry.register(manifest)); + + extensionRegistry.register({ + type: 'condition', + name: 'test-condition-invalid', + alias: 'Umb.Test.Condition.Invalid', + }); + }); + + it('should have the extensions registered', () => { + expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.true; + expect(extensionRegistry.isRegistered('Umb.Test.Section.2')).to.be.true; + expect(extensionRegistry.isRegistered('Umb.Test.Condition.Invalid')).to.be.true; + expect(extensionRegistry.isRegistered('Umb.Test.Condition.Valid')).to.be.false; + }); + + it('allows an extension condition to be updated', async () => { + const ext = extensionRegistry.getByAlias('Umb.Test.Section.1') as ManifestWithDynamicConditions; + expect(ext.conditions?.length).to.equal(1); + + // Register new condition as if I was in my own entrypoint + extensionRegistry.register({ + type: 'condition', + name: 'test-condition-valid', + alias: 'Umb.Test.Condition.Valid', + }); + + // Add the new condition to the extension + const conditionToAdd: UmbConditionConfigBase = { + alias: 'Umb.Test.Condition.Valid', + }; + await extensionRegistry.appendCondition('Umb.Test.Section.1', conditionToAdd); + + // Check new condition is registered + expect(extensionRegistry.isRegistered('Umb.Test.Condition.Valid')).to.be.true; + + // Verify the extension now has two conditions and in correct order with aliases + const updatedExt = extensionRegistry.getByAlias('Umb.Test.Section.1') as ManifestWithDynamicConditions; + expect(updatedExt.conditions?.length).to.equal(2); + expect(updatedExt.conditions?.[0]?.alias).to.equal('Umb.Test.Condition.Invalid'); + expect(updatedExt.conditions?.[1]?.alias).to.equal('Umb.Test.Condition.Valid'); + + // Verify the other extension was not updated: + const otherExt = extensionRegistry.getByAlias('Umb.Test.Section.2') as ManifestWithDynamicConditions; + expect(otherExt.conditions).to.be.undefined; + + // Add a condition with a specific config to Section2 + const workspaceCondition: WorkspaceAliasConditionConfig = { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + }; + + await extensionRegistry.appendCondition('Umb.Test.Section.2', workspaceCondition); + + const updatedWorkspaceExt = extensionRegistry.getByAlias('Umb.Test.Section.2') as ManifestWithDynamicConditions; + expect(updatedWorkspaceExt.conditions?.length).to.equal(1); + expect(updatedWorkspaceExt.conditions?.[0]?.alias).to.equal('Umb.Condition.WorkspaceAlias'); + }); + + it('allows an extension to update with multiple conditions', async () => { + const ext = extensionRegistry.getByAlias('Umb.Test.Section.1') as ManifestWithDynamicConditions; + expect(ext.conditions?.length).to.equal(1); + + const conditions: Array = [ + { + alias: 'Umb.Test.Condition.Valid', + }, + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + } as WorkspaceAliasConditionConfig, + ]; + + await extensionRegistry.appendConditions('Umb.Test.Section.1', conditions); + + const extUpdated = extensionRegistry.getByAlias('Umb.Test.Section.1') as ManifestWithDynamicConditions; + expect(extUpdated.conditions?.length).to.equal(3); + expect(extUpdated.conditions?.[0]?.alias).to.equal('Umb.Test.Condition.Invalid'); + expect(extUpdated.conditions?.[1]?.alias).to.equal('Umb.Test.Condition.Valid'); + expect(extUpdated.conditions?.[2]?.alias).to.equal('Umb.Condition.WorkspaceAlias'); + }); + + it('allows conditions to be prepended when an extension is loaded later on', async () => { + const conditions: Array = [ + { + alias: 'Umb.Test.Condition.Invalid', + }, + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + } as WorkspaceAliasConditionConfig, + ]; + + // Prepend the conditions, but do not await this. + extensionRegistry.appendConditions('Late.Extension.To.Be.Loaded', conditions); + + // Make sure the extension is not registered YET + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.false; + + // Register the extension LATE/after the conditions have been added + extensionRegistry.register({ + type: 'section', + name: 'Late Section Extension with one condition', + alias: 'Late.Extension.To.Be.Loaded', + weight: 200, + conditions: [ + { + alias: 'Umb.Test.Condition.Valid', + }, + ], + }); + + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.true; + + const extUpdated = extensionRegistry.getByAlias('Late.Extension.To.Be.Loaded') as ManifestWithDynamicConditions; + + expect(extUpdated.conditions?.length).to.equal(3); + expect(extUpdated.conditions?.[0]?.alias).to.equal('Umb.Test.Condition.Valid'); + expect(extUpdated.conditions?.[1]?.alias).to.equal('Umb.Test.Condition.Invalid'); + expect(extUpdated.conditions?.[2]?.alias).to.equal('Umb.Condition.WorkspaceAlias'); + }); + + /** + * As of current state, it is by design without further reasons to why, but it is made so additional conditions are only added to a current or next time registered manifest. + * Meaning if it happens to be unregistered and re-registered it does not happen again. + * Unless the exact same appending of conditions happens again. [NL] + * + * This makes sense if extensions gets offloaded and re-registered, but the extension that registered additional conditions didn't get loaded/registered second time. Therefor they need to be re-registered for such to work. [NL] + */ + it('only append conditions to the next time the extension is registered', async () => { + const conditions: Array = [ + { + alias: 'Umb.Test.Condition.Invalid', + }, + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + } as WorkspaceAliasConditionConfig, + ]; + + // Prepend the conditions, but do not await this. + extensionRegistry.appendConditions('Late.Extension.To.Be.Loaded', conditions); + + // Make sure the extension is not registered YET + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.false; + + // Register the extension LATE/after the conditions have been added + extensionRegistry.register({ + type: 'section', + name: 'Late Section Extension with one condition', + alias: 'Late.Extension.To.Be.Loaded', + weight: 200, + conditions: [ + { + alias: 'Umb.Test.Condition.Valid', + }, + ], + }); + + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.true; + + const extUpdateFirstTime = extensionRegistry.getByAlias( + 'Late.Extension.To.Be.Loaded', + ) as ManifestWithDynamicConditions; + expect(extUpdateFirstTime.conditions?.length).to.equal(3); + + extensionRegistry.unregister('Late.Extension.To.Be.Loaded'); + + // Make sure the extension is not registered YET + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.false; + + // Register the extension LATE/after the conditions have been added + extensionRegistry.register({ + type: 'section', + name: 'Late Section Extension with one condition', + alias: 'Late.Extension.To.Be.Loaded', + weight: 200, + conditions: [ + { + alias: 'Umb.Test.Condition.Valid', + }, + ], + }); + + expect(extensionRegistry.isRegistered('Late.Extension.To.Be.Loaded')).to.be.true; + + const extUpdateSecondTime = extensionRegistry.getByAlias( + 'Late.Extension.To.Be.Loaded', + ) as ManifestWithDynamicConditions; + + expect(extUpdateSecondTime.conditions?.length).to.equal(1); + expect(extUpdateSecondTime.conditions?.[0]?.alias).to.equal('Umb.Test.Condition.Valid'); + }); +}); diff --git a/src/libs/extension-api/registry/extension.registry.ts b/src/libs/extension-api/registry/extension.registry.ts index 2680d599e9..e1535825a4 100644 --- a/src/libs/extension-api/registry/extension.registry.ts +++ b/src/libs/extension-api/registry/extension.registry.ts @@ -1,4 +1,9 @@ -import type { ManifestBase, ManifestKind } from '../types/index.js'; +import type { + ManifestBase, + ManifestKind, + ManifestWithDynamicConditions, + UmbConditionConfigBase, +} from '../types/index.js'; import type { SpecificManifestTypeOrManifestBase } from '../types/map.types.js'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; @@ -100,8 +105,26 @@ export class UmbExtensionRegistry< private _kinds = new UmbBasicState>>([]); public readonly kinds = this._kinds.asObservable(); + #exclusions: Array = []; + #additionalConditions: Map> = new Map(); + #appendAdditionalConditions(manifest: ManifestTypes) { + const newConditions = this.#additionalConditions.get(manifest.alias); + if (newConditions) { + // Append the condition to the extensions conditions array + if ((manifest as ManifestWithDynamicConditions).conditions) { + for (const condition of newConditions) { + (manifest as ManifestWithDynamicConditions).conditions!.push(condition); + } + } else { + (manifest as ManifestWithDynamicConditions).conditions = newConditions; + } + this.#additionalConditions.delete(manifest.alias); + } + return manifest; + } + defineKind(kind: ManifestKind): void { const extensionsValues = this._extensions.getValue(); const extension = extensionsValues.find( @@ -136,12 +159,25 @@ export class UmbExtensionRegistry< }; register(manifest: ManifestTypes | ManifestKind): void { - const isValid = this.#checkExtension(manifest); + const isValid = this.#validateExtension(manifest); if (!isValid) { return; } - this._extensions.setValue([...this._extensions.getValue(), manifest as ManifestTypes]); + if (manifest.type === 'kind') { + this.defineKind(manifest as ManifestKind); + return; + } + + const isApproved = this.#isExtensionApproved(manifest); + if (!isApproved) { + return; + } + + this._extensions.setValue([ + ...this._extensions.getValue(), + this.#appendAdditionalConditions(manifest as ManifestTypes), + ]); } getAllExtensions(): Array { @@ -177,7 +213,7 @@ export class UmbExtensionRegistry< return false; } - #checkExtension(manifest: ManifestTypes | ManifestKind): boolean { + #validateExtension(manifest: ManifestTypes | ManifestKind): boolean { if (!manifest.type) { console.error(`Extension is missing type`, manifest); return false; @@ -188,11 +224,9 @@ export class UmbExtensionRegistry< return false; } - if (manifest.type === 'kind') { - this.defineKind(manifest as ManifestKind); - return false; - } - + return true; + } + #isExtensionApproved(manifest: ManifestTypes | ManifestKind): boolean { if (!this.#acceptExtension(manifest as ManifestTypes)) { return false; } @@ -430,4 +464,41 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchArrayMemoization), ) as Observable>; } + + /** + * Append a new condition to an existing extension + * Useful to add a condition for example the Save And Publish workspace action shipped by core. + * @param {string} alias - The alias of the extension to append the condition to. + * @param {UmbConditionConfigBase} newCondition - The condition to append to the extension. + */ + appendCondition(alias: string, newCondition: UmbConditionConfigBase) { + this.appendConditions(alias, [newCondition]); + } + + /** + * Appends an array of conditions to an existing extension + * @param {string} alias - The alias of the extension to append the condition to + * @param {Array} newConditions - An array of conditions to be appended to an extension manifest. + */ + appendConditions(alias: string, newConditions: Array) { + const existingConditionsToBeAdded = this.#additionalConditions.get(alias); + this.#additionalConditions.set( + alias, + existingConditionsToBeAdded ? [...existingConditionsToBeAdded, ...newConditions] : newConditions, + ); + + const allExtensions = this._extensions.getValue(); + for (const extension of allExtensions) { + if (extension.alias === alias) { + // Replace the existing extension with the updated one + allExtensions[allExtensions.indexOf(extension)] = this.#appendAdditionalConditions(extension as ManifestTypes); + + // Update the main extensions collection/observable + this._extensions.setValue(allExtensions); + + //Stop the search: + break; + } + } + } }