diff --git a/CHANGELOG.md b/CHANGELOG.md index daee9aa86b..e767c09a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Libc based restart strategy for posix operating systems ([#1079](https://github.com/scm-manager/scm-manager/pull/1079)) - Simple restart strategy with System.exit ([#1079](https://github.com/scm-manager/scm-manager/pull/1079)) - Notification if restart is not supported on the underlying platform ([#1079](https://github.com/scm-manager/scm-manager/pull/1079)) +- Extension point before title in repository cards ([#1080](https://github.com/scm-manager/scm-manager/pull/1080)) +- Extension point after title on repository detail page ([#1080](https://github.com/scm-manager/scm-manager/pull/1080)) ### Changed - Update resteasy to version 4.5.2.Final diff --git a/scm-core/src/main/java/sonia/scm/web/api/RepositoryToHalMapper.java b/scm-core/src/main/java/sonia/scm/web/api/RepositoryToHalMapper.java new file mode 100644 index 0000000000..b5184fb202 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/api/RepositoryToHalMapper.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +package sonia.scm.web.api; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.repository.Repository; + +/** + * Maps a repository to a hal representation. + * This is especially useful if a plugin would deliver a repository to the frontend. + * + * @since 2.0.0 + */ +public interface RepositoryToHalMapper { + + /** + * Returns the hal representation of the repository. + * + * @param repository repository to map + * @return hal representation + */ + HalRepresentation map(Repository repository); +} diff --git a/scm-ui/ui-components/src/CardColumn.tsx b/scm-ui/ui-components/src/CardColumn.tsx index 8931fe6a1e..0ff63e31dd 100644 --- a/scm-ui/ui-components/src/CardColumn.tsx +++ b/scm-ui/ui-components/src/CardColumn.tsx @@ -27,7 +27,7 @@ import styled from "styled-components"; import { Link } from "react-router-dom"; type Props = { - title: string; + title: ReactNode; description?: string; avatar: ReactNode; contentRight?: ReactNode; @@ -100,7 +100,7 @@ export default class CardColumn extends React.Component {

- {title} + {title}

{description}

diff --git a/scm-ui/ui-components/src/__resources__/git-logo.png b/scm-ui/ui-components/src/__resources__/git-logo.png new file mode 100644 index 0000000000..ed9393dc36 Binary files /dev/null and b/scm-ui/ui-components/src/__resources__/git-logo.png differ diff --git a/scm-ui/ui-components/src/__resources__/repository.ts b/scm-ui/ui-components/src/__resources__/repository.ts new file mode 100644 index 0000000000..8facf86e57 --- /dev/null +++ b/scm-ui/ui-components/src/__resources__/repository.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ +export default { + contact: "heart-of-gold@hitchhiher.com", + creationDate: "2020-03-23T08:26:01.164Z", + description: "The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive", + healthCheckFailures: [], + lastModified: "2020-03-23T08:26:01.876Z", + namespace: "hitchhiher", + name: "heartOfGold", + type: "git", + _links: { + self: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git" }, + delete: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git" }, + update: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git" }, + permissions: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/permissions/" }, + protocol: [ + { href: "ssh://scmadmin@localhost:4567/repo/scmadmin/Git", name: "ssh" }, + { href: "http://localhost:8081/scm/repo/scmadmin/Git", name: "http" } + ], + tags: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/tags/" }, + branches: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/branches/" }, + incomingChangesets: { + href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/incoming/{source}/{target}/changesets", + templated: true + }, + incomingDiff: { + href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/incoming/{source}/{target}/diff", + templated: true + }, + incomingDiffParsed: { + href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/incoming/{source}/{target}/diff/parsed", + templated: true + }, + changesets: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/changesets/" }, + sources: { href: "http://localhost:8081/scm/api/v2/repositories/scmadmin/Git/sources/" }, + authorMappingConfig: { + href: "http://localhost:8081/scm/api/v2/authormapping/configuration/scmadmin/Git" + }, + unfavorize: { href: "http://localhost:8081/scm/api/v2/unfavorize/scmadmin/Git" }, + favorites: [ + { href: "http://localhost:8081/scm/api/v2/unfavorize/scmadmin/Git", name: "unfavorize" }, + { href: "http://localhost:8081/scm/api/v2/favorize/scmadmin/Git", name: "favorize" } + ] + } +}; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 790dc01249..77aec20677 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -34188,6 +34188,472 @@ exports[`Storyshots MarkdownView Xml Code Block 1`] = `
`; +exports[`Storyshots RepositoryEntry Avatar EP 1`] = ` +
+ + +
+`; + +exports[`Storyshots RepositoryEntry Before Title EP 1`] = ` + +`; + +exports[`Storyshots RepositoryEntry Default 1`] = ` + +`; + +exports[`Storyshots RepositoryEntry Quick Link EP 1`] = ` + +`; + exports[`Storyshots SyntaxHighlighter Go 1`] = `
{ componentDidUpdate() { const { title } = this.props; @@ -80,7 +90,7 @@ export default class Page extends React.Component { } renderPageHeader() { - const { error, title, subtitle, children } = this.props; + const { error, title, afterTitle, subtitle, children } = this.props; let pageActions = null; let pageActionsExists = false; @@ -104,7 +114,9 @@ export default class Page extends React.Component { <>
- + <FlexContainer> + <Title title={title} /> {afterTitle && <MarginLeft>{afterTitle}</MarginLeft>} + </FlexContainer> <Subtitle subtitle={subtitle} /> </div> {pageActions} diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryAvatar.tsx b/scm-ui/ui-components/src/repos/RepositoryAvatar.tsx similarity index 100% rename from scm-ui/ui-webapp/src/repos/components/list/RepositoryAvatar.tsx rename to scm-ui/ui-components/src/repos/RepositoryAvatar.tsx diff --git a/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx b/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx new file mode 100644 index 0000000000..f8b9994fd0 --- /dev/null +++ b/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx @@ -0,0 +1,93 @@ +/* + * 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 {storiesOf} from "@storybook/react"; +import React, {FC, ReactNode} from "react"; +import styled from "styled-components"; +import repository from "../__resources__/repository"; +// @ts-ignore ignore unknown png +import Git from "../__resources__/git-logo.png"; +import RepositoryEntry from "./RepositoryEntry"; +import {Binder, BinderContext} from "@scm-manager/ui-extensions"; +import {Repository} from "@scm-manager/ui-types"; +import Image from "../Image"; +import classNames from "classnames"; +import Icon from "../Icon"; + +const Spacing = styled.div` + margin: 2rem; +`; + +const Container: FC = ({children}) => ( + <Spacing className="box box-link-shadow">{children}</Spacing> +); + +const bindAvatar = (binder: Binder, avatar: string) => { + binder.bind("repos.repository-avatar", () => { + return <Image src={avatar} alt="Logo"/>; + }); +}; + +const bindBeforeTitle = (binder: Binder, extension: ReactNode) => { + binder.bind("repository.card.beforeTitle", () => { + return extension; + }); +}; + +const bindQuickLink = (binder: Binder, extension: ReactNode) => { + binder.bind("repository.card.quickLink", () => { + return extension; + }); +}; + +const withBinder = (binder: Binder, repository: Repository) => { + return ( + <BinderContext.Provider value={binder}> + <RepositoryEntry repository={repository}/> + </BinderContext.Provider> + ); +}; + +const QuickLink = <a className="level-item"><Icon className="fa-lg" name="fas fa-code-branch fa-rotate-180 fa-fw" + color="inherit"/></a>; + +storiesOf("RepositoryEntry", module) + .addDecorator(storyFn => <Container>{storyFn()}</Container>) + .add("Default", () => { + return <RepositoryEntry repository={repository}/>; + }) + .add("Avatar EP", () => { + const binder = new Binder("avatar"); + bindAvatar(binder, Git); + return withBinder(binder, repository); + }) + .add("Before Title EP", () => { + const binder = new Binder("title"); + bindBeforeTitle(binder, <i className="fas fa-star"/>); + return withBinder(binder, repository); + }) + .add("Quick Link EP", () => { + const binder = new Binder("title"); + bindQuickLink(binder, QuickLink); + return withBinder(binder, repository); + }); diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryEntry.tsx b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx similarity index 93% rename from scm-ui/ui-webapp/src/repos/components/list/RepositoryEntry.tsx rename to scm-ui/ui-components/src/repos/RepositoryEntry.tsx index 37b4bd192b..8bd29309aa 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryEntry.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx @@ -85,15 +85,25 @@ class RepositoryEntry extends React.Component<Props> { ); }; + createTitle = () => { + const { repository } = this.props; + return ( + <> + <ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} /> <strong>{repository.name}</strong> + </> + ); + }; + render() { const { repository } = this.props; const repositoryLink = this.createLink(repository); const footerLeft = this.createFooterLeft(repository, repositoryLink); const footerRight = this.createFooterRight(repository); + const title = this.createTitle(); return ( <CardColumn avatar={<RepositoryAvatar repository={repository} />} - title={repository.name} + title={title} description={repository.description} link={repositoryLink} footerLeft={footerLeft} diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryEntryLink.tsx b/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx similarity index 100% rename from scm-ui/ui-webapp/src/repos/components/list/RepositoryEntryLink.tsx rename to scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx diff --git a/scm-ui/ui-components/src/repos/index.ts b/scm-ui/ui-components/src/repos/index.ts index b3208df266..01f6c97daa 100644 --- a/scm-ui/ui-components/src/repos/index.ts +++ b/scm-ui/ui-components/src/repos/index.ts @@ -46,6 +46,9 @@ export { default as DiffFile } from "./DiffFile"; export { default as DiffButton } from "./DiffButton"; export { default as LoadingDiff } from "./LoadingDiff"; export { DefaultCollapsed, DefaultCollapsedFunction } from "./defaultCollapsed"; +export { default as RepositoryAvatar } from "./RepositoryAvatar"; +export { default as RepositoryEntry } from "./RepositoryEntry"; +export { default as RepositoryEntryLink } from "./RepositoryEntryLink"; export { File, diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index 3adb51da90..e799fe38e2 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -433,6 +433,7 @@ $danger-25: scale-color($danger, $lightness: 75%); @import "~@fortawesome/fontawesome-free/scss/fontawesome"; $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "~@fortawesome/fontawesome-free/scss/solid"; +@import "~@fortawesome/fontawesome-free/scss/regular"; @import "~@fortawesome/fontawesome-free/scss/brands"; @import "~react-diff-view/style/index"; diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx index 96d123361f..cb93ef5d6c 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx @@ -181,7 +181,7 @@ class PluginEntry extends React.Component<Props, State> { <CardColumn action={this.isInstallable() ? () => this.toggleModal("showInstallModal") : null} avatar={avatar} - title={plugin.displayName ? plugin.displayName : plugin.name} + title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>} description={plugin.description} contentRight={plugin.pending || plugin.markedForUninstall ? this.createPendingSpinner() : actionbar} footerRight={footerRight} diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryDetailTable.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryDetailTable.tsx index 9e2e6536b4..f1a5fbcf2e 100644 --- a/scm-ui/ui-webapp/src/repos/components/RepositoryDetailTable.tsx +++ b/scm-ui/ui-webapp/src/repos/components/RepositoryDetailTable.tsx @@ -25,6 +25,7 @@ import React from "react"; import { WithTranslation, withTranslation } from "react-i18next"; import { Repository } from "@scm-manager/ui-types"; import { DateFromNow, MailLink } from "@scm-manager/ui-components"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = WithTranslation & { repository: Repository; diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx index 15a1171525..a62eff1b46 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx +++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx @@ -22,9 +22,8 @@ * SOFTWARE. */ import React from "react"; -import { CardColumnGroup } from "@scm-manager/ui-components"; +import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components"; import { RepositoryGroup } from "@scm-manager/ui-types"; -import RepositoryEntry from "./RepositoryEntry"; type Props = { group: RepositoryGroup; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 88aff9fcd7..9651cd0a0b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -172,7 +172,10 @@ class RepositoryRoot extends React.Component<Props, State> { setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }} > - <Page title={repository.namespace + "/" + repository.name}> + <Page + title={repository.namespace + "/" + repository.name} + afterTitle={<ExtensionPoint name={"repository.afterTitle"} props={{ repository }} />} + > <CustomQueryFlexWrappedColumns> <PrimaryContentColumn collapsed={menuCollapsed}> <Switch> diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 4b11e53c4c..83be1f3636 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources; import com.google.inject.AbstractModule; import com.google.inject.servlet.ServletScopes; import org.mapstruct.factory.Mappers; +import sonia.scm.web.api.RepositoryToHalMapper; public class MapperModule extends AbstractModule { @Override @@ -70,6 +71,8 @@ public class MapperModule extends AbstractModule { bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass()); bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); + bind(RepositoryToHalMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + // no mapstruct required bind(MeDtoFactory.class); bind(UIPluginDtoMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 51a1583255..2105124015 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -38,6 +38,7 @@ import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.ScmProtocol; +import sonia.scm.web.api.RepositoryToHalMapper; import java.util.List; @@ -49,7 +50,7 @@ import static java.util.stream.Collectors.toList; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @Mapper -public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Repository, RepositoryDto> { +public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Repository, RepositoryDto> implements RepositoryToHalMapper { @Inject private ResourceLinks resourceLinks; @@ -58,6 +59,9 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); + @Override + public abstract RepositoryDto map(Repository modelObject); + @ObjectFactory RepositoryDto createDto(Repository repository) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.repository().self(repository.getNamespace(), repository.getName()));