New extension points for repository overview (#1828)

The landing-page-plugin is being reworked and integrated into the repository overview. This requires new extension points and slightly adjusted components to better match the repository overview page visually. Also, binder options can now be passed as an object which offer a new priority option that causes sorting in descending order.
This commit is contained in:
Konstantin Schaper
2021-10-19 09:31:40 +02:00
committed by GitHub
parent 35f4cb3e61
commit 57aacba03a
15 changed files with 347 additions and 78 deletions

View File

@@ -74,7 +74,7 @@ describe("binder tests", () => {
binder.bind("hitchhiker.trillian", "earth2", (props: Props) => props.category === "a");
const extensions = binder.getExtensions("hitchhiker.trillian", {
category: "b"
category: "b",
});
expect(extensions).toEqual(["earth"]);
});
@@ -109,7 +109,7 @@ describe("binder tests", () => {
expect(binderExtensionA).not.toBeNull();
binder.bind<TestExtensionPointB>("test.extension.b", 2);
const binderExtensionsB = binder.getExtensions<TestExtensionPointB>("test.extension.b", {
testProp: [true, false]
testProp: [true, false],
});
expect(binderExtensionsB).toHaveLength(1);
binder.bind("test.extension.c", 2, () => false);
@@ -123,24 +123,78 @@ describe("binder tests", () => {
value: string;
};
type MarkdownCodeLanguageRendererExtensionPoint<
S extends string | undefined = undefined
> = SimpleDynamicExtensionPointDefinition<
"markdown-renderer.code.",
(props: any) => any,
MarkdownCodeLanguageRendererProps,
S
>;
type MarkdownCodeLanguageRendererExtensionPoint<S extends string | undefined = undefined> =
SimpleDynamicExtensionPointDefinition<
"markdown-renderer.code.",
(props: any) => any,
MarkdownCodeLanguageRendererProps,
S
>;
type UmlExtensionPoint = MarkdownCodeLanguageRendererExtensionPoint<"uml">;
binder.bind<UmlExtensionPoint>("markdown-renderer.code.uml", props => props.value);
binder.bind<UmlExtensionPoint>("markdown-renderer.code.uml", (props) => props.value);
const language = "uml";
const extensionPointName = `markdown-renderer.code.${language}` as const;
const dynamicExtension = binder.getExtension<MarkdownCodeLanguageRendererExtensionPoint>(extensionPointName, {
language: "uml",
value: "const a = 2;"
value: "const a = 2;",
});
expect(dynamicExtension).not.toBeNull();
});
it("should allow options parameter", () => {
binder.bind("hitchhiker.trillian", "planetA", {
predicate: () => true,
extensionName: "zeroWaste",
});
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetA"]);
});
it("should allow empty options parameter", () => {
binder.bind("hitchhiker.trillian", "planetA", {});
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetA"]);
});
it("should allow options parameter with only predicate", () => {
binder.bind("hitchhiker.trillian", "planetA", {
predicate: () => true,
});
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetA"]);
});
it("should allow options parameter with only extensionName", () => {
binder.bind("hitchhiker.trillian", "planetA", {
extensionName: "zeroWaste",
});
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetA"]);
});
it("should order by priority in descending order", () => {
binder.bind("hitchhiker.trillian", "planetA", { priority: 10 });
binder.bind("hitchhiker.trillian", "planetB", { priority: 50 });
binder.bind("hitchhiker.trillian", "planetC", { priority: 100 });
binder.bind("hitchhiker.trillian", "planetD", { priority: 75 });
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetC", "planetD", "planetB", "planetA"]);
});
it("should order by priority over ordering by name", () => {
binder.bind("hitchhiker.trillian", "planetA", { priority: 10, extensionName: "ignore" });
binder.bind("hitchhiker.trillian", "planetB", { priority: 50 });
binder.bind("hitchhiker.trillian", "planetC", { priority: 100, extensionName: "me" });
binder.bind("hitchhiker.trillian", "planetD", { priority: 75 });
const extensions = binder.getExtensions("hitchhiker.trillian");
expect(extensions).toEqual(["planetC", "planetD", "planetB", "planetA"]);
});
});

View File

@@ -28,6 +28,7 @@ type ExtensionRegistration<P, T> = {
predicate: Predicate<P>;
extension: T;
extensionName: string;
priority: number;
};
export type ExtensionPointDefinition<N extends string, T, P = undefined> = {
@@ -36,7 +37,26 @@ export type ExtensionPointDefinition<N extends string, T, P = undefined> = {
props: P;
};
export type SimpleDynamicExtensionPointDefinition<P extends string, T, Props, S extends string | undefined> = ExtensionPointDefinition<S extends string ? `${P}${S}` : `${P}${string}`, T, Props>;
export type SimpleDynamicExtensionPointDefinition<P extends string, T, Props, S extends string | undefined> =
ExtensionPointDefinition<S extends string ? `${P}${S}` : `${P}${string}`, T, Props>;
export type BindOptions<Props> = {
predicate?: Predicate<Props>;
/**
* Extensions are ordered by name (ASC).
*/
extensionName?: string;
/**
* Extensions are ordered by priority (DESC).
*/
priority?: number;
};
function isBindOptions<Props>(input?: Predicate<Props> | BindOptions<Props>): input is BindOptions<Props> {
return typeof input !== "function" && typeof input === "object";
}
/**
* Binder is responsible for binding plugin extensions to their corresponding extension points.
@@ -60,10 +80,7 @@ export class Binder {
* @param extension provided extension
* @param predicate to decide if the extension gets rendered for the given props
*/
bind<E extends ExtensionPointDefinition<string, unknown, undefined>>(
extensionPoint: E["name"],
extension: E["type"]
): void;
bind<E extends ExtensionPointDefinition<string, unknown>>(extensionPoint: E["name"], extension: E["type"]): void;
bind<E extends ExtensionPointDefinition<string, unknown, any>>(
extensionPoint: E["name"],
extension: E["type"],
@@ -73,17 +90,38 @@ export class Binder {
bind<E extends ExtensionPointDefinition<string, unknown, any>>(
extensionPoint: E["name"],
extension: E["type"],
predicate?: Predicate<E["props"]>,
options?: BindOptions<E["props"]>
): void;
bind<E extends ExtensionPointDefinition<string, unknown, any>>(
extensionPoint: E["name"],
extension: E["type"],
predicateOrOptions?: Predicate<E["props"]> | BindOptions<E["props"]>,
extensionName?: string
) {
let predicate: Predicate<E["props"]> = () => true;
let priority = 0;
if (isBindOptions(predicateOrOptions)) {
if (predicateOrOptions.predicate) {
predicate = predicateOrOptions.predicate;
}
if (predicateOrOptions.extensionName) {
extensionName = predicateOrOptions.extensionName;
}
if (typeof predicateOrOptions.priority === "number") {
priority = predicateOrOptions.priority;
}
} else if (predicateOrOptions) {
predicate = predicateOrOptions;
}
if (!this.extensionPoints[extensionPoint]) {
this.extensionPoints[extensionPoint] = [];
}
const registration = {
predicate: predicate ? predicate : () => true,
predicate,
extension,
extensionName: extensionName ? extensionName : ""
};
extensionName: extensionName ? extensionName : "",
priority,
} as ExtensionRegistration<E["props"], E["type"]>;
this.extensionPoints[extensionPoint].push(registration);
}
@@ -128,10 +166,10 @@ export class Binder {
): Array<E["type"]> {
let registrations = this.extensionPoints[extensionPoint] || [];
if (props) {
registrations = registrations.filter(reg => reg.predicate(props));
registrations = registrations.filter((reg) => reg.predicate(props));
}
registrations.sort(this.sortExtensions);
return registrations.map(reg => reg.extension);
return registrations.map((reg) => reg.extension);
}
/**
@@ -151,7 +189,11 @@ export class Binder {
const regA = a.extensionName ? a.extensionName.toUpperCase() : "";
const regB = b.extensionName ? b.extensionName.toUpperCase() : "";
if (regA === "" && regB !== "") {
if (a.priority > b.priority) {
return -1;
} else if (a.priority < b.priority) {
return 1;
} else if (regA === "" && regB !== "") {
return 1;
} else if (regA !== "" && regB === "") {
return -1;

View File

@@ -135,3 +135,24 @@ export type PrimaryNavigationLogoutButtonExtension = ExtensionPointDefinition<
"primary-navigation.logout",
PrimaryNavigationLogoutButtonProps
>;
export type RepositoryOverviewTopExtensionProps = {
page: number;
search: string;
namespace?: string;
};
export type RepositoryOverviewTopExtension = ExtensionPointDefinition<
"repository.overview.top",
React.ComponentType<RepositoryOverviewTopExtensionProps>,
RepositoryOverviewTopExtensionProps
>;
export type RepositoryOverviewLeftExtension = ExtensionPointDefinition<"repository.overview.left", React.ComponentType>;
export type RepositoryOverviewTitleExtension = ExtensionPointDefinition<
"repository.overview.title",
React.ComponentType
>;
export type RepositoryOverviewSubtitleExtension = ExtensionPointDefinition<
"repository.overview.subtitle",
React.ComponentType
>;