name="changeset.description"
props={{
changeset,
diff --git a/scm-ui/ui-extensions/src/ExtensionPoint.tsx b/scm-ui/ui-extensions/src/ExtensionPoint.tsx
index 83622f0918..d0d94607b3 100644
--- a/scm-ui/ui-extensions/src/ExtensionPoint.tsx
+++ b/scm-ui/ui-extensions/src/ExtensionPoint.tsx
@@ -21,39 +21,70 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React, { FC, ReactNode } from "react";
-import { Binder } from "./binder";
+import React, { PropsWithChildren, ReactNode } from "react";
+import { Binder, ExtensionPointDefinition } from "./binder";
import useBinder from "./useBinder";
+export type Renderable = React.ReactElement | React.ComponentType
;
+export type RenderableExtensionPointDefinition<
+ Name extends string = string,
+ P = undefined
+> = ExtensionPointDefinition, P>;
+
+export type SimpleRenderableDynamicExtensionPointDefinition<
+ Prefix extends string,
+ Suffix extends string | undefined,
+ Properties
+> = RenderableExtensionPointDefinition;
+
+/**
+ * @deprecated Obsolete type
+ */
type PropTransformer = (props: object) => object;
-type Props = {
- name: string;
+type BaseProps = {
+ name: E["name"];
renderAll?: boolean;
- props?: object;
+ /**
+ * @deprecated Obsolete property, do not use
+ */
propTransformer?: PropTransformer;
wrapper?: boolean;
};
-const createInstance = (Component: any, props: object, key?: number) => {
- const instanceProps = {
- ...props,
- ...(Component.props || {}),
- key,
- };
- if (React.isValidElement(Component)) {
- return React.cloneElement(Component, instanceProps);
- }
- return ;
-};
+type Props = BaseProps &
+ (E["props"] extends undefined
+ ? { props?: E["props"] }
+ : {
+ props: E["props"];
+ });
-const renderAllExtensions = (binder: Binder, name: string, props: object) => {
- const extensions = binder.getExtensions(name, props);
+function createInstance(Component: Renderable
, props: P, key?: number) {
+ if (React.isValidElement(Component)) {
+ return React.cloneElement(Component, {
+ ...props,
+ ...Component.props,
+ key,
+ });
+ }
+ return ;
+}
+
+const renderAllExtensions = >(
+ binder: Binder,
+ name: E["name"],
+ props: E["props"]
+) => {
+ const extensions = binder.getExtensions(name, props);
return <>{extensions.map((cmp, index) => createInstance(cmp, props, index))}>;
};
-const renderWrapperExtensions = (binder: Binder, name: string, props: object) => {
- const extensions = [...(binder.getExtensions(name, props) || [])];
+const renderWrapperExtensions = >(
+ binder: Binder,
+ name: E["name"],
+ props: E["props"]
+) => {
+ const extensions = binder.getExtensions(name, props);
extensions.reverse();
let instance: any = null;
@@ -68,8 +99,12 @@ const renderWrapperExtensions = (binder: Binder, name: string, props: object) =>
return instance;
};
-const renderSingleExtension = (binder: Binder, name: string, props: object) => {
- const cmp = binder.getExtension(name, props);
+const renderSingleExtension = >(
+ binder: Binder,
+ name: E["name"],
+ props: E["props"]
+) => {
+ const cmp = binder.getExtension(name, props);
if (!cmp) {
return null;
}
@@ -97,18 +132,21 @@ const createRenderProps = (propTransformer?: PropTransformer, props?: object) =>
/**
* ExtensionPoint renders components which are bound to an extension point.
*/
-const ExtensionPoint: FC = ({ name, propTransformer, props, renderAll, wrapper, children }) => {
+export default function ExtensionPoint<
+ E extends RenderableExtensionPointDefinition = RenderableExtensionPointDefinition
+>({ name, propTransformer, props, renderAll, wrapper, children }: PropsWithChildren>): JSX.Element | null {
const binder = useBinder();
- const renderProps = createRenderProps(propTransformer, { ...(props || {}), children });
- if (!binder.hasExtension(name, renderProps)) {
+ const renderProps: E["props"] | {} = createRenderProps(propTransformer, {
+ ...(props || {}),
+ children,
+ });
+ if (!binder.hasExtension(name, renderProps)) {
return renderDefault(children);
} else if (renderAll) {
if (wrapper) {
- return renderWrapperExtensions(binder, name, renderProps);
+ return renderWrapperExtensions(binder, name, renderProps);
}
- return renderAllExtensions(binder, name, renderProps);
+ return renderAllExtensions(binder, name, renderProps);
}
- return renderSingleExtension(binder, name, renderProps);
-};
-
-export default ExtensionPoint;
+ return renderSingleExtension(binder, name, renderProps);
+}
diff --git a/scm-ui/ui-extensions/src/binder.test.ts b/scm-ui/ui-extensions/src/binder.test.tsx
similarity index 80%
rename from scm-ui/ui-extensions/src/binder.test.ts
rename to scm-ui/ui-extensions/src/binder.test.tsx
index c6855457b1..86a38017e6 100644
--- a/scm-ui/ui-extensions/src/binder.test.ts
+++ b/scm-ui/ui-extensions/src/binder.test.tsx
@@ -22,7 +22,9 @@
* SOFTWARE.
*/
+import React from "react";
import { Binder, ExtensionPointDefinition, SimpleDynamicExtensionPointDefinition } from "./binder";
+import ExtensionPoint, { RenderableExtensionPointDefinition } from "./ExtensionPoint";
describe("binder tests", () => {
let binder: Binder;
@@ -117,6 +119,59 @@ describe("binder tests", () => {
expect(binderExtensionC).not.toBeNull();
});
+ it("should allow typings for renderable extension points", () => {
+ type TestExtensionPointA = RenderableExtensionPointDefinition<"test.extension.a">;
+ type TestExtensionPointB = RenderableExtensionPointDefinition<"test.extension.b", { testProp: boolean[] }>;
+
+ binder.bind(
+ "test.extension.a",
+ () => Hello world ,
+ () => false
+ );
+ const binderExtensionA = binder.getExtension("test.extension.a");
+ expect(binderExtensionA).not.toBeNull();
+ binder.bind("test.extension.b", ({ testProp }) => (
+
+ {testProp.map((b) => (
+ {b}
+ ))}
+
+ ));
+ const binderExtensionsB = binder.getExtensions("test.extension.b", {
+ testProp: [true, false],
+ });
+ expect(binderExtensionsB).toHaveLength(1);
+ });
+
+ it("should render typed extension point", () => {
+ type TestExtensionPointA = RenderableExtensionPointDefinition<"test.extension.a">;
+ type TestExtensionPointB = RenderableExtensionPointDefinition<"test.extension.b", { testProp: boolean[] }>;
+
+ binder.bind(
+ "test.extension.a",
+ () => Hello world ,
+ () => false
+ );
+ const binderExtensionA = name="test.extension.a" />;
+ expect(binderExtensionA).not.toBeNull();
+ binder.bind("test.extension.b", ({ testProp }) => (
+
+ {testProp.map((b) => (
+ {b}
+ ))}
+
+ ));
+ const binderExtensionsB = (
+
+ name="test.extension.b"
+ props={{
+ testProp: [true, false],
+ }}
+ />
+ );
+ expect(binderExtensionsB).not.toBeNull();
+ });
+
it("should allow typings for dynamic extension points", () => {
type MarkdownCodeLanguageRendererProps = {
language?: string;
diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts
index 9d99a2de72..a41de04a14 100644
--- a/scm-ui/ui-extensions/src/binder.ts
+++ b/scm-ui/ui-extensions/src/binder.ts
@@ -21,8 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
-type Predicate = Record> = (props: P) => boolean;
+type Predicate = Record> = (props: P) => unknown;
type ExtensionRegistration = {
predicate: Predicate
;
@@ -78,15 +77,29 @@ export class Binder {
*
* @param extensionPoint name of extension point
* @param extension provided extension
- * @param predicate to decide if the extension gets rendered for the given props
*/
bind>(extensionPoint: E["name"], extension: E["type"]): void;
+ /**
+ * Binds an extension to the extension point.
+ *
+ * @param extensionPoint name of extension point
+ * @param extension provided extension
+ * @param predicate to decide if the extension gets rendered for the given props
+ * @param extensionName name used for sorting alphabetically on retrieval (ASC)
+ */
bind>(
extensionPoint: E["name"],
extension: E["type"],
predicate?: Predicate,
extensionName?: string
): void;
+ /**
+ * Binds an extension to the extension point.
+ *
+ * @param extensionPoint name of extension point
+ * @param extension provided extension
+ * @param options object with additional settings
+ */
bind>(
extensionPoint: E["name"],
extension: E["type"],
@@ -125,13 +138,18 @@ export class Binder {
this.extensionPoints[extensionPoint].push(registration);
}
+ /**
+ * Returns the first extension or null for the given extension point and its props.
+ *
+ * @param extensionPoint name of extension point
+ */
+ getExtension>(extensionPoint: E["name"]): E["type"] | null;
/**
* Returns the first extension or null for the given extension point and its props.
*
* @param extensionPoint name of extension point
* @param props of the extension point
*/
- getExtension>(extensionPoint: E["name"]): E["type"] | null;
getExtension>(
extensionPoint: E["name"],
props: E["props"]
@@ -147,16 +165,19 @@ export class Binder {
return null;
}
+ /**
+ * Returns all registered extensions for the given extension point and its props.
+ *
+ * @param extensionPoint name of extension point
+ */
+ getExtensions>(extensionPoint: E["name"]): Array;
/**
* Returns all registered extensions for the given extension point and its props.
*
* @param extensionPoint name of extension point
* @param props of the extension point
*/
- getExtensions>(
- extensionPoint: E["name"]
- ): Array;
- getExtensions>(
+ getExtensions>(
extensionPoint: E["name"],
props: E["props"]
): Array;
@@ -175,11 +196,19 @@ export class Binder {
/**
* Returns true if at least one extension is bound to the extension point and its props.
*/
+ hasExtension>(extensionPoint: E["name"]): boolean;
+ /**
+ * Returns true if at least one extension is bound to the extension point and its props.
+ */
+ hasExtension>(
+ extensionPoint: E["name"],
+ props: E["props"]
+ ): boolean;
hasExtension>(
extensionPoint: E["name"],
props?: E["props"]
): boolean {
- return this.getExtensions(extensionPoint, props).length > 0;
+ return this.getExtensions(extensionPoint, props).length > 0;
}
/**
diff --git a/scm-ui/ui-extensions/src/extensionPoints.ts b/scm-ui/ui-extensions/src/extensionPoints.ts
index d7e1421803..d4e0c7b7dd 100644
--- a/scm-ui/ui-extensions/src/extensionPoints.ts
+++ b/scm-ui/ui-extensions/src/extensionPoints.ts
@@ -22,19 +22,32 @@
* SOFTWARE.
*/
-import React from "react";
+import React, { ReactNode } from "react";
import {
Branch,
- BranchDetails,
+ Changeset,
File,
+ Group,
+ HalRepresentation,
+ Hit,
IndexResources,
Links,
+ Me,
+ Namespace,
NamespaceStrategies,
+ Person,
+ Plugin,
Repository,
RepositoryCreation,
- RepositoryTypeCollection
+ RepositoryRole,
+ RepositoryRoleBase,
+ RepositoryTypeCollection,
+ Tag,
+ User,
} from "@scm-manager/ui-types";
import { ExtensionPointDefinition } from "./binder";
+import { RenderableExtensionPointDefinition, SimpleRenderableDynamicExtensionPointDefinition } from "./ExtensionPoint";
+import ExtractProps from "./extractProps";
type RepositoryCreatorSubFormProps = {
repository: RepositoryCreation;
@@ -43,128 +56,466 @@ type RepositoryCreatorSubFormProps = {
disabled?: boolean;
};
-export type RepositoryCreatorComponentProps = {
- namespaceStrategies: NamespaceStrategies;
- repositoryTypes: RepositoryTypeCollection;
- index: IndexResources;
+export type RepositoryCreatorComponentProps = ExtractProps;
- nameForm: React.ComponentType;
- informationForm: React.ComponentType;
-};
+/**
+ * @deprecated use {@link RepositoryCreator}`["type"]` instead
+ */
+export type RepositoryCreatorExtension = RepositoryCreator["type"];
+export type RepositoryCreator = ExtensionPointDefinition<"repos.creator",
+ {
+ subtitle: string;
+ path: string;
+ icon: string;
+ label: string;
+ component: React.ComponentType<{
+ namespaceStrategies: NamespaceStrategies;
+ repositoryTypes: RepositoryTypeCollection;
+ index: IndexResources;
-export type RepositoryCreatorExtension = {
- subtitle: string;
- path: string;
- icon: string;
- label: string;
- component: React.ComponentType;
-};
+ nameForm: React.ComponentType;
+ informationForm: React.ComponentType;
+ }>;
+ }>;
-export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
+export type RepositoryFlags = RenderableExtensionPointDefinition<"repository.flags",
+ { repository: Repository; tooltipLocation?: "bottom" | "right" | "top" | "left" }>;
-export type RepositoryFlags = ExtensionPointDefinition<"repository.flags", { repository: Repository }>;
+/**
+ * @deprecated use {@link ReposSourcesActionbar}`["props"]` instead
+ */
+export type ReposSourcesActionbarExtensionProps = ReposSourcesActionbar["props"];
+/**
+ * @deprecated use {@link ReposSourcesActionbar} instead
+ */
+export type ReposSourcesActionbarExtension = ReposSourcesActionbar;
+export type ReposSourcesActionbar = RenderableExtensionPointDefinition<"repos.sources.actionbar",
+ {
+ baseUrl: string;
+ revision: string;
+ branch: Branch | undefined;
+ path: string;
+ sources: File;
+ repository: Repository;
+ }>;
-export type ReposSourcesActionbarExtensionProps = {
- baseUrl: string;
- revision: string;
- branch: Branch | undefined;
- path: string;
- sources: File;
- repository: Repository;
-};
-export type ReposSourcesActionbarExtension = React.ComponentType;
-export type ReposSourcesActionbar = ExtensionPointDefinition<"repos.sources.actionbar", ReposSourcesActionbarExtension>;
+/**
+ * @deprecated use {@link ReposSourcesEmptyActionbar}`["props"]` instead
+ */
+export type ReposSourcesEmptyActionbarExtensionProps = ReposSourcesEmptyActionbar["props"];
+/**
+ * @deprecated use {@link ReposSourcesEmptyActionbar} instead
+ */
+export type ReposSourcesEmptyActionbarExtension = ReposSourcesEmptyActionbar;
+export type ReposSourcesEmptyActionbar = RenderableExtensionPointDefinition<"repos.sources.empty.actionbar",
+ {
+ sources: File;
+ repository: Repository;
+ }>;
-export type ReposSourcesEmptyActionbarExtensionProps = {
- sources: File;
- repository: Repository;
-};
-export type ReposSourcesEmptyActionbarExtension = ReposSourcesActionbarExtension;
-export type ReposSourcesEmptyActionbar = ExtensionPointDefinition<
- "repos.sources.empty.actionbar",
- ReposSourcesEmptyActionbarExtension
->;
+/**
+ * @deprecated use {@link ReposSourcesTreeWrapper}`["props"]` instead
+ */
+export type ReposSourcesTreeWrapperProps = ReposSourcesTreeWrapper["props"];
-export type ReposSourcesTreeWrapperProps = {
- repository: Repository;
- directory: File;
- baseUrl: string;
- revision: string;
-};
-
-export type ReposSourcesTreeWrapperExtension = ExtensionPointDefinition<
- "repos.source.tree.wrapper",
- React.ComponentType
->;
+/**
+ * @deprecated use {@link ReposSourcesTreeWrapper} instead
+ */
+export type ReposSourcesTreeWrapperExtension = ReposSourcesTreeWrapper;
+export type ReposSourcesTreeWrapper = RenderableExtensionPointDefinition<"repos.source.tree.wrapper",
+ {
+ repository: Repository;
+ directory: File;
+ baseUrl: string;
+ revision: string;
+ }>;
export type ReposSourcesTreeRowProps = {
repository: Repository;
file: File;
};
-export type ReposSourcesTreeRowRightExtension = ExtensionPointDefinition<
- "repos.sources.tree.row.right",
- React.ComponentType
->;
-export type ReposSourcesTreeRowAfterExtension = ExtensionPointDefinition<
- "repos.sources.tree.row.after",
- React.ComponentType
->;
+/**
+ * @deprecated use {@link ReposSourcesTreeRowRight} instead
+ */
+export type ReposSourcesTreeRowRightExtension = ReposSourcesTreeRowRight;
+export type ReposSourcesTreeRowRight = RenderableExtensionPointDefinition<"repos.sources.tree.row.right",
+ ReposSourcesTreeRowProps>;
-export type PrimaryNavigationLoginButtonProps = {
- links: Links;
- label: string;
- loginUrl: string;
- from: string;
- to: string;
- className: string;
- content: React.ReactNode;
+/**
+ * @deprecated use {@link ReposSourcesTreeRowAfter} instead
+ */
+export type ReposSourcesTreeRowAfterExtension = ReposSourcesTreeRowAfter;
+export type ReposSourcesTreeRowAfter = RenderableExtensionPointDefinition<"repos.sources.tree.row.after",
+ ReposSourcesTreeRowProps>;
+
+/**
+ * @deprecated use {@link PrimaryNavigationLoginButton}`["props"]` instead
+ */
+export type PrimaryNavigationLoginButtonProps = PrimaryNavigationLoginButton["props"];
+
+/**
+ * use {@link PrimaryNavigationLoginButton} instead
+ */
+export type PrimaryNavigationLoginButtonExtension = PrimaryNavigationLoginButton;
+export type PrimaryNavigationLoginButton = RenderableExtensionPointDefinition<"primary-navigation.login",
+ {
+ links: Links;
+ label: string;
+ loginUrl: string;
+ from: string;
+ to: string;
+ className: string;
+ content: React.ReactNode;
+ }>;
+
+/**
+ * @deprecated use {@link PrimaryNavigationLogoutButtonExtension}`["props"]` instead
+ */
+export type PrimaryNavigationLogoutButtonProps = PrimaryNavigationLogoutButton["props"];
+
+/**
+ * @deprecated use {@link PrimaryNavigationLogoutButton} instead
+ */
+export type PrimaryNavigationLogoutButtonExtension = PrimaryNavigationLogoutButton;
+export type PrimaryNavigationLogoutButton = RenderableExtensionPointDefinition<"primary-navigation.logout",
+ {
+ links: Links;
+ label: string;
+ className: string;
+ content: React.ReactNode;
+ }>;
+
+/**
+ * @deprecated use {@link SourceExtension}`["props"]` instead
+ */
+export type SourceExtensionProps = SourceExtension["props"];
+export type SourceExtension = RenderableExtensionPointDefinition<"repos.sources.extensions",
+ {
+ repository: Repository;
+ baseUrl: string;
+ revision: string;
+ extension: string;
+ sources: File | undefined;
+ path: string;
+ }>;
+
+/**
+ * @deprecated use {@link RepositoryOverviewTop}`["props"]` instead
+ */
+export type RepositoryOverviewTopExtensionProps = RepositoryOverviewTop["props"];
+
+/**
+ * @deprecated use {@link RepositoryOverviewTop} instead
+ */
+export type RepositoryOverviewTopExtension = RepositoryOverviewTop;
+export type RepositoryOverviewTop = RenderableExtensionPointDefinition<"repository.overview.top",
+ {
+ page: number;
+ search: string;
+ namespace?: string;
+ }>;
+
+/**
+ * @deprecated use {@link RepositoryOverviewLeft} instead
+ */
+export type RepositoryOverviewLeftExtension = RepositoryOverviewLeft;
+export type RepositoryOverviewLeft = ExtensionPointDefinition<"repository.overview.left", React.ComponentType>;
+
+/**
+ * @deprecated use {@link RepositoryOverviewTitle} instead
+ */
+export type RepositoryOverviewTitleExtension = RepositoryOverviewTitle;
+export type RepositoryOverviewTitle = RenderableExtensionPointDefinition<"repository.overview.title">;
+
+/**
+ * @deprecated use {@link RepositoryOverviewSubtitle} instead
+ */
+export type RepositoryOverviewSubtitleExtension = RepositoryOverviewSubtitle;
+export type RepositoryOverviewSubtitle = RenderableExtensionPointDefinition<"repository.overview.subtitle">;
+
+// From docs
+
+export type AdminNavigation = RenderableExtensionPointDefinition<"admin.navigation", { links: Links; url: string }>;
+
+export type AdminRoute = RenderableExtensionPointDefinition<"admin.route", { links: Links; url: string }>;
+
+export type AdminSetting = RenderableExtensionPointDefinition<"admin.setting", { links: Links; url: string }>;
+
+/**
+ * - can be used to replace the whole description of a changeset
+ *
+ * @deprecated Use `changeset.description.tokens` instead
+ */
+export type ChangesetDescription = RenderableExtensionPointDefinition<"changeset.description",
+ { changeset: Changeset; value: string }>;
+
+/**
+ * - Can be used to replace parts of a changeset description with components
+ * - Has to be bound with a funktion taking the changeset and the (partial) description and returning `Replacement` objects with the following attributes:
+ * - textToReplace: The text part of the description that should be replaced by a component
+ * - replacement: The component to take instead of the text to replace
+ * - replaceAll: Optional boolean; if set to `true`, all occurances of the text will be replaced (default: `false`)
+ */
+export type ChangesetDescriptionTokens = ExtensionPointDefinition<"changeset.description.tokens",
+ (changeset: Changeset, value: string) => Array<{
+ textToReplace: string;
+ replacement: ReactNode;
+ replaceAll?: boolean;
+ }>,
+ { changeset: Changeset; value: string }>;
+
+export type ChangesetRight = RenderableExtensionPointDefinition<"changeset.right",
+ { repository: Repository; changeset: Changeset }>;
+
+export type ChangesetsAuthorSuffix = RenderableExtensionPointDefinition<"changesets.author.suffix",
+ { changeset: Changeset }>;
+
+export type GroupNavigation = RenderableExtensionPointDefinition<"group.navigation", { group: Group; url: string }>;
+
+export type GroupRoute = RenderableExtensionPointDefinition<"group.route", { group: Group; url: string }>;
+export type GroupSetting = RenderableExtensionPointDefinition<"group.setting", { group: Group; url: string }>;
+
+/**
+ * - Add a new Route to the main Route (scm/)
+ * - Props: authenticated?: boolean, links: Links
+ */
+export type MainRoute = RenderableExtensionPointDefinition<"main.route",
+ {
+ me: Me;
+ authenticated?: boolean;
+ }>;
+
+export type PluginAvatar = RenderableExtensionPointDefinition<"plugins.plugin-avatar",
+ {
+ plugin: Plugin;
+ }>;
+
+export type PrimaryNavigation = RenderableExtensionPointDefinition<"primary-navigation", { links: Links }>;
+
+/**
+ * - A placeholder for the first navigation menu.
+ * - A PrimaryNavigationLink Component can be used here
+ * - Actually this Extension Point is used from the Activity Plugin to display the activities at the first Main Navigation menu.
+ */
+export type PrimaryNavigationFirstMenu = RenderableExtensionPointDefinition<"primary-navigation.first-menu",
+ { links: Links; label: string }>;
+
+export type ProfileRoute = RenderableExtensionPointDefinition<"profile.route", { me: Me; url: string }>;
+export type ProfileSetting = RenderableExtensionPointDefinition<"profile.setting",
+ { me?: Me; url: string; links: Links }>;
+
+export type RepoConfigRoute = RenderableExtensionPointDefinition<"repo-config.route",
+ { repository: Repository; url: string }>;
+
+export type RepoConfigDetails = RenderableExtensionPointDefinition<"repo-config.details",
+ { repository: Repository; url: string }>;
+
+export type ReposBranchDetailsInformation = RenderableExtensionPointDefinition<"repos.branch-details.information",
+ { repository: Repository; branch: Branch }>;
+
+/**
+ * - Location: At meta data view for file
+ * - can be used to render additional meta data line
+ * - Props: file: string, repository: Repository, revision: string
+ */
+export type ReposContentMetaData = RenderableExtensionPointDefinition<"repos.content.metadata",
+ { file: File; repository: Repository; revision: string }>;
+
+export type ReposCreateNamespace = RenderableExtensionPointDefinition<"repos.create.namespace",
+ {
+ label: string;
+ helpText: string;
+ value: string;
+ onChange: (namespace: string) => void;
+ errorMessage: string;
+ validationError?: boolean;
+ }>;
+
+export type ReposSourcesContentActionBar = RenderableExtensionPointDefinition<"repos.sources.content.actionbar",
+ {
+ repository: Repository;
+ file: File;
+ revision: string;
+ handleExtensionError: React.Dispatch>;
+ }>;
+
+export type RepositoryNavigation = RenderableExtensionPointDefinition<"repository.navigation",
+ { repository: Repository; url: string; indexLinks: Links }>;
+
+export type RepositoryNavigationTopLevel = RenderableExtensionPointDefinition<"repository.navigation.topLevel",
+ { repository: Repository; url: string; indexLinks: Links }>;
+
+export type RepositoryRoleDetailsInformation = RenderableExtensionPointDefinition<"repositoryRole.role-details.information",
+ { role: RepositoryRole }>;
+
+export type RepositorySetting = RenderableExtensionPointDefinition<"repository.setting",
+ { repository: Repository; url: string; indexLinks: Links }>;
+
+export type RepositoryAvatar = RenderableExtensionPointDefinition<"repos.repository-avatar",
+ { repository: Repository }>;
+
+/**
+ * - Location: At each repository in repository overview
+ * - can be used to add avatar for each repository (e.g., to mark repository type)
+ */
+export type PrimaryRepositoryAvatar = RenderableExtensionPointDefinition<"repos.repository-avatar.primary",
+ { repository: Repository }>;
+
+/**
+ * - Location: At bottom of a single repository view
+ * - can be used to show detailed information about the repository (how to clone, e.g.)
+ */
+export type RepositoryDetailsInformation = RenderableExtensionPointDefinition<"repos.repository-details.information",
+ { repository: Repository }>;
+
+/**
+ * - Location: At sources viewer
+ * - can be used to render a special source that is not an image or a source code
+ */
+export type RepositorySourcesView = RenderableExtensionPointDefinition<"repos.sources.view",
+ { file: File; contentType: string; revision: string; basePath: string }>;
+
+export type RolesRoute = RenderableExtensionPointDefinition<"roles.route",
+ { role: HalRepresentation & RepositoryRoleBase & { creationDate?: string; lastModified?: string }; url: string }>;
+
+export type UserRoute = RenderableExtensionPointDefinition<"user.route", { user: User; url: string }>;
+export type UserSetting = RenderableExtensionPointDefinition<"user.setting", { user: User; url: string }>;
+
+/**
+ * - Dynamic extension point for custom language-specific renderers
+ * - Overrides the default Syntax Highlighter for the given language
+ * - Used by the Markdown Plantuml Plugin
+ */
+export type MarkdownCodeRenderer =
+ SimpleRenderableDynamicExtensionPointDefinition<"markdown-renderer.code.",
+ Language,
+ {
+ language?: Language extends string ? Language : string;
+ value: string;
+ indexLinks: Links;
+ }>;
+
+/**
+ * - Define custom protocols and their renderers for links in markdown
+ *
+ * Example:
+ * ```markdown
+ * [description](myprotocol:somelink)
+ * ```
+ *
+ * ```typescript
+ * binder.bind>("markdown-renderer.link.protocol", { protocol: "myprotocol", renderer: MyProtocolRenderer })
+ * ```
+ */
+export type MarkdownLinkProtocolRenderer = ExtensionPointDefinition<"markdown-renderer.link.protocol",
+ {
+ protocol: Protocol extends string ? Protocol : string;
+ renderer: React.ComponentType<{
+ protocol: Protocol extends string ? Protocol : string;
+ href: string;
+ }>;
+ }>;
+
+/**
+ * Used to determine an avatar image url from a given {@link Person}.
+ *
+ * @see https://github.com/scm-manager/scm-gravatar-plugin
+ */
+export type AvatarFactory = ExtensionPointDefinition<"avatar.factory", (person: Person) => string | undefined>;
+
+/**
+ * - Location: At every changeset (detailed view as well as changeset overview)
+ * - can be used to add avatar (such as gravatar) for each changeset
+ *
+ * @deprecated Has no effect, use {@link AvatarFactory} instead
+ */
+export type ChangesetAvatarFactory = ExtensionPointDefinition<"changeset.avatar-factory",
+ (changeset: Changeset) => void>;
+
+type MainRedirectProps = {
+ me: Me;
+ authenticated?: boolean;
};
-export type PrimaryNavigationLoginButtonExtension = ExtensionPointDefinition<
- "primary-navigation.login",
- PrimaryNavigationLoginButtonProps
->;
+/**
+ * - Extension Point for a link factory that provide the Redirect Link
+ * - Actually used from the activity plugin: binder.bind("main.redirect", () => "/activity");
+ */
+export type MainRedirect = ExtensionPointDefinition<"main.redirect",
+ (props: MainRedirectProps) => string,
+ MainRedirectProps>;
-export type PrimaryNavigationLogoutButtonProps = {
- links: Links;
- label: string;
- className: string;
- content: React.ReactNode;
-};
+/**
+ * - A Factory function to create markdown [renderer](https://github.com/rexxars/react-markdown#node-types)
+ * - The factory function will be called with a renderContext parameter of type Object. this parameter is given as a prop for the {@link MarkdownView} component.
+ *
+ * @deprecated Use {@link MarkdownCodeRenderer} or {@link MarkdownLinkProtocolRenderer} instead
+ */
+export type MarkdownRendererFactory = ExtensionPointDefinition<"markdown-renderer-factory",
+ (renderContext: unknown) => Record>>;
-export type PrimaryNavigationLogoutButtonExtension = ExtensionPointDefinition<
- "primary-navigation.logout",
- PrimaryNavigationLogoutButtonProps
->;
+export type RepositoryCardBeforeTitle = RenderableExtensionPointDefinition<"repository.card.beforeTitle",
+ { repository: Repository }>;
-export type SourceExtensionProps = {
+export type RepositoryCreationInitialization = RenderableExtensionPointDefinition<"repos.create.initialize",
+ {
+ repository: Repository;
+ setCreationContextEntry: (key: string, value: any) => void;
+ indexResources: Partial & {
+ links?: Links;
+ version?: string;
+ initialization?: string;
+ };
+ }>;
+
+export type NamespaceTopLevelNavigation = RenderableExtensionPointDefinition<"namespace.navigation.topLevel",
+ { namespace: Namespace; url: string }>;
+
+export type NamespaceRoute = RenderableExtensionPointDefinition<"namespace.route",
+ { namespace: Namespace; url: string }>;
+
+export type NamespaceSetting = RenderableExtensionPointDefinition<"namespace.setting",
+ { namespace: Namespace; url: string }>;
+
+export type RepositoryTagDetailsInformation = RenderableExtensionPointDefinition<"repos.tag-details.information",
+ { repository: Repository; tag: Tag }>;
+
+export type SearchHitRenderer = RenderableExtensionPointDefinition;
+
+export type RepositorySourcesContentDownloadButton = RenderableExtensionPointDefinition<"repos.sources.content.downloadButton",
+ { repository: Repository; file: File }>;
+
+export type RepositoryRoute = RenderableExtensionPointDefinition<"repository.route",
+ { repository: Repository; url: string; indexLinks: Links }>;
+
+type RepositoryRedirectProps = {
+ namespace: string;
+ name: string;
repository: Repository;
- baseUrl: string;
- revision: string;
- extension: string;
- sources: File | undefined;
- path: string;
-};
-export type SourceExtension = ExtensionPointDefinition<"repos.sources.extensions", SourceExtensionProps>;
-
-export type RepositoryOverviewTopExtensionProps = {
- page: number;
- search: string;
- namespace?: string;
+ loading: false;
+ error: null;
+ repoLink: string;
+ indexLinks: Links;
+ match: {
+ params: {
+ namespace: string;
+ name: string;
+ };
+ isExact: boolean;
+ path: string;
+ url: string;
+ };
};
-export type RepositoryOverviewTopExtension = ExtensionPointDefinition<
- "repository.overview.top",
- React.ComponentType,
- 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
->;
+export type RepositoryRedirect = ExtensionPointDefinition<"repository.redirect",
+ (props: RepositoryRedirectProps) => string,
+ RepositoryRedirectProps>;
+
+export type InitializationStep =
+ SimpleRenderableDynamicExtensionPointDefinition<"initialization.step.",
+ Step,
+ {
+ data: HalRepresentation;
+ }>;
diff --git a/scm-ui/ui-extensions/src/extractProps.ts b/scm-ui/ui-extensions/src/extractProps.ts
new file mode 100644
index 0000000000..3b8f42c977
--- /dev/null
+++ b/scm-ui/ui-extensions/src/extractProps.ts
@@ -0,0 +1,28 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React from "react";
+
+type ExtractProps = T extends React.ComponentType ? U : never;
+
+export default ExtractProps;
diff --git a/scm-ui/ui-extensions/src/index.ts b/scm-ui/ui-extensions/src/index.ts
index 746a089f6e..61520badff 100644
--- a/scm-ui/ui-extensions/src/index.ts
+++ b/scm-ui/ui-extensions/src/index.ts
@@ -25,6 +25,7 @@
export { default as binder, Binder, ExtensionPointDefinition } from "./binder";
export * from "./useBinder";
export { default as ExtensionPoint } from "./ExtensionPoint";
+export { default as ExtractProps } from "./extractProps";
// suppress eslint prettier warning,
// because prettier does not understand "* as"
diff --git a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx
index e7fccb56fe..cea38e7252 100644
--- a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx
+++ b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx
@@ -24,7 +24,7 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Redirect, Route, RouteProps, Switch, useRouteMatch } from "react-router-dom";
-import { ExtensionPoint } from "@scm-manager/ui-extensions";
+import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import {
CustomQueryFlexWrappedColumns,
NavLink,
@@ -97,7 +97,7 @@ const Admin: FC = () => {
-
+ name="admin.route" props={extensionProps} renderAll={true} />
@@ -142,7 +142,11 @@ const Admin: FC = () => {
activeWhenMatch={matchesRoles}
activeOnlyWhenExact={false}
/>
-
+
+ name="admin.navigation"
+ props={extensionProps}
+ renderAll={true}
+ />
{
label={t("admin.menu.generalNavLink")}
testId="admin-settings-general-link"
/>
-
+
+ name="admin.setting"
+ props={extensionProps}
+ renderAll={true}
+ />
diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginAvatar.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginAvatar.tsx
index 5cd692798d..572193c1cf 100644
--- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginAvatar.tsx
+++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginAvatar.tsx
@@ -23,7 +23,7 @@
*/
import React from "react";
import styled from "styled-components";
-import { ExtensionPoint } from "@scm-manager/ui-extensions";
+import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { Plugin } from "@scm-manager/ui-types";
import { Image } from "@scm-manager/ui-components";
@@ -44,7 +44,7 @@ export default class PluginAvatar extends React.Component {
const { plugin } = this.props;
return (
-
name="plugins.plugin-avatar"
props={{
plugin
diff --git a/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx b/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx
index a00e5a8e86..dee63113a1 100644
--- a/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx
+++ b/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx
@@ -23,7 +23,7 @@
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
-import { ExtensionPoint } from "@scm-manager/ui-extensions";
+import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { RepositoryRole } from "@scm-manager/ui-types";
import { Button, Level } from "@scm-manager/ui-components";
import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable";
@@ -54,7 +54,7 @@ class PermissionRoleDetails extends React.Component {
<>
{this.renderEditButton()}
-
name="repositoryRole.role-details.information"
renderAll={true}
props={{
diff --git a/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx b/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx
index c13eeb64a4..44887c8037 100644
--- a/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx
+++ b/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx
@@ -24,7 +24,7 @@
import React, { FC } from "react";
import { Route, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
-import { ExtensionPoint } from "@scm-manager/ui-extensions";
+import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { ErrorPage, Loading, Title, urls } from "@scm-manager/ui-components";
import PermissionRoleDetail from "../components/PermissionRoleDetails";
import EditRepositoryRole from "./EditRepositoryRole";
@@ -62,7 +62,7 @@ const SingleRepositoryRole: FC = () => {
-
+ name="roles.route" props={extensionProps} renderAll={true} />
>
);
};
diff --git a/scm-ui/ui-webapp/src/containers/Index.tsx b/scm-ui/ui-webapp/src/containers/Index.tsx
index e1e2be082b..e95533cda8 100644
--- a/scm-ui/ui-webapp/src/containers/Index.tsx
+++ b/scm-ui/ui-webapp/src/containers/Index.tsx
@@ -23,14 +23,14 @@
*/
import React, { FC, useState } from "react";
import App from "./App";
-import { ErrorBoundary, Loading, Header } from "@scm-manager/ui-components";
+import { ErrorBoundary, Header, Loading } from "@scm-manager/ui-components";
import PluginLoader from "./PluginLoader";
import ScrollToTop from "./ScrollToTop";
import IndexErrorPage from "./IndexErrorPage";
import { useIndex } from "@scm-manager/ui-api";
import { Link } from "@scm-manager/ui-types";
import i18next from "i18next";
-import { binder } from "@scm-manager/ui-extensions";
+import { binder, extensionPoints } from "@scm-manager/ui-extensions";
import InitializationAdminAccountStep from "./InitializationAdminAccountStep";
const Index: FC = () => {
@@ -69,4 +69,7 @@ const Index: FC = () => {
export default Index;
-binder.bind("initialization.step.adminAccount", InitializationAdminAccountStep);
+binder.bind>(
+ "initialization.step.adminAccount",
+ InitializationAdminAccountStep
+);
diff --git a/scm-ui/ui-webapp/src/containers/LoginButton.tsx b/scm-ui/ui-webapp/src/containers/LoginButton.tsx
index 905a68f8cd..5831852fb3 100644
--- a/scm-ui/ui-webapp/src/containers/LoginButton.tsx
+++ b/scm-ui/ui-webapp/src/containers/LoginButton.tsx
@@ -27,10 +27,9 @@ import { urls } from "@scm-manager/ui-components";
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { useTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
-import { useLocation } from "react-router-dom";
+import { Link, useLocation } from "react-router-dom";
import classNames from "classnames";
import HeaderButton from "../components/HeaderButton";
-import { Link } from "react-router-dom";
import HeaderButtonContent, { headerButtonContentClassName } from "../components/HeaderButtonContent";
type Props = {
@@ -63,14 +62,20 @@ const LoginButton: FC = ({ burgerMode, links, className }) => {
};
if (links?.login) {
- const shouldRenderExtension = binder.hasExtension("primary-navigation.login", extensionProps);
+ const shouldRenderExtension = binder.hasExtension(
+ "primary-navigation.login",
+ extensionProps
+ );
return (
{shouldRenderExtension ? (
-
+
+ name="primary-navigation.login"
+ props={extensionProps}
+ />
) : (
{content}
diff --git a/scm-ui/ui-webapp/src/containers/LogoutButton.tsx b/scm-ui/ui-webapp/src/containers/LogoutButton.tsx
index 887dddd7dd..50bbc2ccf9 100644
--- a/scm-ui/ui-webapp/src/containers/LogoutButton.tsx
+++ b/scm-ui/ui-webapp/src/containers/LogoutButton.tsx
@@ -43,16 +43,18 @@ const LogoutButton: FC = ({ burgerMode, links, className }) => {
const label = t("primary-navigation.logout");
const content = ;
- const extensionProps: extensionPoints.PrimaryNavigationLogoutButtonProps = {
+ const extensionProps = {
links,
label,
-
className: headerButtonContentClassName,
content
};
if (links?.logout) {
- const shouldRenderExtension = binder.hasExtension("primary-navigation.logout", extensionProps);
+ const shouldRenderExtension = binder.hasExtension(
+ "primary-navigation.logout",
+ extensionProps
+ );
return (
= ({ burgerMode, links, className }) => {
className={classNames("is-flex-start", "navbar-item", className)}
>
{shouldRenderExtension ? (
-
+
+ name="primary-navigation.logout"
+ props={extensionProps}
+ />
) : (
{content}
diff --git a/scm-ui/ui-webapp/src/containers/Main.tsx b/scm-ui/ui-webapp/src/containers/Main.tsx
index a9fe986f3c..4bd3870722 100644
--- a/scm-ui/ui-webapp/src/containers/Main.tsx
+++ b/scm-ui/ui-webapp/src/containers/Main.tsx
@@ -27,7 +27,7 @@ import { Redirect, Route, Switch } from "react-router-dom";
import { Links, Me } from "@scm-manager/ui-types";
import { ErrorBoundary, Loading, ProtectedRoute } from "@scm-manager/ui-components";
-import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
+import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
// auth routes
const Login = React.lazy(() => import("../containers/Login"));
@@ -65,7 +65,7 @@ type Props = {
const Main: FC = props => {
const { authenticated, me } = props;
- const redirectUrlFactory = binder.getExtension("main.redirect", props);
+ const redirectUrlFactory = binder.getExtension("main.redirect", props);
let url = "/";
if (authenticated) {
url = "/repos/";
@@ -110,7 +110,7 @@ const Main: FC = props => {
-
+ name="main.route" renderAll={true} props={props} />