diff --git a/docs/en/development/plugins/extension-points.md b/docs/en/development/plugins/extension-points.md index 2430327e9d..5ffe7fbc04 100644 --- a/docs/en/development/plugins/extension-points.md +++ b/docs/en/development/plugins/extension-points.md @@ -2,123 +2,6 @@ title: Extension Points --- -The following extension points are provided for the frontend: +The available extension points are now maintained in-code, providing typescript types for improved developer experience and code quality assurance. -### admin.navigation -### admin.route -### admin.setting -### changeset.description.tokens -- 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`) -### changeset.right -### changesets.author.suffix -### group.navigation -### group.route -### group.setting -### main.route -- Add a new Route to the main Route (scm/) -- Props: authenticated?: boolean, links: Links - -### plugins.plugin-avatar -### primary-navigation -### primary-navigation.first-menu -- 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. - -### primary-navigation.logout -### profile.route -### profile.setting -### repo-config.route -### repo-config.details -### repos.branch-details.information -### repos.content.metadata -- Location: At meta data view for file -- can be used to render additional meta data line -- Props: file: string, repository: Repository, revision: string - -### repos.create.namespace -### repos.sources.content.actionbar -### repository.navigation -### repository.navigation.topLevel -### repositoryRole.role-details.information -### repository.setting -### repos.repository-avatar -### repos.repository-avatar.primary -- Location: At each repository in repository overview -- can be used to add avatar for each repository (e.g., to mark repository type) - -### repos.repository-details.information -- Location: At bottom of a single repository view -- can be used to show detailed information about the repository (how to clone, e.g.) -### repos.sources.view -### roles.route -### user.route -### user.setting -### markdown-renderer.code.{language} -- Dynamic extension point for custom language-specific renderers -- Overrides the default Syntax Highlighter -- Used by the Markdown Plantuml Plugin -### markdown-renderer.link.protocol -- 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 }) -``` - -# Deprecated - -### changeset.description -- can be used to replace the whole description of a changeset - -**Deprecated:** Use `changeset.description.tokens` instead - -### changeset.avatar-factory -- Location: At every changeset (detailed view as well as changeset overview) -- can be used to add avatar (such as gravatar) for each changeset -- expects a function: `(Changeset) => void` - -### repos.sources.view -- Location: At sources viewer -- can be used to render a special source that is not an image or a source code - -### main.redirect -- Extension Point for a link factory that provide the Redirect Link -- Actually used from the activity plugin: binder.bind("main.redirect", () => "/activity"); - -### markdown-renderer-factory -- 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 MarkdownView component. - -**example:** - - -```javascript -let MarkdownFactory = (renderContext) => { - - let Heading= (props) => { - return React.createElement(`h${props.level}`, - props['data-sourcepos'] ? {'data-sourcepos': props['data-sourcepos']} : {}, - props.children); - }; - return {heading : Heading}; -}; - -binder.bind("markdown-renderer-factory", MarkdownFactory); -``` - -```javascript - -``` +You can browse and import them directly in your frontend code from the `@scm-manager/ui-extensions` package. diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.ts b/scm-plugins/scm-git-plugin/src/main/js/index.ts index dc0a82613d..c32e969aa9 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-git-plugin/src/main/js/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { binder } from "@scm-manager/ui-extensions"; +import { binder, extensionPoints } from "@scm-manager/ui-extensions"; import ProtocolInformation from "./ProtocolInformation"; import GitAvatar from "./GitAvatar"; @@ -40,9 +40,18 @@ export const gitPredicate = (props: any) => { return !!(props && props.repository && props.repository.type === "git"); }; -binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate); +binder.bind( + "repos.repository-details.information", + ProtocolInformation, + gitPredicate +); binder.bind("repos.branch-details.information", GitBranchInformation, { priority: 100, predicate: gitPredicate }); -binder.bind("repos.tag-details.information", GitTagInformation, gitPredicate); + +binder.bind( + "repos.tag-details.information", + GitTagInformation, + gitPredicate +); binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate); binder.bind("repos.repository-avatar", GitAvatar, gitPredicate); diff --git a/scm-plugins/scm-hg-plugin/src/main/js/index.ts b/scm-plugins/scm-hg-plugin/src/main/js/index.ts index d961af2327..9083e6e4f3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-hg-plugin/src/main/js/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { binder } from "@scm-manager/ui-extensions"; +import { binder, extensionPoints } from "@scm-manager/ui-extensions"; import ProtocolInformation from "./ProtocolInformation"; import HgAvatar from "./HgAvatar"; import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; @@ -35,14 +35,23 @@ const hgPredicate = (props: any) => { return props.repository && props.repository.type === "hg"; }; -binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate); +binder.bind( + "repos.repository-details.information", + ProtocolInformation, + hgPredicate +); binder.bind("repos.branch-details.information", HgBranchInformation, { priority: 100, predicate: hgPredicate }); -binder.bind("repos.tag-details.information", HgTagInformation, hgPredicate); + +binder.bind( + "repos.tag-details.information", + HgTagInformation, + hgPredicate +); binder.bind("repos.repository-avatar", HgAvatar, hgPredicate); // bind repository specific configuration -binder.bind("repo-config.route", HgRepositoryConfigurationForm, hgPredicate); +binder.bind("repo-config.route", HgRepositoryConfigurationForm, hgPredicate); // bind global configuration diff --git a/scm-plugins/scm-svn-plugin/src/main/js/index.ts b/scm-plugins/scm-svn-plugin/src/main/js/index.ts index 628f037a51..64b9213ab1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-svn-plugin/src/main/js/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { binder } from "@scm-manager/ui-extensions"; +import { binder, extensionPoints } from "@scm-manager/ui-extensions"; import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; import ProtocolInformation from "./ProtocolInformation"; import SvnAvatar from "./SvnAvatar"; @@ -32,7 +32,11 @@ const svnPredicate = (props: any) => { return props.repository && props.repository.type === "svn"; }; -binder.bind("repos.repository-details.information", ProtocolInformation, svnPredicate); +binder.bind( + "repos.repository-details.information", + ProtocolInformation, + svnPredicate +); binder.bind("repos.repository-avatar", SvnAvatar, svnPredicate); // bind global configuration diff --git a/scm-ui/ui-components/src/Breadcrumb.tsx b/scm-ui/ui-components/src/Breadcrumb.tsx index d05517549c..aeac6504ef 100644 --- a/scm-ui/ui-components/src/Breadcrumb.tsx +++ b/scm-ui/ui-components/src/Breadcrumb.tsx @@ -255,13 +255,25 @@ const Breadcrumb: FC = ({ const renderExtensionPoints = () => { if ( - binder.hasExtension("repos.sources.empty.actionbar") && + binder.hasExtension("repos.sources.empty.actionbar", extProps) && sources?._embedded?.children?.length === 0 ) { - return ; + return ( + + name="repos.sources.empty.actionbar" + props={{ repository, sources }} + renderAll={true} + /> + ); } - if (binder.hasExtension("repos.sources.actionbar")) { - return ; + if (binder.hasExtension("repos.sources.actionbar", extProps)) { + return ( + + name="repos.sources.actionbar" + props={extProps} + renderAll={true} + /> + ); } return null; }; diff --git a/scm-ui/ui-components/src/avatar/Avatar.ts b/scm-ui/ui-components/src/avatar/Avatar.ts index 9690c37ad6..a6d6cd92cd 100644 --- a/scm-ui/ui-components/src/avatar/Avatar.ts +++ b/scm-ui/ui-components/src/avatar/Avatar.ts @@ -23,9 +23,10 @@ */ import { Person } from "@scm-manager/ui-types"; +import { extensionPoints } from "@scm-manager/ui-extensions"; // re export type to avoid breaking changes, // after the type was moved to ui-types export { Person }; -export const EXTENSION_POINT = "avatar.factory"; +export const EXTENSION_POINT: extensionPoints.AvatarFactory["name"] = "avatar.factory"; diff --git a/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx index b92ae5c71b..2d35c3e460 100644 --- a/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx +++ b/scm-ui/ui-components/src/avatar/AvatarWrapper.tsx @@ -22,12 +22,12 @@ * SOFTWARE. */ import React, { FC } from "react"; -import { useBinder } from "@scm-manager/ui-extensions"; +import { extensionPoints, useBinder } from "@scm-manager/ui-extensions"; import { EXTENSION_POINT } from "./Avatar"; const AvatarWrapper: FC = ({ children }) => { const binder = useBinder(); - if (binder.hasExtension(EXTENSION_POINT)) { + if (binder.hasExtension(EXTENSION_POINT)) { return <>{children}; } return null; diff --git a/scm-ui/ui-components/src/layout/Footer.stories.tsx b/scm-ui/ui-components/src/layout/Footer.stories.tsx index 41cc2048bd..60c0b1c8ee 100644 --- a/scm-ui/ui-components/src/layout/Footer.stories.tsx +++ b/scm-ui/ui-components/src/layout/Footer.stories.tsx @@ -24,7 +24,7 @@ import React from "react"; import { storiesOf } from "@storybook/react"; import Footer from "./Footer"; -import { Binder, BinderContext } from "@scm-manager/ui-extensions"; +import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions"; import { Me } from "@scm-manager/ui-types"; import { EXTENSION_POINT } from "../avatar/Avatar"; // @ts-ignore ignore unknown png @@ -43,11 +43,8 @@ const trillian: Me = { _links: {}, }; -const bindAvatar = (binder: Binder, avatar: string) => { - binder.bind(EXTENSION_POINT, () => { - return avatar; - }); -}; +const bindAvatar = (binder: Binder, avatar: string) => + binder.bind(EXTENSION_POINT, () => avatar); const bindLinks = (binder: Binder) => { binder.bind("footer.information", () => ); diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index b603c5b453..c5f154407d 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -23,7 +23,7 @@ */ import React, { FC } from "react"; import { Links, Me } from "@scm-manager/ui-types"; -import { ExtensionPoint, useBinder } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints, useBinder } from "@scm-manager/ui-extensions"; import { AvatarImage } from "../avatar"; import NavLink from "../navigation/NavLink"; import FooterSection from "./FooterSection"; @@ -84,7 +84,7 @@ const Footer: FC = ({ me, version, links }) => { const extensionProps = { me, url: "/me", links }; let meSectionTile; if (me) { - if (binder.hasExtension(EXTENSION_POINT)) { + if (binder.hasExtension(EXTENSION_POINT)) { meSectionTile = ; } else { meSectionTile = ; @@ -105,7 +105,11 @@ const Footer: FC = ({ me, version, links }) => { )} {me?._links?.apiKeys && } - + + name="profile.setting" + props={extensionProps} + renderAll={true} + /> ) : null} }> diff --git a/scm-ui/ui-components/src/markdown/MarkdownCodeRenderer.tsx b/scm-ui/ui-components/src/markdown/MarkdownCodeRenderer.tsx index a1f7710fe4..f85a4cd037 100644 --- a/scm-ui/ui-components/src/markdown/MarkdownCodeRenderer.tsx +++ b/scm-ui/ui-components/src/markdown/MarkdownCodeRenderer.tsx @@ -24,7 +24,7 @@ import React, { FC } from "react"; import SyntaxHighlighter from "../SyntaxHighlighter"; -import { ExtensionPoint, useBinder } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints, useBinder } from "@scm-manager/ui-extensions"; import { useIndexLinks } from "@scm-manager/ui-api"; type Props = { @@ -32,14 +32,15 @@ type Props = { value: string; }; -const MarkdownCodeRenderer: FC = (props) => { +const MarkdownCodeRenderer: FC = props => { const binder = useBinder(); const indexLinks = useIndexLinks(); const { language } = props; - const extensionKey = `markdown-renderer.code.${language}`; - if (binder.hasExtension(extensionKey, props)) { - return ; + const extensionProps = { ...props, indexLinks }; + const extensionKey = `markdown-renderer.code.${language}` as const; + if (binder.hasExtension(extensionKey, extensionProps)) { + return name={extensionKey} props={extensionProps} />; } return ; }; diff --git a/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx b/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx index 82b0e40f55..326164fa05 100644 --- a/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx +++ b/scm-ui/ui-components/src/markdown/MarkdownView.stories.tsx @@ -38,8 +38,8 @@ import MarkdownChangelog from "../__resources__/markdown-changelog.md"; import Title from "../layout/Title"; import { Subtitle } from "../layout"; import { MemoryRouter } from "react-router-dom"; -import { Binder, BinderContext } from "@scm-manager/ui-extensions"; -import { ProtocolLinkRendererExtension, ProtocolLinkRendererProps } from "./markdownExtensions"; +import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions"; +import { ProtocolLinkRendererProps } from "./markdownExtensions"; const Spacing = styled.div` padding: 2em; @@ -64,10 +64,10 @@ storiesOf("MarkdownView", module) )) .add("Links", () => { const binder = new Binder("custom protocol link renderer"); - binder.bind("markdown-renderer.link.protocol", { + binder.bind>("markdown-renderer.link.protocol", { protocol: "scw", renderer: ProtocolLinkRenderer - } as ProtocolLinkRendererExtension); + }); return ( @@ -76,10 +76,10 @@ storiesOf("MarkdownView", module) }) .add("Links without Base Path", () => { const binder = new Binder("custom protocol link renderer"); - binder.bind("markdown-renderer.link.protocol", { + binder.bind>("markdown-renderer.link.protocol", { protocol: "scw", renderer: ProtocolLinkRenderer - } as ProtocolLinkRendererExtension); + }); return ( @@ -107,7 +107,7 @@ storiesOf("MarkdownView", module) ); }; - binder.bind("markdown-renderer.code.uml", Container); + binder.bind>("markdown-renderer.code.uml", Container); return ( @@ -116,7 +116,7 @@ storiesOf("MarkdownView", module) }) .add("XSS Prevention", () => ); -export const ProtocolLinkRenderer: FC = ({ protocol, href, children }) => { +export const ProtocolLinkRenderer: FC> = ({ protocol, href, children }) => { return (

diff --git a/scm-ui/ui-components/src/markdown/markdownExtensions.ts b/scm-ui/ui-components/src/markdown/markdownExtensions.ts index 4d3a0758a8..dfe2f34fd6 100644 --- a/scm-ui/ui-components/src/markdown/markdownExtensions.ts +++ b/scm-ui/ui-components/src/markdown/markdownExtensions.ts @@ -21,18 +21,17 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { FC } from "react"; +import { extensionPoints, ExtractProps } from "@scm-manager/ui-extensions"; -export type ProtocolLinkRendererProps = { - protocol: string; - href: string; -}; +export type ProtocolLinkRendererProps = ExtractProps< + extensionPoints.MarkdownLinkProtocolRenderer["type"]["renderer"] +>; -export type ProtocolLinkRendererExtension = { - protocol: string; - renderer: FC; -}; +/** + * @deprecated use {@link MarkdownLinkProtocolRenderer}`["type"]` instead + */ +export type ProtocolLinkRendererExtension = extensionPoints.MarkdownLinkProtocolRenderer["type"]; export type ProtocolLinkRendererExtensionMap = { - [protocol: string]: FC; + [protocol: string]: extensionPoints.MarkdownLinkProtocolRenderer["type"]["renderer"] | undefined; }; diff --git a/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx b/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx index 7662ac1485..dcd3fb1be3 100644 --- a/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/PrimaryNavigation.tsx @@ -24,9 +24,7 @@ import React, { FC, ReactNode } from "react"; import PrimaryNavigationLink from "./PrimaryNavigationLink"; import { Links } from "@scm-manager/ui-types"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; -import { urls } from "@scm-manager/ui-api"; -import { useLocation } from "react-router-dom"; +import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { useTranslation } from "react-i18next"; type Props = { @@ -37,7 +35,6 @@ type Appender = (to: string, match: string, label: string, linkName: string) => const PrimaryNavigation: FC = ({ links }) => { const [t] = useTranslation("commons"); - const location = useLocation(); const createNavigationAppender = (navItems: ReactNode[]): Appender => { return (to: string, match: string, label: string, linkName: string) => { @@ -63,13 +60,15 @@ const PrimaryNavigation: FC = ({ links }) => { const extensionProps = { links, - label: t("primary-navigation.first-menu"), + label: t("primary-navigation.first-menu") }; const append = createNavigationAppender(navItems); - if (binder.hasExtension("primary-navigation.first-menu", extensionProps)) { + if ( + binder.hasExtension("primary-navigation.first-menu", extensionProps) + ) { navItems.push( - key="primary-navigation.first-menu" name="primary-navigation.first-menu" props={extensionProps} @@ -82,12 +81,12 @@ const PrimaryNavigation: FC = ({ links }) => { append("/admin", "/admin", "primary-navigation.admin", "config"); navItems.push( - key="primary-navigation" name="primary-navigation" renderAll={true} props={{ - links, + links }} /> ); diff --git a/scm-ui/ui-components/src/repos/RepositoryAvatar.tsx b/scm-ui/ui-components/src/repos/RepositoryAvatar.tsx index 113fa4d5ad..2a9a5c4fe9 100644 --- a/scm-ui/ui-components/src/repos/RepositoryAvatar.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryAvatar.tsx @@ -22,7 +22,7 @@ * SOFTWARE. */ import React, { FC } from "react"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; import { Image } from "@scm-manager/ui-components"; import styled from "styled-components"; @@ -38,13 +38,13 @@ type Props = { const renderExtensionPoint = (repository: Repository) => { return ( - name="repos.repository-avatar.primary" props={{ repository, }} > - name="repos.repository-avatar" props={{ repository, diff --git a/scm-ui/ui-components/src/repos/RepositoryEntry.tsx b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx index 30a0daca8e..778b0a9c47 100644 --- a/scm-ui/ui-components/src/repos/RepositoryEntry.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx @@ -25,7 +25,7 @@ import React, { FC, useState } from "react"; import { Repository } from "@scm-manager/ui-types"; import { DateFromNow, Modal } from "@scm-manager/ui-components"; import RepositoryAvatar from "./RepositoryAvatar"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import GroupEntry from "../layout/GroupEntry"; import RepositoryFlags from "./RepositoryFlags"; import styled from "styled-components"; @@ -103,7 +103,7 @@ const RepositoryEntry: FC = ({ repository, baseDate }) => { active={openCloneModal} title={t("overview.clone")} body={ - name="repos.repository-details.information" renderAll={true} props={{ @@ -144,7 +144,10 @@ const RepositoryEntry: FC = ({ repository, baseDate }) => { const actions = createContentRight(); const name = (
- + + name="repository.card.beforeTitle" + props={{ repository }} + /> {repository.name}
); diff --git a/scm-ui/ui-components/src/repos/RepositoryFlags.tsx b/scm-ui/ui-components/src/repos/RepositoryFlags.tsx index b4ca888626..541e34e97d 100644 --- a/scm-ui/ui-components/src/repos/RepositoryFlags.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryFlags.tsx @@ -26,7 +26,7 @@ import { useTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; import { Repository } from "@scm-manager/ui-types"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { TooltipLocation } from "../Tooltip"; import RepositoryFlag from "./RepositoryFlag"; import HealthCheckFailureDetail from "./HealthCheckFailureDetail"; @@ -90,7 +90,11 @@ const RepositoryFlags: FC = ({ repository, className, tooltipLocation = " {modal} {repositoryFlags} - + + name="repository.flags" + props={{ repository, tooltipLocation }} + renderAll={true} + />

); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx index c2c513eed1..514b546cdd 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx @@ -24,7 +24,7 @@ import React, { FC } from "react"; import { Changeset, Person } from "@scm-manager/ui-types"; import { useTranslation } from "react-i18next"; -import { useBinder } from "@scm-manager/ui-extensions"; +import { extensionPoints, useBinder } from "@scm-manager/ui-extensions"; import { EXTENSION_POINT } from "../../avatar/Avatar"; import styled from "styled-components"; import CommaSeparatedList from "../../CommaSeparatedList"; @@ -42,7 +42,7 @@ type PersonProps = { const useAvatar = (person: Person): string | undefined => { const binder = useBinder(); - const factory: (person: Person) => string | undefined = binder.getExtension(EXTENSION_POINT); + const factory = binder.getExtension(EXTENSION_POINT); if (factory) { return factory(person); } @@ -171,7 +171,9 @@ const ChangesetAuthor: FC = ({ changeset }) => { } // extensions - const extensions = binder.getExtensions("changesets.author.suffix", { changeset }); + const extensions = binder.getExtensions("changesets.author.suffix", { + changeset + }); if (extensions) { authorLine.push(...extensions); } diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx index eac2c00823..1be6f0d569 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx @@ -24,8 +24,8 @@ import React, { FC } from "react"; import { Changeset } from "@scm-manager/ui-types"; -import { useBinder } from "@scm-manager/ui-extensions"; -import { SplitAndReplace, Replacement } from "@scm-manager/ui-components"; +import { extensionPoints, useBinder } from "@scm-manager/ui-extensions"; +import { Replacement, SplitAndReplace } from "@scm-manager/ui-components"; type Props = { changeset: Changeset; @@ -35,15 +35,14 @@ type Props = { const ChangesetDescription: FC = ({ changeset, value }) => { const binder = useBinder(); - const replacements: ((changeset: Changeset, value: string) => Replacement[])[] = binder.getExtensions( - "changeset.description.tokens", - { - changeset, - value, - } - ); + const replacements: ((changeset: Changeset, value: string) => Replacement[])[] = binder.getExtensions< + extensionPoints.ChangesetDescriptionTokens + >("changeset.description.tokens", { + changeset, + value + }); - return r(changeset, value))} />; + return r(changeset, value))} />; }; export default ChangesetDescription; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx index 2b49c10af7..8aeeb41e6d 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx @@ -24,7 +24,7 @@ import React, { FC } from "react"; import classNames from "classnames"; import styled from "styled-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Changeset, File, Repository } from "@scm-manager/ui-types"; import ChangesetButtonGroup from "./ChangesetButtonGroup"; import SingleChangeset from "./SingleChangeset"; @@ -54,7 +54,7 @@ const ChangesetRow: FC = ({ repository, changeset, file }) => {
- name="changeset.right" props={{ repository, diff --git a/scm-ui/ui-components/src/repos/changesets/SingleChangeset.tsx b/scm-ui/ui-components/src/repos/changesets/SingleChangeset.tsx index c6e90db13c..eca0831fb6 100644 --- a/scm-ui/ui-components/src/repos/changesets/SingleChangeset.tsx +++ b/scm-ui/ui-components/src/repos/changesets/SingleChangeset.tsx @@ -24,7 +24,7 @@ import React, { FC } from "react"; import classNames from "classnames"; import { AvatarImage, AvatarWrapper } from "../../avatar"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import ChangesetDescription from "./ChangesetDescription"; import { Trans } from "react-i18next"; import ChangesetAuthor from "./ChangesetAuthor"; @@ -72,7 +72,7 @@ const SingleChangeset: FC = ({ repository, changeset }) => {

- 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} />

diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index 6c06359582..ee5f9076b8 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -38,7 +38,7 @@ import { } from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys"; import SetPublicKeysNavLink from "../users/components/navLinks/SetPublicKeysNavLink"; import SetApiKeys from "../users/components/apiKeys/SetApiKeys"; @@ -100,7 +100,11 @@ const Profile: FC = () => { )} - + + name="profile.route" + props={extensionProps} + renderAll={true} + /> diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index 0711aef0d6..4c7d9f8e89 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.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 { CustomQueryFlexWrappedColumns, ErrorPage, @@ -79,7 +79,7 @@ const SingleGroup: FC = () => { - + name="group.route" props={extensionProps} renderAll={true} /> @@ -89,7 +89,11 @@ const SingleGroup: FC = () => { label={t("singleGroup.menu.informationNavLink")} title={t("singleGroup.menu.informationNavLink")} /> - + + name="group.navigation" + props={extensionProps} + renderAll={true} + /> { > - + + name="group.setting" + props={extensionProps} + renderAll={true} + /> diff --git a/scm-ui/ui-webapp/src/index.tsx b/scm-ui/ui-webapp/src/index.tsx index 26a6363f3a..95cef5f44e 100644 --- a/scm-ui/ui-webapp/src/index.tsx +++ b/scm-ui/ui-webapp/src/index.tsx @@ -31,13 +31,13 @@ import i18n from "./i18n"; import { BrowserRouter as Router } from "react-router-dom"; import { urls } from "@scm-manager/ui-components"; -import { binder } from "@scm-manager/ui-extensions"; +import { binder, extensionPoints } from "@scm-manager/ui-extensions"; import ChangesetShortLink from "./repos/components/changesets/ChangesetShortLink"; import "./tokenExpired"; import { ApiProvider } from "@scm-manager/ui-api"; -binder.bind("changeset.description.tokens", ChangesetShortLink); +binder.bind("changeset.description.tokens", ChangesetShortLink); const root = document.getElementById("root"); if (!root) { diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx index d7d13e43e3..03a82ddd49 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx @@ -23,7 +23,7 @@ */ import React from "react"; import BranchDetail from "./BranchDetail"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Branch, Repository } from "@scm-manager/ui-types"; import BranchDangerZone from "../containers/BranchDangerZone"; @@ -40,7 +40,7 @@ class BranchView extends React.Component {
- name="repos.branch-details.information" renderAll={true} props={{ diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx index 91b3347ea7..3165a5f38a 100644 --- a/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx +++ b/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx @@ -25,7 +25,7 @@ import React, { FC } from "react"; import { CUSTOM_NAMESPACE_STRATEGY } from "@scm-manager/ui-types"; import { Autocomplete } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { useTranslation } from "react-i18next"; import { useNamespaceSuggestions } from "@scm-manager/ui-api"; @@ -77,7 +77,13 @@ const NamespaceInput: FC = ({ ); } - return ; + return ( + + name="repos.create.namespace" + props={props} + renderAll={false} + /> + ); }; export default NamespaceInput; diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx index 9aa1fa2ba1..04a6a756ef 100644 --- a/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Repository } from "@scm-manager/ui-types"; import RepositoryDetailTable from "./RepositoryDetailTable"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; type Props = { repository: Repository; @@ -38,7 +38,7 @@ class RepositoryDetails extends React.Component {
- name="repos.repository-details.information" renderAll={true} props={{ diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index 3695bbc7ff..d6ab8ffa0b 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -25,7 +25,7 @@ import React, { FC, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Changeset, ParentChangeset, Repository } from "@scm-manager/ui-types"; import { AvatarImage, @@ -146,7 +146,7 @@ const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory <>
- name="changeset.description" props={{ changeset, @@ -206,7 +206,7 @@ const ChangesetDetails: FC = ({ changeset, repository, fileControlFactory {description.message.split("\n").map((item, key) => { return ( - name="changeset.description" props={{ changeset, diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ContributorTable.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ContributorTable.tsx index 44d853c2dc..247ebfa27d 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ContributorTable.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ContributorTable.tsx @@ -25,8 +25,8 @@ import React, { FC } from "react"; import { Changeset, Person } from "@scm-manager/ui-types"; import styled from "styled-components"; import { useTranslation } from "react-i18next"; -import { useBinder } from "@scm-manager/ui-extensions"; -import { ContributorAvatar, CommaSeparatedList } from "@scm-manager/ui-components"; +import { extensionPoints, useBinder } from "@scm-manager/ui-extensions"; +import { CommaSeparatedList, ContributorAvatar } from "@scm-manager/ui-components"; type Props = { changeset: Changeset; @@ -39,7 +39,7 @@ const SizedTd = styled.td` const Contributor: FC<{ person: Person }> = ({ person }) => { const [t] = useTranslation("repos"); const binder = useBinder(); - const avatarFactory = binder.getExtension("avatar.factory"); + const avatarFactory = binder.getExtension("avatar.factory"); let prefix = null; if (avatarFactory) { const avatar = avatarFactory(person); diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx index 1115a07119..bd1c9d6e1d 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx +++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx @@ -27,7 +27,7 @@ import { NamespaceCollection, Repository } from "@scm-manager/ui-types"; import groupByNamespace from "./groupByNamespace"; import RepositoryGroupEntry from "./RepositoryGroupEntry"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; type Props = { repositories: Repository[]; @@ -44,7 +44,7 @@ class RepositoryList extends React.Component { const groups = groupByNamespace(repositories, namespaces); return (
- name="repository.overview.top" renderAll={true} props={{ diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index 2a5827475a..68b4ffbb46 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -26,7 +26,7 @@ import { useRouteMatch } from "react-router-dom"; import RepositoryForm from "../components/form"; import { Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Subtitle, urls } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import RepositoryDangerZone from "./RepositoryDangerZone"; import { useTranslation } from "react-i18next"; import ExportRepository from "./ExportRepository"; @@ -57,9 +57,17 @@ const EditRepo: FC = ({ repository }) => { - + + name="repo-config.details" + props={extensionProps} + renderAll={true} + /> {repository._links.exportInfo && } - + + name="repo-config.route" + props={extensionProps} + renderAll={true} + /> {(repository._links.runHealthCheck || repository.healthCheckRunning) && ( )} diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx index d2cc1fb112..074cc3202d 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx @@ -128,7 +128,7 @@ const Overview: FC = () => { const [t] = useTranslation("repos"); const binder = useBinder(); - const extensions = binder.getExtensions("repository.overview.left"); + const extensions = binder.getExtensions("repository.overview.left"); // we keep the create permission in the state, // because it does not change during searching or paging @@ -167,8 +167,16 @@ const Overview: FC = () => { return ( {t("overview.title")}} - subtitle={{t("overview.subtitle")}} + title={ + name="repository.overview.title"> + {t("overview.title")} + + } + subtitle={ + name="repository.overview.subtitle"> + {t("overview.subtitle")} + + } loading={isLoading} error={error} > diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 7fcbf36b01..ec8146da26 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -25,7 +25,7 @@ import React, { useState } from "react"; import { match as Match } from "react-router"; import { Link as RouteLink, Redirect, Route, RouteProps, Switch, useRouteMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Changeset, Link } from "@scm-manager/ui-types"; import { CustomQueryFlexWrappedColumns, @@ -122,7 +122,7 @@ const RepositoryRoot = () => { match }; - const redirectUrlFactory = binder.getExtension("repository.redirect", props); + const redirectUrlFactory = binder.getExtension("repository.redirect", props); let redirectedUrl; if (redirectUrlFactory) { redirectedUrl = url + redirectUrlFactory(props); @@ -295,12 +295,20 @@ const RepositoryRoot = () => { - + + name="repository.route" + props={extensionProps} + renderAll={true} + /> - + + name="repository.navigation.topLevel" + props={extensionProps} + renderAll={true} + /> { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.sourcesNavLink")} /> - + + name="repository.navigation" + props={extensionProps} + renderAll={true} + /> { > - + + name="repository.setting" + props={extensionProps} + renderAll={true} + /> diff --git a/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceRoot.tsx b/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceRoot.tsx index 9d70f961de..0b739b9db0 100644 --- a/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceRoot.tsx @@ -34,12 +34,12 @@ import { SecondaryNavigation, SecondaryNavigationColumn, StateMenuContextProvider, - SubNavigation + SubNavigation, + urls, } from "@scm-manager/ui-components"; import Permissions from "../../permissions/containers/Permissions"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import PermissionsNavLink from "./PermissionsNavLink"; -import { urls } from "@scm-manager/ui-components"; import { useNamespace } from "@scm-manager/ui-api"; type Params = { @@ -81,15 +81,27 @@ const NamespaceRoot: FC = () => { - - + + name="namespace.navigation.topLevel" + props={extensionProps} + renderAll={true} + /> + + name="namespace.route" + props={extensionProps} + renderAll={true} + /> - + + name="namespace.setting" + props={extensionProps} + renderAll={true} + /> diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 139362c579..6036cc6796 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -87,7 +87,12 @@ const FileTree: FC = ({ repository, directory, baseUrl, revision, fetchNe return (
- + + name="repos.source.tree.wrapper" + props={extProps} + renderAll={true} + wrapper={true} + > diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx index c48281f9c9..4994f2d0a3 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -116,15 +116,24 @@ class FileTreeLeaf extends React.Component { {this.contentIfPresent(file, "description", file => file.description)} - {binder.hasExtension("repos.sources.tree.row.right") && ( + + {binder.hasExtension("repos.sources.tree.row.right", extProps) && ( {!file.directory && ( - + + name="repos.sources.tree.row.right" + props={extProps} + renderAll={true} + /> )} )} - + + name="repos.sources.tree.row.after" + props={extProps} + renderAll={true} + /> ); } diff --git a/scm-ui/ui-webapp/src/repos/sources/components/content/DownloadViewer.tsx b/scm-ui/ui-webapp/src/repos/sources/components/content/DownloadViewer.tsx index 9691aeb7e1..aabd1f4cbd 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/content/DownloadViewer.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/content/DownloadViewer.tsx @@ -25,7 +25,7 @@ import React from "react"; import { WithTranslation, withTranslation } from "react-i18next"; import { File, Link, Repository } from "@scm-manager/ui-types"; import { DownloadButton } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; type Props = WithTranslation & { repository: Repository; @@ -38,7 +38,10 @@ class DownloadViewer extends React.Component { return (
- + + name="repos.sources.content.downloadButton" + props={{ repository, file }} + >
diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx index 05364c9074..4235fd9cd8 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx @@ -25,7 +25,7 @@ import React, { FC, ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { File, Repository } from "@scm-manager/ui-types"; import { DateFromNow, ErrorNotification, FileSize, Icon, OpenInFullscreenButton } from "@scm-manager/ui-components"; import FileButtonAddons from "../components/content/FileButtonAddons"; @@ -136,7 +136,7 @@ const Content: FC = ({ file, repository, revision, breadcrumb, error }) = modalBody={{content}} tooltipStyle="htmlTitle" /> - name="repos.sources.content.actionbar" props={{ repository, @@ -195,7 +195,7 @@ const Content: FC = ({ file, repository, revision, breadcrumb, error }) =
- name="repos.content.metadata" renderAll={true} props={{ diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx index 8579bbc278..cecde05b68 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx @@ -26,7 +26,7 @@ import React, { FC } from "react"; import SourcecodeViewer from "../components/content/SourcecodeViewer"; import ImageViewer from "../components/content/ImageViewer"; import DownloadViewer from "../components/content/DownloadViewer"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { File, Link, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, PdfViewer } from "@scm-manager/ui-components"; import SwitchableMarkdownViewer from "../components/content/SwitchableMarkdownViewer"; @@ -75,7 +75,7 @@ const SourcesView: FC = ({ file, repository, revision }) => { sources = ; } else { sources = ( - name="repos.sources.view" props={{ file, diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx index 6c94fb72e7..54d7d1ec59 100644 --- a/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx @@ -24,7 +24,7 @@ import React, { FC } from "react"; import { Repository, Tag } from "@scm-manager/ui-types"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import TagDetail from "./TagDetail"; import TagDangerZone from "../container/TagDangerZone"; @@ -39,7 +39,7 @@ const TagView: FC = ({ repository, tag }) => {
- name="repos.tag-details.information" renderAll={true} props={{ diff --git a/scm-ui/ui-webapp/src/search/Hits.tsx b/scm-ui/ui-webapp/src/search/Hits.tsx index b2f1148a9d..e6cbb431b5 100644 --- a/scm-ui/ui-webapp/src/search/Hits.tsx +++ b/scm-ui/ui-webapp/src/search/Hits.tsx @@ -28,9 +28,9 @@ import RepositoryHit from "./RepositoryHit"; import GenericHit from "./GenericHit"; import UserHit from "./UserHit"; import GroupHit from "./GroupHit"; -import { Notification, HitProps } from "@scm-manager/ui-components"; +import { HitProps, Notification } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; type Props = { type: string; @@ -62,7 +62,7 @@ const InternalHitRenderer: FC = ({ type, hit }) => { }; const HitComponent: FC = ({ hit, type }) => ( - + name={`search.hit.${type}.renderer`} props={{ hit }}> ); diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index beddc3d838..f0c16b81b1 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -23,7 +23,7 @@ */ import React, { FC } from "react"; import { Route, useParams, useRouteMatch } from "react-router-dom"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { CustomQueryFlexWrappedColumns, ErrorPage, @@ -97,7 +97,7 @@ const SingleUser: FC = () => { - + name="user.route" props={extensionProps} renderAll={true} /> @@ -119,7 +119,11 @@ const SingleUser: FC = () => { - + + name="user.setting" + props={extensionProps} + renderAll={true} + />
{t("sources.content.description")} {description}