diff --git a/CHANGELOG.md b/CHANGELOG.md index 760da98469..a715533bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added footer extension points for links and avatar - Create OpenAPI specification during build +- Extension point entries with supplied extensionName are sorted ascending ### Changed - New footer design diff --git a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx index f2e3669a5e..d99c3d12ce 100644 --- a/scm-ui/ui-components/src/config/ConfigurationBinder.tsx +++ b/scm-ui/ui-components/src/config/ConfigurationBinder.tsx @@ -41,7 +41,7 @@ class ConfigurationBinder { }); // bind navigation link to extension point - binder.bind("admin.setting", ConfigNavLink, configPredicate); + binder.bind("admin.setting", ConfigNavLink, configPredicate, labelI18nKey); // route for global configuration, passes the link from the index resource to component const ConfigRoute = ({ url, links, ...additionalProps }: GlobalRouteProps) => { diff --git a/scm-ui/ui-extensions/src/binder.test.ts b/scm-ui/ui-extensions/src/binder.test.ts index 6915f04282..352218aadd 100644 --- a/scm-ui/ui-extensions/src/binder.test.ts +++ b/scm-ui/ui-extensions/src/binder.test.ts @@ -13,31 +13,31 @@ describe("binder tests", () => { }); it("should return the binded extensions", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - const extensions = binder.getExtensions("hitchhicker.trillian"); + const extensions = binder.getExtensions("hitchhiker.trillian"); expect(extensions).toEqual(["heartOfGold", "earth"]); }); it("should return the first bound extension", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - binder.bind("hitchhicker.trillian", "earth"); + binder.bind("hitchhiker.trillian", "heartOfGold"); + binder.bind("hitchhiker.trillian", "earth"); - expect(binder.getExtension("hitchhicker.trillian")).toBe("heartOfGold"); + expect(binder.getExtension("hitchhiker.trillian")).toBe("heartOfGold"); }); it("should return null if no extension was bound", () => { - expect(binder.getExtension("hitchhicker.trillian")).toBe(null); + expect(binder.getExtension("hitchhiker.trillian")).toBe(null); }); it("should return true, if an extension is bound", () => { - binder.bind("hitchhicker.trillian", "heartOfGold"); - expect(binder.hasExtension("hitchhicker.trillian")).toBe(true); + binder.bind("hitchhiker.trillian", "heartOfGold"); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(true); }); it("should return false, if no extension is bound", () => { - expect(binder.hasExtension("hitchhicker.trillian")).toBe(false); + expect(binder.hasExtension("hitchhiker.trillian")).toBe(false); }); type Props = { @@ -45,13 +45,34 @@ describe("binder tests", () => { }; it("should return only extensions which predicates matches", () => { - binder.bind("hitchhicker.trillian", "heartOfGold", (props: Props) => props.category === "a"); - binder.bind("hitchhicker.trillian", "earth", (props: Props) => props.category === "b"); - binder.bind("hitchhicker.trillian", "earth2", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "heartOfGold", (props: Props) => props.category === "a"); + binder.bind("hitchhiker.trillian", "earth", (props: Props) => props.category === "b"); + binder.bind("hitchhiker.trillian", "earth2", (props: Props) => props.category === "a"); - const extensions = binder.getExtensions("hitchhicker.trillian", { + const extensions = binder.getExtensions("hitchhiker.trillian", { category: "b" }); expect(extensions).toEqual(["earth"]); }); + + it("should return extensions in ascending order", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetB", () => true, "EPSILON"); + binder.bind("hitchhiker.trillian", "planetC", () => true, "emptyBin"); + binder.bind("hitchhiker.trillian", "planetD", () => true, "absolute"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions).toEqual(["planetD", "planetC", "planetB", "planetA"]); + }); + + it("should return extensions starting with entries with specified extensionName", () => { + binder.bind("hitchhiker.trillian", "planetA", () => true); + binder.bind("hitchhiker.trillian", "planetB", () => true, "zeroWaste"); + binder.bind("hitchhiker.trillian", "planetC", () => true); + binder.bind("hitchhiker.trillian", "planetD", () => true, "emptyBin"); + + const extensions = binder.getExtensions("hitchhiker.trillian"); + expect(extensions[0]).toEqual("planetD"); + expect(extensions[1]).toEqual("planetB"); + }); }); diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts index e3f2b9f120..ca7c395333 100644 --- a/scm-ui/ui-extensions/src/binder.ts +++ b/scm-ui/ui-extensions/src/binder.ts @@ -3,6 +3,7 @@ type Predicate = (props: any) => boolean; type ExtensionRegistration = { predicate: Predicate; extension: any; + extensionName: string; }; /** @@ -27,13 +28,14 @@ export class Binder { * @param extension provided extension * @param predicate to decide if the extension gets rendered for the given props */ - bind(extensionPoint: string, extension: any, predicate?: Predicate) { + bind(extensionPoint: string, extension: any, predicate?: Predicate, extensionName?: string) { if (!this.extensionPoints[extensionPoint]) { this.extensionPoints[extensionPoint] = []; } const registration = { predicate: predicate ? predicate : () => true, - extension + extension, + extensionName: extensionName ? extensionName : "" }; this.extensionPoints[extensionPoint].push(registration); } @@ -63,6 +65,7 @@ export class Binder { if (props) { registrations = registrations.filter(reg => reg.predicate(props || {})); } + registrations.sort(this.sortExtensions); return registrations.map(reg => reg.extension); } @@ -72,6 +75,25 @@ export class Binder { hasExtension(extensionPoint: string, props?: object): boolean { return this.getExtensions(extensionPoint, props).length > 0; } + + /** + * Sort extensions in ascending order, starting with entries with specified extensionName. + */ + sortExtensions = (a: ExtensionRegistration, b: ExtensionRegistration) => { + const regA = a.extensionName ? a.extensionName.toUpperCase() : ""; + const regB = b.extensionName ? b.extensionName.toUpperCase() : ""; + + if (regA === "" && regB !== "") { + return 1; + } else if (regA !== "" && regB === "") { + return -1; + } else if (regA > regB) { + return 1; + } else if (regA < regB) { + return -1; + } + return 0; + }; } // singleton binder