Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Adds new method to extensionRegistry for appendCondition & appendConditions #2233

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5a6dcf8
Adds new method to extensionRegistry for appendCondition
warrenbuckley Aug 22, 2024
c6c80dd
Adds appendConditions to allow adding multiple in one go
warrenbuckley Aug 23, 2024
21cf587
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Aug 26, 2024
2b2e2b7
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Aug 26, 2024
561dff0
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Aug 27, 2024
be2f95a
Adds prependCondition and prependConditions so you can make sure your…
warrenbuckley Aug 29, 2024
6e3a3e6
Adds _whenExtensionAliasIsRegistered as a promise so that we basicall…
warrenbuckley Aug 29, 2024
3ba8315
simplify _whenExtensionAliasIsRegistered
madsrasmussen Aug 30, 2024
130646d
split some tests
madsrasmussen Aug 30, 2024
017f1b3
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Aug 30, 2024
900e98b
Rework of tests to pass and minor fix to _whenExtensionAliasIsRegiste…
warrenbuckley Aug 30, 2024
7a0a86e
Trying to add a test to show that a late registered extension works -…
warrenbuckley Aug 30, 2024
120a631
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 1, 2024
19f0431
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 2, 2024
d902e2c
Remove the prepend conditions and rename append to addCondition and a…
warrenbuckley Sep 2, 2024
28f9c7c
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 2, 2024
17b9ee5
Update tests
warrenbuckley Sep 2, 2024
ccd4dd4
Fix up code so test passes & my rough entrypoint demo of usage works
warrenbuckley Sep 3, 2024
f826b76
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 9, 2024
3f1d639
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 9, 2024
a577823
Merge branch 'main' into feature/extRegistry-appendCondition
nielslyngsoe Sep 11, 2024
f617a41
Merge branch 'main' into feature/extRegistry-appendCondition
warrenbuckley Sep 11, 2024
a40c759
Merge branch 'main' into feature/extRegistry-appendCondition
nielslyngsoe Sep 12, 2024
75309b3
Merge remote-tracking branch 'origin/v15/dev' into feature/extRegistr…
nielslyngsoe Sep 12, 2024
6f0df55
import type
nielslyngsoe Sep 12, 2024
46ec826
remove test code
nielslyngsoe Sep 12, 2024
d8c59cb
refactor for a more direct appending implementation
nielslyngsoe Sep 12, 2024
5f038a4
Merge branch 'v15/dev' into feature/extRegistry-appendCondition
nielslyngsoe Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}
}
Loading