From af9f6ab6299aca7b12890198eb6e080ac7ec036d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 30 Nov 2020 16:20:39 +0100 Subject: [PATCH] refactor UI --- scm-ui/ui-components/src/validation.ts | 6 + scm-ui/ui-types/src/index.ts | 2 +- scm-ui/ui-webapp/public/locales/de/repos.json | 7 + scm-ui/ui-webapp/public/locales/en/repos.json | 7 + .../repos/components/ImportFromUrlForm.tsx | 98 ++++++++++++ .../components/ImportRepositoryFromUrl.tsx | 109 +++++++++++++ .../src/repos/components/ImportTypeSelect.tsx | 11 +- .../components/NamespaceAndNameFields.tsx | 117 ++++++++++++++ .../components/RepositoryImportFromUrl.tsx | 93 ----------- .../components/RepositoryInformationForm.tsx | 70 ++++++++ .../repos/components/form/RepositoryForm.tsx | 149 +++++------------- .../src/repos/containers/AddRepository.tsx | 70 ++------ .../src/repos/containers/ImportRepository.tsx | 17 +- .../ui-webapp/src/repos/modules/repos.test.ts | 71 +-------- scm-ui/ui-webapp/src/repos/modules/repos.ts | 57 +------ .../resources/RepositoryImportResource.java | 12 +- .../resources/RepositoryRootResourceTest.java | 6 +- .../api/v2/import-repo-with-credentials.json | 2 +- .../sonia/scm/api/v2/import-repo.json | 2 +- 19 files changed, 503 insertions(+), 403 deletions(-) create mode 100644 scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx create mode 100644 scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx create mode 100644 scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx delete mode 100644 scm-ui/ui-webapp/src/repos/components/RepositoryImportFromUrl.tsx create mode 100644 scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx diff --git a/scm-ui/ui-components/src/validation.ts b/scm-ui/ui-components/src/validation.ts index 9e2b372dcb..e092e487e5 100644 --- a/scm-ui/ui-components/src/validation.ts +++ b/scm-ui/ui-components/src/validation.ts @@ -49,3 +49,9 @@ const pathRegex = /^((?!\/{2,}).)*$/; export const isPathValid = (path: string) => { return pathRegex.test(path); }; + +const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + +export const isUrlValid = (url: string) => { + return urlRegex.test(url); +}; diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index 35adda77bb..b2c5dd76a1 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -29,7 +29,7 @@ export { Me } from "./Me"; export { DisplayedUser, User } from "./User"; export { Group, Member } from "./Group"; -export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation, Namespace, NamespaceCollection } from "./Repositories"; +export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation, Namespace, NamespaceCollection, RepositoryUrlImport } from "./Repositories"; export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export { Branch, BranchRequest } from "./Branches"; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index c2eb4fc80d..bbd66aa541 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -12,6 +12,7 @@ "namespace-invalid": "Der Namespace des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig", "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein", + "url-invalid": "Die URL ist ungültig", "branch": { "nameInvalid": "Der Name des Branches ist ungültig" } @@ -68,6 +69,12 @@ "pending": { "subtitle": "Repository wird importiert...", "infoText": "Ihr Repository wird gerade importiert. Dies kann einen Moment dauern. Sie werden weitergeleitet, sobald der Import abgeschlossen ist. Wenn Sie diese Seite verlassen, können Sie nicht zurückkehren, um den Import-Status zu erfahren." + }, + "importTypes": { + "url": { + "label": "Import via URL", + "helpText": "Das Repository wird über eine URL importiert." + } } }, "branches": { diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index a7badcbdd4..9281d3f29c 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -12,6 +12,7 @@ "namespace-invalid": "The repository namespace is invalid", "name-invalid": "The repository name is invalid", "contact-invalid": "Contact must be a valid mail address", + "url-invalid": "The URL is invalid", "branch": { "nameInvalid": "The branch name is invalid" } @@ -69,6 +70,12 @@ "pending": { "subtitle": "Importing Repository...", "infoText": "Your repository is currently being imported. This may take a moment. You will be forwarded as soon as the import is finished. If you leave this page you cannot return to find out the import status." + }, + "importTypes": { + "url": { + "label": "Import via URL", + "helpText": "The Repository will be imported via the provided URL." + } } }, "branches": { diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx new file mode 100644 index 0000000000..49c6da14dc --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx @@ -0,0 +1,98 @@ +/* + * 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, useState } from "react"; +import { RepositoryUrlImport } from "@scm-manager/ui-types"; +import { InputField, validation } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; + +type Props = { + repository: RepositoryUrlImport; + onChange: (repository: RepositoryUrlImport) => void; + setValid: (valid: boolean) => void; +}; + +const Column = styled.div` + padding: 0 0.75rem; +`; + +const Columns = styled.div` + padding: 0.75rem 0 0; +`; + +const ImportFromUrlForm: FC = ({ repository, onChange, setValid }) => { + const [t] = useTranslation("repos"); + const [urlValidationError, setUrlValidationError] = useState(false); + + const handleImportUrlChange = (importUrl: string) => { + const changedRepo = { ...repository, importUrl }; + + if (!repository.name) { + // If the repository name is not fill we set a name suggestion + const match = importUrl.match(/([^\/]+?)(?:.git)?$/); + if (match && match[1]) { + changedRepo.name = match[1]; + } + } + onChange(changedRepo); + const valid = validation.isUrlValid(importUrl); + setUrlValidationError(!valid); + setValid(valid); + }; + + return ( + + + + + + onChange({ ...repository, username })} + value={repository.username} + helpText={t("help.usernameHelpText")} + /> + + + onChange({ ...repository, password })} + value={repository.password} + type="password" + helpText={t("help.passwordHelpText")} + /> + + + ); +}; + +export default ImportFromUrlForm; diff --git a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx new file mode 100644 index 0000000000..085d3c5d50 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx @@ -0,0 +1,109 @@ +/* + * 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, useState } from "react"; +import NamespaceAndNameFields from "./NamespaceAndNameFields"; +import { Repository, 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"; + +type Props = { + url: string; + setImportPending: (pending: boolean) => void; +}; + +const ImportRepositoryFromUrl: FC = ({ url, setImportPending }) => { + const [repo, setRepo] = useState({ + name: "", + namespace: "", + type: "", + contact: "", + description: "", + importUrl: "", + username: "", + password: "", + _links: {} + }); + + const [valid, setValid] = useState({ namespaceAndName: false, contact: true, importUrl: false }); + const isValid = () => { + return Object.values(valid).every(v => v); + }; + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const history = useHistory(); + const [t] = useTranslation("repos"); + + const handleImportLoading = (loading: boolean) => { + setLoading(loading); + setImportPending(loading); + }; + + const submit = () => { + handleImportLoading(true); + apiClient + .post(url, repo, "application/vnd.scmm-repository+json;v=2") + .then(response => { + const location = response.headers.get("Location"); + // @ts-ignore Location is always set if the repository import was successful + return apiClient.get(location); + }) + .then(response => response.json()) + .then(repo => history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`)) + .catch(error => { + setError(error); + handleImportLoading(false); + }); + }; + + return ( +
+ + setValid({ ...valid, importUrl })} + /> +
+ >} + setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })} + /> + >} + disabled={false} + setValid={(contact: boolean) => setValid({ ...valid, contact })} + /> + } + /> + + ); +}; + +export default ImportRepositoryFromUrl; diff --git a/scm-ui/ui-webapp/src/repos/components/ImportTypeSelect.tsx b/scm-ui/ui-webapp/src/repos/components/ImportTypeSelect.tsx index 85be1e5924..3151a08d53 100644 --- a/scm-ui/ui-webapp/src/repos/components/ImportTypeSelect.tsx +++ b/scm-ui/ui-webapp/src/repos/components/ImportTypeSelect.tsx @@ -24,6 +24,7 @@ import React, { FC } from "react"; import { RepositoryType, Link } from "@scm-manager/ui-types"; import { Radio } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; type Props = { repositoryType: RepositoryType; @@ -31,7 +32,8 @@ type Props = { setImportType: (type: string) => void; }; -const ImportTypeSelect: FC = ({repositoryType, importType, setImportType}) => { +const ImportTypeSelect: FC = ({ repositoryType, importType, setImportType }) => { + const [t] = useTranslation("repos"); const changeImportType = (checked: boolean, name?: string) => { if (name && checked) { @@ -39,16 +41,17 @@ const ImportTypeSelect: FC = ({repositoryType, importType, setImportType} } }; - //TODO Add helptext translation return ( <> - {(repositoryType._links.import as Link[]).map(type => ( + {(repositoryType._links.import as Link[]).map((type, index) => ( ))} diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx new file mode 100644 index 0000000000..3ad26512f9 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx @@ -0,0 +1,117 @@ +/* + * 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, useEffect, useState } from "react"; +import { Repository } from "@scm-manager/ui-types"; +import { CUSTOM_NAMESPACE_STRATEGY } from "../modules/repos"; +import { useTranslation } from "react-i18next"; +import { InputField } from "@scm-manager/ui-components"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import * as validator from "./form/repositoryValidation"; +import { getNamespaceStrategies } from "../../admin/modules/namespaceStrategies"; +import { connect } from "react-redux"; + +type Props = { + repository: Repository; + onChange: (repository: Repository) => void; + namespaceStrategy: string; + setValid: (valid: boolean) => void; +}; + +const NamespaceAndNameFields: FC = ({ repository, onChange, namespaceStrategy, setValid }) => { + const [t] = useTranslation("repos"); + const [namespaceValidationError, setNamespaceValidationError] = useState(false); + const [nameValidationError, setNameValidationError] = useState(false); + + useEffect(() => { + //TODO fix validation + if (repository.name) { + const valid = validator.isNameValid(repository.name); + setNameValidationError(!valid); + onFieldChange(); + } + }, [repository.name]); + + const handleNamespaceChange = (namespace: string) => { + const valid = validator.isNamespaceValid(namespace); + setNamespaceValidationError(!valid); + onFieldChange(valid); + onChange({ ...repository, namespace }); + }; + + const handleNameChange = (name: string) => { + const valid = validator.isNameValid(name); + setNameValidationError(!valid); + onFieldChange(); + onChange({ ...repository, name }); + }; + + //TODO fix validation + const onFieldChange = (namespaceValid?: boolean) => { + if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && !validator.isNamespaceValid(repository.namespace)) { + setValid(false); + } else { + setValid(!nameValidationError && !namespaceValidationError); + } + }; + + const renderNamespaceField = () => { + const props = { + label: t("repository.namespace"), + helpText: t("help.namespaceHelpText"), + value: repository ? repository.namespace : "", + onChange: handleNamespaceChange, + errorMessage: t("validation.namespace-invalid"), + validationError: namespaceValidationError + }; + + if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) { + return ; + } + + return ; + }; + + return ( + <> + {renderNamespaceField()} + + + ); +}; + +const mapStateToProps = (state: any) => { + const namespaceStrategy = getNamespaceStrategies(state).current; + return { + namespaceStrategy + }; +}; + +export default connect(mapStateToProps)(NamespaceAndNameFields); diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryImportFromUrl.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryImportFromUrl.tsx deleted file mode 100644 index 68517e21fa..0000000000 --- a/scm-ui/ui-webapp/src/repos/components/RepositoryImportFromUrl.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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, useState } from "react"; -import { InputField } from "@scm-manager/ui-components"; -import styled from "styled-components"; -import { useTranslation } from "react-i18next"; - -type Props = { - url: string; -}; - -const Column = styled.div` - padding: 0 0.75rem; -`; - -const Columns = styled.div` - padding: 0.75rem 0 0; -`; - -const RepositoryImportFromUrl: FC = ({}) => { - const [name, setName] = useState(""); - const [importUrl, setImportUrl] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [t] = useTranslation("repos"); - - const handleImportUrlChange = (url: string) => { - if (!name) { - // If the repository name is not fill we set a name suggestion - const match = url.match(/([^\/]+)(\.git)?/i); - if (match && match[1]) { - setName(match[1]); - } - } - setImportUrl(url); - }; - - return ( - <> - - - - - - - - - - - -
- - ); -}; - -export default RepositoryImportFromUrl; diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx new file mode 100644 index 0000000000..7de1c9201a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/RepositoryInformationForm.tsx @@ -0,0 +1,70 @@ +/* + * 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, useState } from "react"; +import { InputField, Textarea } from "@scm-manager/ui-components"; +import { Repository } 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; + setValid: (valid: boolean) => void; +}; + +const RepositoryInformationForm: FC = ({ repository, onChange, disabled, setValid }) => { + const [t] = useTranslation("repos"); + const [contactValidationError, setContactValidationError] = useState(false); + + const handleContactChange = (contact: string) => { + const valid = validator.isContactValid(contact); + setContactValidationError(!valid); + setValid(valid); + onChange({ ...repository, contact }); + }; + + return ( + <> + +