Skip to content

Commit

Permalink
Merge pull request #2233 from warrenbuckley/feature/extRegistry-appen…
Browse files Browse the repository at this point in the history
…dCondition

Feature: Adds new method to extensionRegistry for addCondition & addConditions
  • Loading branch information
nielslyngsoe authored Sep 13, 2024
2 parents 97ee4cd + 5f038a4 commit bb583bd
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 10 deletions.
230 changes: 229 additions & 1 deletion src/libs/extension-api/registry/extension.registry.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -453,3 +460,224 @@ describe('UmbExtensionRegistry with exclusions', () => {
expect(extensionRegistry.isRegistered('Umb.Test.Section.Late')).to.be.false;
});
});

describe('Add Conditions', () => {
let extensionRegistry: UmbExtensionRegistry<any>;
let manifests: Array<ManifestWithDynamicConditions>;

beforeEach(() => {
extensionRegistry = new UmbExtensionRegistry<ManifestWithDynamicConditions>();
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<UmbConditionConfigBase> = [
{
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<UmbConditionConfigBase> = [
{
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<UmbConditionConfigBase> = [
{
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');
});
});
89 changes: 80 additions & 9 deletions src/libs/extension-api/registry/extension.registry.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -100,8 +105,26 @@ export class UmbExtensionRegistry<

private _kinds = new UmbBasicState<Array<ManifestKind<ManifestTypes>>>([]);
public readonly kinds = this._kinds.asObservable();

#exclusions: Array<string> = [];

#additionalConditions: Map<string, Array<UmbConditionConfigBase>> = 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<ManifestTypes>): void {
const extensionsValues = this._extensions.getValue();
const extension = extensionsValues.find(
Expand Down Expand Up @@ -136,12 +159,25 @@ export class UmbExtensionRegistry<
};

register(manifest: ManifestTypes | ManifestKind<ManifestTypes>): 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<ManifestTypes>);
return;
}

const isApproved = this.#isExtensionApproved(manifest);
if (!isApproved) {
return;
}

this._extensions.setValue([
...this._extensions.getValue(),
this.#appendAdditionalConditions(manifest as ManifestTypes),
]);
}

getAllExtensions(): Array<ManifestTypes> {
Expand Down Expand Up @@ -177,7 +213,7 @@ export class UmbExtensionRegistry<
return false;
}

#checkExtension(manifest: ManifestTypes | ManifestKind<ManifestTypes>): boolean {
#validateExtension(manifest: ManifestTypes | ManifestKind<ManifestTypes>): boolean {
if (!manifest.type) {
console.error(`Extension is missing type`, manifest);
return false;
Expand All @@ -188,11 +224,9 @@ export class UmbExtensionRegistry<
return false;
}

if (manifest.type === 'kind') {
this.defineKind(manifest as ManifestKind<ManifestTypes>);
return false;
}

return true;
}
#isExtensionApproved(manifest: ManifestTypes | ManifestKind<ManifestTypes>): boolean {
if (!this.#acceptExtension(manifest as ManifestTypes)) {
return false;
}
Expand Down Expand Up @@ -430,4 +464,41 @@ export class UmbExtensionRegistry<
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
) as Observable<Array<ExtensionTypes>>;
}

/**
* 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<UmbConditionConfigBase>} newConditions - An array of conditions to be appended to an extension manifest.
*/
appendConditions(alias: string, newConditions: Array<UmbConditionConfigBase>) {
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;
}
}
}
}

0 comments on commit bb583bd

Please sign in to comment.