diff --git a/gradle/changelog/extension_point_repository_creator.yaml b/gradle/changelog/extension_point_repository_creator.yaml
new file mode 100644
index 0000000000..ce478ef35e
--- /dev/null
+++ b/gradle/changelog/extension_point_repository_creator.yaml
@@ -0,0 +1,2 @@
+- type: added
+ description: Extension Point for repository creators ([#1657](https://github.com/scm-manager/scm-manager/pull/1657))
diff --git a/scm-ui/ui-extensions/package.json b/scm-ui/ui-extensions/package.json
index 2b99a2835a..957f9bdc52 100644
--- a/scm-ui/ui-extensions/package.json
+++ b/scm-ui/ui-extensions/package.json
@@ -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",
diff --git a/scm-ui/ui-extensions/src/binder.ts b/scm-ui/ui-extensions/src/binder.ts
index 978de8f633..ef80db9746 100644
--- a/scm-ui/ui-extensions/src/binder.ts
+++ b/scm-ui/ui-extensions/src/binder.ts
@@ -30,7 +30,7 @@ type ExtensionRegistration
= {
extensionName: string;
};
-export type ExtensionPointDefinition = {
+export type ExtensionPointDefinition = {
name: N;
type: T;
props: P;
diff --git a/scm-ui/ui-extensions/src/extensionPoints.ts b/scm-ui/ui-extensions/src/extensionPoints.ts
new file mode 100644
index 0000000000..2eb9cafe4b
--- /dev/null
+++ b/scm-ui/ui-extensions/src/extensionPoints.ts
@@ -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;
+ informationForm: React.ComponentType;
+};
+
+export type RepositoryCreatorExtension = {
+ subtitle: string;
+ path: string;
+ icon: string;
+ label: string;
+ component: React.ComponentType;
+};
+
+export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
diff --git a/scm-ui/ui-extensions/src/index.ts b/scm-ui/ui-extensions/src/index.ts
index e6f7646601..746a089f6e 100644
--- a/scm-ui/ui-extensions/src/index.ts
+++ b/scm-ui/ui-extensions/src/index.ts
@@ -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";
diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts
index 2985350977..4f5fab167c 100644
--- a/scm-ui/ui-types/src/Repositories.ts
+++ b/scm-ui/ui-types/src/Repositories.ts
@@ -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;
diff --git a/scm-ui/ui-webapp/src/containers/Main.tsx b/scm-ui/ui-webapp/src/containers/Main.tsx
index 3cffe27d01..f8bf785922 100644
--- a/scm-ui/ui-webapp/src/containers/Main.tsx
+++ b/scm-ui/ui-webapp/src/containers/Main.tsx
@@ -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 {
-
-
+
+
diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx
index fe55407fef..6a6123b6b1 100644
--- a/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/ImportFullRepository.tsx
@@ -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;
+ informationForm: React.ComponentType;
};
-const ImportFullRepository: FC = ({ url, repositoryType, setImportPending }) => {
- const [repo, setRepo] = useState({
+const ImportFullRepository: FC = ({
+ url,
+ repositoryType,
+ setImportPending,
+ nameForm: NameForm,
+ informationForm: InformationForm
+}) => {
+ const [repo, setRepo] = useState({
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 = ({ url, repositoryType, setImportPending
setValid={(file: boolean) => setValid({ ...valid, file })}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
disabled={loading}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
disabled={loading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
/>
diff --git a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromBundle.tsx b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromBundle.tsx
index 0f449605d6..341eac8827 100644
--- a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromBundle.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromBundle.tsx
@@ -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;
+ informationForm: React.ComponentType;
};
-const ImportRepositoryFromBundle: FC = ({ url, repositoryType, setImportPending }) => {
- const [repo, setRepo] = useState({
+const ImportRepositoryFromBundle: FC = ({
+ url,
+ repositoryType,
+ setImportPending,
+ nameForm: NameForm,
+ informationForm: InformationForm
+}) => {
+ const [repo, setRepo] = useState({
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 = ({ url, repositoryType, setImportP
disabled={loading}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
disabled={loading}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
disabled={loading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
/>
diff --git a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx
index 30dade297e..3b9a485701 100644
--- a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx
@@ -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;
+ informationForm: React.ComponentType;
};
-const ImportRepositoryFromUrl: FC = ({ url, repositoryType, setImportPending }) => {
+const ImportRepositoryFromUrl: FC = ({
+ url,
+ repositoryType,
+ setImportPending,
+ nameForm: NameForm,
+ informationForm: InformationForm
+}) => {
const [repo, setRepo] = useState({
name: "",
namespace: "",
@@ -46,7 +53,7 @@ const ImportRepositoryFromUrl: FC = ({ 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 = ({ url, repositoryType, setImportPend
disabled={loading}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
disabled={loading}
/>
- >}
+ onChange={setRepo as React.Dispatch>}
disabled={loading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
/>
diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx
index 85f2d93848..51ec213280 100644
--- a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx
@@ -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 = ({ 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"),
diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx
index 7de1c9201a..32f40ce766 100644
--- a/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx
@@ -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 = ({ repository, onChange, disabled, setValid }) => {
diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryFormSwitcher.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryFormSwitcher.tsx
index 81b8377935..4edff0992c 100644
--- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryFormSwitcher.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryFormSwitcher.tsx
@@ -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 = ({ 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 = ({ path, icon, label }) => {
+ const location = useLocation();
+ const href = urls.concat("/repos/create", path);
+ const isSelected = href === location.pathname;
return (
-
-
-
- {t("repositoryForm.createButton")}
-
-
-
- {t("repositoryForm.importButton")}
-
-
- }
- />
+
+
+ {label}
+
);
};
+type Props = {
+ forms: RepositoryForm[];
+};
+
+const RepositoryFormSwitcher: FC = ({ forms }) => (
+
+ {(forms || []).map(form => (
+
+ ))}
+
+ }
+ />
+);
+
export default RepositoryFormSwitcher;
diff --git a/scm-ui/ui-webapp/src/repos/containers/CreateRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/CreateRepository.tsx
index 270870dea4..ee9b90c231 100644
--- a/scm-ui/ui-webapp/src/repos/containers/CreateRepository.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/CreateRepository.tsx
@@ -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 = ({ repositoryTypes, namespaceStrategies, index }) => {
const { isLoading, error, repository, create } = useCreateRepository();
- const [t] = useTranslation("repos");
if (repository) {
return ;
}
return (
- }
- loading={isPageLoading}
- error={pageLoadingError || error || undefined}
- showContentOnError={true}
- >
- {namespaceStrategies && repositoryTypes ? (
-
- ) : null}
-
+ <>
+
+
+ >
);
};
diff --git a/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx
new file mode 100644
index 0000000000..641409bd96
--- /dev/null
+++ b/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx
@@ -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 = ({ creator, creators }) => {
+ const { isPageLoading, pageLoadingError, namespaceStrategies, repositoryTypes, index } = useCreateRepositoryData();
+ const [t] = useTranslation(["repos", "plugins"]);
+
+ const Component = creator.component;
+
+ return (
+ }
+ loading={isPageLoading}
+ error={pageLoadingError}
+ >
+ {namespaceStrategies && repositoryTypes && index ? (
+
+ ) : null}
+
+ );
+};
+
+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("repos.creator");
+ if (extCreators) {
+ creators.push(...extCreators);
+ }
+
+ return (
+
+ {creators.map(creator => (
+
+
+
+ ))}
+
+ );
+};
+
+export default CreateRepositoryRoot;
diff --git a/scm-ui/ui-webapp/src/repos/containers/ImportRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/ImportRepository.tsx
index 07041ec76a..844311d86d 100644
--- a/scm-ui/ui-webapp/src/repos/containers/ImportRepository.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/ImportRepository.tsx
@@ -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 = ({ repositoryTypes, nameForm, informationForm }) => {
const [importPending, setImportPending] = useState(false);
const [repositoryType, setRepositoryType] = useState();
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 (
- }
- loading={isLoading}
- error={error || undefined}
- showContentOnError={true}
- >
+ <>
{
>
)}
{importType && renderImportComponent()}
-
+ >
);
};
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeDto.java
index f28cdeb948..3fbd5c41ae 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeDto.java
@@ -21,26 +21,28 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
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);
}
}
+
+
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapper.java
index cc8e9d8be9..6b34efe051 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapper.java
@@ -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 {
+ 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());
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapperTest.java
index fd528a2e8a..3997895536 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapperTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeToRepositoryTypeDtoMapperTest.java
@@ -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 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());
+ }
}
}