mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-21 15:59:48 +01:00
Add extension point for repository creators (#1657)
Adds an extension point for repository creator such as repository create, repository import or repository mirror.
This commit is contained in:
2
gradle/changelog/extension_point_repository_creator.yaml
Normal file
2
gradle/changelog/extension_point_repository_creator.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Extension Point for repository creators ([#1657](https://github.com/scm-manager/scm-manager/pull/1657))
|
||||
@@ -10,7 +10,8 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^17.0.1"
|
||||
"react": "^17.0.1",
|
||||
"@scm-manager/ui-types": "^2.18.1-SNAPSHOT"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/babel-preset": "^2.12.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ type ExtensionRegistration<P, T> = {
|
||||
extensionName: string;
|
||||
};
|
||||
|
||||
export type ExtensionPointDefinition<N extends string, T, P> = {
|
||||
export type ExtensionPointDefinition<N extends string, T, P = undefined> = {
|
||||
name: N;
|
||||
type: T;
|
||||
props: P;
|
||||
|
||||
57
scm-ui/ui-extensions/src/extensionPoints.ts
Normal file
57
scm-ui/ui-extensions/src/extensionPoints.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { ExtensionPointDefinition } from "./binder";
|
||||
import {
|
||||
IndexResources,
|
||||
NamespaceStrategies,
|
||||
RepositoryCreation,
|
||||
RepositoryTypeCollection
|
||||
} from "@scm-manager/ui-types";
|
||||
|
||||
type RepositoryCreatorSubFormProps = {
|
||||
repository: RepositoryCreation;
|
||||
onChange: (repository: RepositoryCreation) => void;
|
||||
setValid: (valid: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type RepositoryCreatorComponentProps = {
|
||||
namespaceStrategies: NamespaceStrategies;
|
||||
repositoryTypes: RepositoryTypeCollection;
|
||||
index: IndexResources;
|
||||
|
||||
nameForm: React.ComponentType<RepositoryCreatorSubFormProps>;
|
||||
informationForm: React.ComponentType<RepositoryCreatorSubFormProps>;
|
||||
};
|
||||
|
||||
export type RepositoryCreatorExtension = {
|
||||
subtitle: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
component: React.ComponentType<RepositoryCreatorComponentProps>;
|
||||
};
|
||||
|
||||
export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
|
||||
@@ -25,3 +25,8 @@
|
||||
export { default as binder, Binder, ExtensionPointDefinition } from "./binder";
|
||||
export * from "./useBinder";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint";
|
||||
|
||||
// suppress eslint prettier warning,
|
||||
// because prettier does not understand "* as"
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
export * as extensionPoints from "./extensionPoints";
|
||||
|
||||
@@ -56,7 +56,7 @@ export type RepositoryCreation = RepositoryBase & {
|
||||
contextEntries: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type RepositoryUrlImport = Repository & {
|
||||
export type RepositoryUrlImport = RepositoryCreation & {
|
||||
importUrl: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
|
||||
@@ -37,7 +37,6 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import CreateUser from "../users/containers/CreateUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
import RepositoryRoot from "../repos/containers/RepositoryRoot";
|
||||
import CreateRepository from "../repos/containers/CreateRepository";
|
||||
|
||||
import Groups from "../groups/containers/Groups";
|
||||
import SingleGroup from "../groups/containers/SingleGroup";
|
||||
@@ -47,8 +46,8 @@ import Admin from "../admin/containers/Admin";
|
||||
|
||||
import Profile from "./Profile";
|
||||
import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot";
|
||||
import ImportRepository from "../repos/containers/ImportRepository";
|
||||
import ImportLog from "../repos/importlog/ImportLog";
|
||||
import CreateRepositoryRoot from "../repos/containers/CreateRepositoryRoot";
|
||||
|
||||
type Props = {
|
||||
me: Me;
|
||||
@@ -79,8 +78,8 @@ class Main extends React.Component<Props> {
|
||||
<Route path="/logout" component={Logout} />
|
||||
<Redirect exact strict from="/repos" to="/repos/" />
|
||||
<ProtectedRoute exact path="/repos/" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/create" component={CreateRepository} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/import" component={ImportRepository} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/repos/create" component={CreateRepositoryRoot} authenticated={authenticated} />
|
||||
<Redirect exact strict from="/repos/import" to="/repos/create/import" />
|
||||
<ProtectedRoute exact path="/repos/:namespace" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/:namespace/:page" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/repo/:namespace/:name" component={RepositoryRoot} authenticated={authenticated} />
|
||||
|
||||
@@ -22,28 +22,35 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import NamespaceAndNameFields from "./NamespaceAndNameFields";
|
||||
import { File, Repository } from "@scm-manager/ui-types";
|
||||
import RepositoryInformationForm from "./RepositoryInformationForm";
|
||||
import { File, RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import ImportFullRepositoryForm from "./ImportFullRepositoryForm";
|
||||
import { SubFormProps } from "../types";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
};
|
||||
|
||||
const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending }) => {
|
||||
const [repo, setRepo] = useState<Repository>({
|
||||
const ImportFullRepository: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryCreation>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
contact: "",
|
||||
description: "",
|
||||
_links: {}
|
||||
contextEntries: []
|
||||
});
|
||||
const [password, setPassword] = useState("");
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
|
||||
@@ -97,15 +104,15 @@ const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending
|
||||
setValid={(file: boolean) => setValid({ ...valid, file })}
|
||||
/>
|
||||
<hr />
|
||||
<NamespaceAndNameFields
|
||||
<NameForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
/>
|
||||
<RepositoryInformationForm
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
|
||||
@@ -22,28 +22,35 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import NamespaceAndNameFields from "./NamespaceAndNameFields";
|
||||
import { File, Repository } from "@scm-manager/ui-types";
|
||||
import RepositoryInformationForm from "./RepositoryInformationForm";
|
||||
import { File, RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import ImportFromBundleForm from "./ImportFromBundleForm";
|
||||
import { SubFormProps } from "../types";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
};
|
||||
|
||||
const ImportRepositoryFromBundle: FC<Props> = ({ url, repositoryType, setImportPending }) => {
|
||||
const [repo, setRepo] = useState<Repository>({
|
||||
const ImportRepositoryFromBundle: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryCreation>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
contact: "",
|
||||
description: "",
|
||||
_links: {}
|
||||
contextEntries: []
|
||||
});
|
||||
const [password, setPassword] = useState("");
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
|
||||
@@ -101,15 +108,15 @@ const ImportRepositoryFromBundle: FC<Props> = ({ url, repositoryType, setImportP
|
||||
disabled={loading}
|
||||
/>
|
||||
<hr />
|
||||
<NamespaceAndNameFields
|
||||
<NameForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
/>
|
||||
<RepositoryInformationForm
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
|
||||
@@ -22,21 +22,28 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import NamespaceAndNameFields from "./NamespaceAndNameFields";
|
||||
import { Repository, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import { RepositoryCreation, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import ImportFromUrlForm from "./ImportFromUrlForm";
|
||||
import RepositoryInformationForm from "./RepositoryInformationForm";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { SubFormProps } from "../types";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
};
|
||||
|
||||
const ImportRepositoryFromUrl: FC<Props> = ({ url, repositoryType, setImportPending }) => {
|
||||
const ImportRepositoryFromUrl: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryUrlImport>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
@@ -46,7 +53,7 @@ const ImportRepositoryFromUrl: FC<Props> = ({ url, repositoryType, setImportPend
|
||||
importUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
_links: {}
|
||||
contextEntries: []
|
||||
});
|
||||
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, importUrl: false });
|
||||
@@ -96,15 +103,15 @@ const ImportRepositoryFromUrl: FC<Props> = ({ url, repositoryType, setImportPend
|
||||
disabled={loading}
|
||||
/>
|
||||
<hr />
|
||||
<NamespaceAndNameFields
|
||||
<NameForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
/>
|
||||
<RepositoryInformationForm
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Repository, CUSTOM_NAMESPACE_STRATEGY } from "@scm-manager/ui-types";
|
||||
import { CUSTOM_NAMESPACE_STRATEGY, RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InputField } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
@@ -31,8 +31,8 @@ import * as validator from "./form/repositoryValidation";
|
||||
import { useNamespaceStrategies } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
onChange: (repository: Repository) => void;
|
||||
repository: RepositoryCreation;
|
||||
onChange: (repository: RepositoryCreation) => void;
|
||||
setValid: (valid: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -83,10 +83,10 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
|
||||
};
|
||||
|
||||
const renderNamespaceField = () => {
|
||||
let informationMessage = undefined;
|
||||
if (repository?.namespace?.indexOf(" ") > 0) {
|
||||
informationMessage = t("validation.namespaceSpaceWarningText");
|
||||
}
|
||||
let informationMessage = undefined;
|
||||
if (repository?.namespace?.indexOf(" ") > 0) {
|
||||
informationMessage = t("validation.namespaceSpaceWarningText");
|
||||
}
|
||||
|
||||
const props = {
|
||||
label: t("repository.namespace"),
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { InputField, Textarea } from "@scm-manager/ui-components";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as validator from "./form/repositoryValidation";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
onChange: (repository: Repository) => void;
|
||||
disabled: boolean;
|
||||
repository: RepositoryCreation;
|
||||
onChange: (repository: RepositoryCreation) => void;
|
||||
setValid: (valid: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const RepositoryInformationForm: FC<Props> = ({ repository, onChange, disabled, setValid }) => {
|
||||
|
||||
@@ -24,12 +24,8 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonAddons, Icon, Level } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
creationMode: "CREATE" | "IMPORT";
|
||||
};
|
||||
import { Button, ButtonAddons, Icon, Level, urls } from "@scm-manager/ui-components";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const MarginIcon = styled(Icon)`
|
||||
padding-right: 0.5rem;
|
||||
@@ -52,40 +48,39 @@ const TopLevel = styled(Level)`
|
||||
}
|
||||
`;
|
||||
|
||||
const RepositoryFormSwitcher: FC<Props> = ({ creationMode }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
type RepositoryForm = {
|
||||
path: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const isImportMode = () => {
|
||||
return creationMode === "IMPORT";
|
||||
};
|
||||
|
||||
const isCreateMode = () => {
|
||||
return creationMode === "CREATE";
|
||||
};
|
||||
const RepositoryFormButton: FC<RepositoryForm> = ({ path, icon, label }) => {
|
||||
const location = useLocation();
|
||||
const href = urls.concat("/repos/create", path);
|
||||
const isSelected = href === location.pathname;
|
||||
|
||||
return (
|
||||
<TopLevel
|
||||
right={
|
||||
<ButtonAddons>
|
||||
<SmallButton
|
||||
color={isCreateMode() ? "link is-selected" : undefined}
|
||||
link={isImportMode() ? "/repos/create" : undefined}
|
||||
>
|
||||
<MarginIcon name="fa fa-plus" color={isCreateMode() ? "white" : "default"} />
|
||||
<p className="is-hidden-mobile is-hidden-tablet-only">{t("repositoryForm.createButton")}</p>
|
||||
</SmallButton>
|
||||
<SmallButton
|
||||
color={isImportMode() ? "link is-selected" : undefined}
|
||||
link={isCreateMode() ? "/repos/import" : undefined}
|
||||
className="has-text-left-desktop"
|
||||
>
|
||||
<MarginIcon name="fa fa-file-upload" color={isImportMode() ? "white" : "default"} />
|
||||
<p className="is-hidden-mobile is-hidden-tablet-only">{t("repositoryForm.importButton")}</p>
|
||||
</SmallButton>
|
||||
</ButtonAddons>
|
||||
}
|
||||
/>
|
||||
<SmallButton color={isSelected ? "link is-selected" : undefined} link={!isSelected ? href : undefined}>
|
||||
<MarginIcon name={icon} color={isSelected ? "white" : "default"} />
|
||||
<p className="is-hidden-mobile is-hidden-tablet-only">{label}</p>
|
||||
</SmallButton>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
forms: RepositoryForm[];
|
||||
};
|
||||
|
||||
const RepositoryFormSwitcher: FC<Props> = ({ forms }) => (
|
||||
<TopLevel
|
||||
right={
|
||||
<ButtonAddons>
|
||||
{(forms || []).map(form => (
|
||||
<RepositoryFormButton key={form.path} {...form} />
|
||||
))}
|
||||
</ButtonAddons>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default RepositoryFormSwitcher;
|
||||
|
||||
@@ -22,54 +22,30 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Page } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||
import RepositoryForm from "../components/form";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { useCreateRepository, useIndex, useNamespaceStrategies, useRepositoryTypes } from "@scm-manager/ui-api";
|
||||
import { useCreateRepository } from "@scm-manager/ui-api";
|
||||
import { CreatorComponentProps } from "../types";
|
||||
|
||||
const useCreateRepositoryData = () => {
|
||||
const { isLoading: isLoadingNS, error: errorNS, data: namespaceStrategies } = useNamespaceStrategies();
|
||||
const { isLoading: isLoadingRT, error: errorRT, data: repositoryTypes } = useRepositoryTypes();
|
||||
const { isLoading: isLoadingIdx, error: errorIdx, data: index } = useIndex();
|
||||
return {
|
||||
isPageLoading: isLoadingNS || isLoadingRT || isLoadingIdx,
|
||||
pageLoadingError: errorNS || errorRT || errorIdx || undefined,
|
||||
namespaceStrategies,
|
||||
repositoryTypes,
|
||||
index
|
||||
};
|
||||
};
|
||||
|
||||
const CreateRepository: FC = () => {
|
||||
const { isPageLoading, pageLoadingError, namespaceStrategies, repositoryTypes, index } = useCreateRepositoryData();
|
||||
const CreateRepository: FC<CreatorComponentProps> = ({ repositoryTypes, namespaceStrategies, index }) => {
|
||||
const { isLoading, error, repository, create } = useCreateRepository();
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (repository) {
|
||||
return <Redirect to={`/repo/${repository.namespace}/${repository.name}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
subtitle={t("create.subtitle")}
|
||||
afterTitle={<RepositoryFormSwitcher creationMode={"CREATE"} />}
|
||||
loading={isPageLoading}
|
||||
error={pageLoadingError || error || undefined}
|
||||
showContentOnError={true}
|
||||
>
|
||||
{namespaceStrategies && repositoryTypes ? (
|
||||
<RepositoryForm
|
||||
repositoryTypes={repositoryTypes._embedded.repositoryTypes}
|
||||
loading={isLoading}
|
||||
namespaceStrategy={namespaceStrategies!.current}
|
||||
createRepository={create}
|
||||
indexResources={index}
|
||||
/>
|
||||
) : null}
|
||||
</Page>
|
||||
<>
|
||||
<ErrorNotification error={error} />
|
||||
<RepositoryForm
|
||||
repositoryTypes={repositoryTypes._embedded.repositoryTypes}
|
||||
loading={isLoading}
|
||||
namespaceStrategy={namespaceStrategies.current}
|
||||
createRepository={create}
|
||||
indexResources={index}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
120
scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx
Normal file
120
scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import CreateRepository from "./CreateRepository";
|
||||
import ImportRepository from "./ImportRepository";
|
||||
import { useBinder } from "@scm-manager/ui-extensions";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Page, urls } from "@scm-manager/ui-components";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import { useIndex, useNamespaceStrategies, useRepositoryTypes } from "@scm-manager/ui-api";
|
||||
import NamespaceAndNameFields from "../components/NamespaceAndNameFields";
|
||||
import RepositoryInformationForm from "../components/RepositoryInformationForm";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions/";
|
||||
|
||||
type CreatorRouteProps = {
|
||||
creator: extensionPoints.RepositoryCreatorExtension;
|
||||
creators: extensionPoints.RepositoryCreatorExtension[];
|
||||
};
|
||||
|
||||
const useCreateRepositoryData = () => {
|
||||
const { isLoading: isLoadingNS, error: errorNS, data: namespaceStrategies } = useNamespaceStrategies();
|
||||
const { isLoading: isLoadingRT, error: errorRT, data: repositoryTypes } = useRepositoryTypes();
|
||||
const { isLoading: isLoadingIdx, error: errorIdx, data: index } = useIndex();
|
||||
return {
|
||||
isPageLoading: isLoadingNS || isLoadingRT || isLoadingIdx,
|
||||
pageLoadingError: errorNS || errorRT || errorIdx || undefined,
|
||||
namespaceStrategies,
|
||||
repositoryTypes,
|
||||
index
|
||||
};
|
||||
};
|
||||
|
||||
const CreatorRoute: FC<CreatorRouteProps> = ({ creator, creators }) => {
|
||||
const { isPageLoading, pageLoadingError, namespaceStrategies, repositoryTypes, index } = useCreateRepositoryData();
|
||||
const [t] = useTranslation(["repos", "plugins"]);
|
||||
|
||||
const Component = creator.component;
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={t("repos:create.title")}
|
||||
subtitle={t(`plugins:${creator.subtitle}`, creator.subtitle)}
|
||||
afterTitle={<RepositoryFormSwitcher forms={creators} />}
|
||||
loading={isPageLoading}
|
||||
error={pageLoadingError}
|
||||
>
|
||||
{namespaceStrategies && repositoryTypes && index ? (
|
||||
<Component
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
repositoryTypes={repositoryTypes}
|
||||
index={index}
|
||||
nameForm={NamespaceAndNameFields}
|
||||
informationForm={RepositoryInformationForm}
|
||||
/>
|
||||
) : null}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateRepositoryRoot: FC = () => {
|
||||
const [t] = useTranslation("repos");
|
||||
const binder = useBinder();
|
||||
|
||||
const creators: extensionPoints.RepositoryCreatorExtension[] = [
|
||||
{
|
||||
subtitle: t("create.subtitle"),
|
||||
path: "",
|
||||
icon: "plus",
|
||||
label: t("repositoryForm.createButton"),
|
||||
component: CreateRepository
|
||||
},
|
||||
{
|
||||
subtitle: t("import.subtitle"),
|
||||
path: "import",
|
||||
icon: "file-upload",
|
||||
label: t("repositoryForm.importButton"),
|
||||
component: ImportRepository
|
||||
}
|
||||
];
|
||||
|
||||
const extCreators = binder.getExtensions<extensionPoints.RepositoryCreator>("repos.creator");
|
||||
if (extCreators) {
|
||||
creators.push(...extCreators);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{creators.map(creator => (
|
||||
<Route key={creator.path} exact path={urls.concat("/repos/create", creator.path)}>
|
||||
<CreatorRoute creator={creator} creators={creators} />
|
||||
</Route>
|
||||
))}
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRepositoryRoot;
|
||||
@@ -29,13 +29,12 @@ import { useTranslation } from "react-i18next";
|
||||
import ImportRepositoryTypeSelect from "../components/ImportRepositoryTypeSelect";
|
||||
import ImportTypeSelect from "../components/ImportTypeSelect";
|
||||
import ImportRepositoryFromUrl from "../components/ImportRepositoryFromUrl";
|
||||
import { Loading, Notification, Page, useNavigationLock } from "@scm-manager/ui-components";
|
||||
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
|
||||
import { Loading, Notification, useNavigationLock } from "@scm-manager/ui-components";
|
||||
|
||||
import ImportRepositoryFromBundle from "../components/ImportRepositoryFromBundle";
|
||||
import ImportFullRepository from "../components/ImportFullRepository";
|
||||
import { useRepositoryTypes } from "@scm-manager/ui-api";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { CreatorComponentProps } from "../types";
|
||||
|
||||
const ImportPendingLoading = ({ importPending }: { importPending: boolean }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
@@ -51,8 +50,7 @@ const ImportPendingLoading = ({ importPending }: { importPending: boolean }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportRepository: FC = () => {
|
||||
const { data: repositoryTypes, error, isLoading } = useRepositoryTypes();
|
||||
const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm, informationForm }) => {
|
||||
const [importPending, setImportPending] = useState(false);
|
||||
const [repositoryType, setRepositoryType] = useState<RepositoryType | undefined>();
|
||||
const [importType, setImportType] = useState("");
|
||||
@@ -72,6 +70,8 @@ const ImportRepository: FC = () => {
|
||||
url={((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "url") as Link).href}
|
||||
repositoryType={repositoryType!.name}
|
||||
setImportPending={setImportPending}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,6 +82,8 @@ const ImportRepository: FC = () => {
|
||||
url={((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "bundle") as Link).href}
|
||||
repositoryType={repositoryType!.name}
|
||||
setImportPending={setImportPending}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -94,6 +96,8 @@ const ImportRepository: FC = () => {
|
||||
}
|
||||
repositoryType={repositoryType!.name}
|
||||
setImportPending={setImportPending}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -102,14 +106,7 @@ const ImportRepository: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
subtitle={t("import.subtitle")}
|
||||
afterTitle={<RepositoryFormSwitcher creationMode={"IMPORT"} />}
|
||||
loading={isLoading}
|
||||
error={error || undefined}
|
||||
showContentOnError={true}
|
||||
>
|
||||
<>
|
||||
<Prompt when={importPending} message={t("import.navigationWarning")} />
|
||||
<ImportPendingLoading importPending={importPending} />
|
||||
<ImportRepositoryTypeSelect
|
||||
@@ -131,7 +128,7 @@ const ImportRepository: FC = () => {
|
||||
</>
|
||||
)}
|
||||
{importType && renderImportComponent()}
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,23 +24,25 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we need no equals for dtos
|
||||
public class RepositoryTypeDto extends HalRepresentation {
|
||||
|
||||
private String name;
|
||||
private String displayName;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
return super.add(links);
|
||||
public RepositoryTypeDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,40 +24,48 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
@Mapper
|
||||
public abstract class RepositoryTypeToRepositoryTypeDtoMapper extends BaseMapper<RepositoryType, RepositoryTypeDto> {
|
||||
|
||||
private static final String REL_IMPORT = "import";
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(RepositoryType repositoryType, @MappingTarget RepositoryTypeDto target) {
|
||||
@ObjectFactory
|
||||
RepositoryTypeDto create(RepositoryType repositoryType) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryType().self(repositoryType.getName()));
|
||||
|
||||
if (RepositoryPermissions.create().isPermitted()) {
|
||||
if (repositoryType.getSupportedCommands().contains(Command.PULL)) {
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().importFromUrl(repositoryType.getName())).withName("url").build());
|
||||
linksBuilder.array(Link.linkBuilder(REL_IMPORT, resourceLinks.repository().importFromUrl(repositoryType.getName())).withName("url").build());
|
||||
}
|
||||
if (repositoryType.getSupportedCommands().contains(Command.UNBUNDLE)) {
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().importFromBundle(repositoryType.getName())).withName("bundle").build());
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().fullImport(repositoryType.getName())).withName("fullImport").build());
|
||||
linksBuilder.array(Link.linkBuilder(REL_IMPORT, resourceLinks.repository().importFromBundle(repositoryType.getName())).withName("bundle").build());
|
||||
linksBuilder.array(Link.linkBuilder(REL_IMPORT, resourceLinks.repository().fullImport(repositoryType.getName())).withName("fullImport").build());
|
||||
}
|
||||
}
|
||||
|
||||
target.add(linksBuilder.build());
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
|
||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), repositoryType);
|
||||
|
||||
return new RepositoryTypeDto(linksBuilder.build(), embeddedBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,28 +26,27 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import de.otto.edison.hal.Link;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.assertj.core.data.Index;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
@SubjectAware("slarti")
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
|
||||
private final URI baseUri = URI.create("https://scm-manager.org/scm/");
|
||||
|
||||
@@ -55,96 +54,119 @@ public class RepositoryTypeToRepositoryTypeDtoMapperTest {
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
private HalEnricherRegistry registry;
|
||||
|
||||
@InjectMocks
|
||||
private RepositoryTypeToRepositoryTypeDtoMapperImpl mapper;
|
||||
|
||||
private final RepositoryType type = new RepositoryType("hk", "Hitchhiker", Sets.newHashSet());
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ThreadContext.unbindSubject();
|
||||
@Test
|
||||
void shouldMapSimpleProperties() {
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertThat(dto.getName()).isEqualTo("hk");
|
||||
assertThat(dto.getDisplayName()).isEqualTo("Hitchhiker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapSimpleProperties() {
|
||||
void shouldAppendSelfLink() {
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertEquals("hk", dto.getName());
|
||||
assertEquals("Hitchhiker", dto.getDisplayName());
|
||||
assertLink(dto, "self", "https://scm-manager.org/scm/v2/repositoryTypes/hk");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAppendSelfLink() {
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertEquals(
|
||||
"https://scm-manager.org/scm/v2/repositoryTypes/hk",
|
||||
dto.getLinks().getLinkBy("self").get().getHref()
|
||||
private void assertLink(RepositoryTypeDto dto, String rel, String href) {
|
||||
assertThat(dto.getLinks().getLinkBy(rel)).hasValueSatisfying(
|
||||
link -> assertThat(link.getHref()).isEqualTo(href)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAppendImportFromUrlLink() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.PULL));
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertEquals(
|
||||
"https://scm-manager.org/scm/v2/repositories/import/hk/url",
|
||||
dto.getLinks().getLinkBy("import").get().getHref()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromUrlLinkIfCommandNotSupported() {
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromUrlLinkIfNotPermitted() {
|
||||
@SubjectAware(value = "trillian", permissions = "repository:create")
|
||||
void shouldAppendImportFromUrlLink() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.PULL));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
assertLink(dto, "import", "https://scm-manager.org/scm/v2/repositories/import/hk/url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAppendImportFromBundleLinkAndFullImportLink() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.UNBUNDLE));
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
@SubjectAware(value = "trillian", permissions = "repository:create")
|
||||
void shouldNotAppendImportFromUrlLinkIfCommandNotSupported() {
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertThat(dto.getLinks().getLinkBy("import")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAppendImportFromUrlLinkIfNotPermitted() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.PULL));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
List<Link> links = dto.getLinks().getLinksBy("import");
|
||||
assertEquals(2, links.size());
|
||||
assertEquals(
|
||||
"https://scm-manager.org/scm/v2/repositories/import/hk/bundle",
|
||||
links.get(0).getHref()
|
||||
);
|
||||
assertEquals(
|
||||
"https://scm-manager.org/scm/v2/repositories/import/hk/full",
|
||||
links.get(1).getHref()
|
||||
);
|
||||
assertThat(dto.getLinks().getLinkBy("import")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromBundleLinkOrFullImportLinkIfCommandNotSupported() {
|
||||
when(subject.isPermitted("repository:create")).thenReturn(true);
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotAppendImportFromBundleLinkIfNotPermitted() {
|
||||
@SubjectAware(value = "trillian", permissions = "repository:create")
|
||||
void shouldAppendImportFromBundleLinkAndFullImportLink() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.UNBUNDLE));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertFalse(dto.getLinks().getLinkBy("import").isPresent());
|
||||
|
||||
assertThat(dto.getLinks().getLinksBy("import"))
|
||||
.hasSize(2)
|
||||
.satisfies(link -> {
|
||||
assertThat(link.getHref()).isEqualTo("https://scm-manager.org/scm/v2/repositories/import/hk/bundle");
|
||||
}, Index.atIndex(0)
|
||||
)
|
||||
.satisfies(link -> {
|
||||
assertThat(link.getHref()).isEqualTo("https://scm-manager.org/scm/v2/repositories/import/hk/full");
|
||||
}, Index.atIndex(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(value = "trillian", permissions = "repository:create")
|
||||
void shouldNotAppendImportFromBundleLinkOrFullImportLinkIfCommandNotSupported() {
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertThat(dto.getLinks().getLinkBy("import")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAppendImportFromBundleLinkIfNotPermitted() {
|
||||
RepositoryType type = new RepositoryType("hk", "Hitchhiker", ImmutableSet.of(Command.UNBUNDLE));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertThat(dto.getLinks().getLinkBy("import")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichLinks() {
|
||||
when(registry.allByType(RepositoryType.class)).thenReturn(Collections.singleton(new LinkEnricher()));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertLink(dto, "spaceship", "/spaceships/heart-of-gold");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichEmbedded() {
|
||||
when(registry.allByType(RepositoryType.class)).thenReturn(Collections.singleton(new EmbeddedEnricher()));
|
||||
|
||||
RepositoryTypeDto dto = mapper.map(type);
|
||||
assertThat(dto.getEmbedded().getItemsBy("spaceship")).hasSize(1);
|
||||
}
|
||||
|
||||
private static class LinkEnricher implements HalEnricher {
|
||||
|
||||
@Override
|
||||
public void enrich(HalEnricherContext context, HalAppender appender) {
|
||||
appender.appendLink("spaceship", "/spaceships/heart-of-gold");
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmbeddedEnricher implements HalEnricher {
|
||||
|
||||
@Override
|
||||
public void enrich(HalEnricherContext context, HalAppender appender) {
|
||||
appender.appendEmbedded("spaceship", new HalRepresentation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user