fix form validation after refactoring

This commit is contained in:
Eduard Heimbuch
2020-12-01 11:35:49 +01:00
parent af9f6ab629
commit 6a93a198e8
12 changed files with 134 additions and 66 deletions

View File

@@ -61,6 +61,7 @@ const MarginLeft = styled.div`
const FlexContainer = styled.div` const FlexContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 2.25rem;
`; `;
export default class Page extends React.Component<Props> { export default class Page extends React.Component<Props> {

View File

@@ -71,6 +71,7 @@
"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." "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": { "importTypes": {
"label": "Import Modus",
"url": { "url": {
"label": "Import via URL", "label": "Import via URL",
"helpText": "Das Repository wird über eine URL importiert." "helpText": "Das Repository wird über eine URL importiert."

View File

@@ -72,6 +72,7 @@
"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." "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": { "importTypes": {
"label": "Import Mode",
"url": { "url": {
"label": "Import via URL", "label": "Import via URL",
"helpText": "The Repository will be imported via the provided URL." "helpText": "The Repository will be imported via the provided URL."

View File

@@ -32,6 +32,7 @@ type Props = {
repository: RepositoryUrlImport; repository: RepositoryUrlImport;
onChange: (repository: RepositoryUrlImport) => void; onChange: (repository: RepositoryUrlImport) => void;
setValid: (valid: boolean) => void; setValid: (valid: boolean) => void;
disabled?: boolean;
}; };
const Column = styled.div` const Column = styled.div`
@@ -42,7 +43,7 @@ const Columns = styled.div`
padding: 0.75rem 0 0; padding: 0.75rem 0 0;
`; `;
const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid }) => { const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled }) => {
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
const [urlValidationError, setUrlValidationError] = useState(false); const [urlValidationError, setUrlValidationError] = useState(false);
@@ -72,6 +73,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid }) => {
helpText={t("help.importUrlHelpText")} helpText={t("help.importUrlHelpText")}
validationError={urlValidationError} validationError={urlValidationError}
errorMessage={t("validation.url-invalid")} errorMessage={t("validation.url-invalid")}
disabled={disabled}
/> />
</Column> </Column>
<Column className="column is-half"> <Column className="column is-half">
@@ -80,6 +82,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid }) => {
onChange={username => onChange({ ...repository, username })} onChange={username => onChange({ ...repository, username })}
value={repository.username} value={repository.username}
helpText={t("help.usernameHelpText")} helpText={t("help.usernameHelpText")}
disabled={disabled}
/> />
</Column> </Column>
<Column className="column is-half"> <Column className="column is-half">
@@ -89,6 +92,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid }) => {
value={repository.password} value={repository.password}
type="password" type="password"
helpText={t("help.passwordHelpText")} helpText={t("help.passwordHelpText")}
disabled={disabled}
/> />
</Column> </Column>
</Columns> </Columns>

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC, useState } from "react"; import React, { FC, FormEvent, useState } from "react";
import NamespaceAndNameFields from "./NamespaceAndNameFields"; import NamespaceAndNameFields from "./NamespaceAndNameFields";
import { Repository, RepositoryUrlImport } from "@scm-manager/ui-types"; import { Repository, RepositoryUrlImport } from "@scm-manager/ui-types";
import ImportFromUrlForm from "./ImportFromUrlForm"; import ImportFromUrlForm from "./ImportFromUrlForm";
@@ -62,7 +62,9 @@ const ImportRepositoryFromUrl: FC<Props> = ({ url, setImportPending }) => {
setImportPending(loading); setImportPending(loading);
}; };
const submit = () => { const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const currentPath = history.location.pathname;
handleImportLoading(true); handleImportLoading(true);
apiClient apiClient
.post(url, repo, "application/vnd.scmm-repository+json;v=2") .post(url, repo, "application/vnd.scmm-repository+json;v=2")
@@ -72,7 +74,11 @@ const ImportRepositoryFromUrl: FC<Props> = ({ url, setImportPending }) => {
return apiClient.get(location); return apiClient.get(location);
}) })
.then(response => response.json()) .then(response => response.json())
.then(repo => history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`)) .then(repo => {
if (history.location.pathname === currentPath) {
history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
}
})
.catch(error => { .catch(error => {
setError(error); setError(error);
handleImportLoading(false); handleImportLoading(false);
@@ -81,22 +87,24 @@ const ImportRepositoryFromUrl: FC<Props> = ({ url, setImportPending }) => {
return ( return (
<form onSubmit={submit}> <form onSubmit={submit}>
<ErrorNotification error={error} />
<ImportFromUrlForm <ImportFromUrlForm
repository={repo} repository={repo}
onChange={setRepo} onChange={setRepo}
setValid={(importUrl: boolean) => setValid({ ...valid, importUrl })} setValid={(importUrl: boolean) => setValid({ ...valid, importUrl })}
disabled={loading}
/> />
<ErrorNotification error={error} />
<hr /> <hr />
<NamespaceAndNameFields <NamespaceAndNameFields
repository={repo} repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>} onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })} setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
disabled={loading}
/> />
<RepositoryInformationForm <RepositoryInformationForm
repository={repo} repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>} onChange={setRepo as React.Dispatch<React.SetStateAction<Repository>>}
disabled={false} disabled={loading}
setValid={(contact: boolean) => setValid({ ...valid, contact })} setValid={(contact: boolean) => setValid({ ...valid, contact })}
/> />
<Level <Level

View File

@@ -30,18 +30,21 @@ type Props = {
repositoryTypes: RepositoryType[]; repositoryTypes: RepositoryType[];
repositoryType?: RepositoryType; repositoryType?: RepositoryType;
setRepositoryType: (repositoryType: RepositoryType) => void; setRepositoryType: (repositoryType: RepositoryType) => void;
disabled?: boolean;
}; };
const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType, setRepositoryType }) => { const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType, setRepositoryType, disabled }) => {
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
const createSelectOptions = () => { const createSelectOptions = () => {
const options = repositoryTypes.filter(repoType => !!repoType._links.import).map(repositoryType => { const options = repositoryTypes
return { .filter(repoType => !!repoType._links.import)
label: repositoryType.displayName, .map(repositoryType => {
value: repositoryType.name return {
}; label: repositoryType.displayName,
}); value: repositoryType.name
};
});
options.unshift({ label: "", value: "" }); options.unshift({ label: "", value: "" });
return options; return options;
}; };
@@ -52,13 +55,14 @@ const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType
}; };
return ( return (
<Select <Select
label={t("repository.type")} label={t("repository.type")}
onChange={onChangeType} onChange={onChangeType}
value={repositoryType ? repositoryType.name : ""} value={repositoryType ? repositoryType.name : ""}
options={createSelectOptions()} options={createSelectOptions()}
helpText={t("help.typeHelpText")} helpText={t("help.typeHelpText")}
/> disabled={disabled}
/>
); );
}; };

View File

@@ -22,17 +22,18 @@
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC } from "react"; import React, { FC } from "react";
import { RepositoryType, Link } from "@scm-manager/ui-types"; import { Link, RepositoryType } from "@scm-manager/ui-types";
import { Radio } from "@scm-manager/ui-components"; import { LabelWithHelpIcon, Radio } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type Props = { type Props = {
repositoryType: RepositoryType; repositoryType: RepositoryType;
importType: string; importType: string;
setImportType: (type: string) => void; setImportType: (type: string) => void;
disabled?: boolean;
}; };
const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType }) => { const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType, disabled }) => {
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
const changeImportType = (checked: boolean, name?: string) => { const changeImportType = (checked: boolean, name?: string) => {
@@ -43,6 +44,7 @@ const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType
return ( return (
<> <>
<LabelWithHelpIcon label={t("import.importTypes.label")} key="import.importTypes.label" />
{(repositoryType._links.import as Link[]).map((type, index) => ( {(repositoryType._links.import as Link[]).map((type, index) => (
<Radio <Radio
name={type.name} name={type.name}
@@ -52,6 +54,7 @@ const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType
helpText={t(`import.importTypes.${type.name}.helpText`)} helpText={t(`import.importTypes.${type.name}.helpText`)}
onChange={changeImportType} onChange={changeImportType}
key={index} key={index}
disabled={disabled}
/> />
))} ))}
</> </>

View File

@@ -36,45 +36,45 @@ type Props = {
onChange: (repository: Repository) => void; onChange: (repository: Repository) => void;
namespaceStrategy: string; namespaceStrategy: string;
setValid: (valid: boolean) => void; setValid: (valid: boolean) => void;
disabled?: boolean;
}; };
const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, namespaceStrategy, setValid }) => { const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, namespaceStrategy, setValid, disabled }) => {
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
const [namespaceValidationError, setNamespaceValidationError] = useState(false); const [namespaceValidationError, setNamespaceValidationError] = useState(false);
const [nameValidationError, setNameValidationError] = useState(false); const [nameValidationError, setNameValidationError] = useState(false);
useEffect(() => { useEffect(() => {
//TODO fix validation
if (repository.name) { if (repository.name) {
const valid = validator.isNameValid(repository.name); const nameValid = validator.isNameValid(repository.name);
setNameValidationError(!valid); setNameValidationError(!nameValid);
onFieldChange(); if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
if (repository.namespace) {
const namespaceValid = validator.isNamespaceValid(repository.namespace);
setValid(!(!namespaceValid || !nameValid));
} else {
setValid(false);
}
} else {
setValid(nameValid);
}
} else {
setValid(false);
} }
}, [repository.name]); }, [repository.name, repository.namespace]);
const handleNamespaceChange = (namespace: string) => { const handleNamespaceChange = (namespace: string) => {
const valid = validator.isNamespaceValid(namespace); const valid = validator.isNamespaceValid(namespace);
setNamespaceValidationError(!valid); setNamespaceValidationError(!valid);
onFieldChange(valid);
onChange({ ...repository, namespace }); onChange({ ...repository, namespace });
}; };
const handleNameChange = (name: string) => { const handleNameChange = (name: string) => {
const valid = validator.isNameValid(name); const valid = validator.isNameValid(name);
setNameValidationError(!valid); setNameValidationError(!valid);
onFieldChange();
onChange({ ...repository, name }); 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 renderNamespaceField = () => {
const props = { const props = {
label: t("repository.namespace"), label: t("repository.namespace"),
@@ -82,7 +82,8 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, namespaceStra
value: repository ? repository.namespace : "", value: repository ? repository.namespace : "",
onChange: handleNamespaceChange, onChange: handleNamespaceChange,
errorMessage: t("validation.namespace-invalid"), errorMessage: t("validation.namespace-invalid"),
validationError: namespaceValidationError validationError: namespaceValidationError,
disabled: disabled
}; };
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) { if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
@@ -102,6 +103,7 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, namespaceStra
validationError={nameValidationError} validationError={nameValidationError}
errorMessage={t("validation.name-invalid")} errorMessage={t("validation.name-invalid")}
helpText={t("help.nameHelpText")} helpText={t("help.nameHelpText")}
disabled={disabled}
/> />
</> </>
); );

View File

@@ -78,7 +78,6 @@ const RepositoryForm: FC<Props> = ({
}); });
const [initRepository, setInitRepository] = useState(false); const [initRepository, setInitRepository] = useState(false);
const [contextEntries, setContextEntries] = useState({}); const [contextEntries, setContextEntries] = useState({});
//TODO fix validation
const [valid, setValid] = useState({ namespaceAndName: false, contact: true }); const [valid, setValid] = useState({ namespaceAndName: false, contact: true });
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
@@ -194,7 +193,7 @@ const RepositoryForm: FC<Props> = ({
<RepositoryInformationForm <RepositoryInformationForm
repository={repo} repository={repo}
onChange={setRepo} onChange={setRepo}
disabled={!createRepository} disabled={!(isModifiable() || createRepository)}
setValid={contact => setValid({ ...valid, contact })} setValid={contact => setValid({ ...valid, contact })}
/> />
{submitButton()} {submitButton()}

View File

@@ -25,12 +25,16 @@
import React, { FC } from "react"; import React, { FC } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, ButtonAddons, Level } from "@scm-manager/ui-components"; import { Button, ButtonAddons, Icon, Level } from "@scm-manager/ui-components";
type Props = { type Props = {
creationMode: "CREATE" | "IMPORT"; creationMode: "CREATE" | "IMPORT";
}; };
const MarginIcon = styled(Icon)`
padding-right: 0.5rem;
`;
const SmallButton = styled(Button)` const SmallButton = styled(Button)`
border-radius: 4px; border-radius: 4px;
font-size: 1rem; font-size: 1rem;
@@ -38,9 +42,14 @@ const SmallButton = styled(Button)`
`; `;
const TopLevel = styled(Level)` const TopLevel = styled(Level)`
margin-top: -2.75rem; margin-top: 1.5rem;
margin-bottom: 2.75rem !important; //TODO Try to remove important margin-bottom: -1.5rem !important;
height: 0; height: 0;
position: absolute;
right: 0;
@media (max-width: 785px) {
margin-top: 4.5rem;
}
`; `;
const RepositoryFormSwitcher: FC<Props> = ({ creationMode }) => { const RepositoryFormSwitcher: FC<Props> = ({ creationMode }) => {
@@ -59,17 +68,20 @@ const RepositoryFormSwitcher: FC<Props> = ({ creationMode }) => {
right={ right={
<ButtonAddons> <ButtonAddons>
<SmallButton <SmallButton
label={t("repositoryForm.createButton")}
icon="fa fa-plus"
color={isCreateMode() ? "link is-selected" : undefined} color={isCreateMode() ? "link is-selected" : undefined}
link={isImportMode() ? "/repos/create" : 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 <SmallButton
label={t("repositoryForm.importButton")}
icon="fa fa-file-upload"
color={isImportMode() ? "link is-selected" : undefined} color={isImportMode() ? "link is-selected" : undefined}
link={isCreateMode() ? "/repos/import" : 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> </ButtonAddons>
} }
/> />

View File

@@ -35,12 +35,7 @@ import {
} from "../modules/repositoryTypes"; } from "../modules/repositoryTypes";
import RepositoryForm from "../components/form"; import RepositoryForm from "../components/form";
import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher"; import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher";
import { import { createRepo, createRepoReset, getCreateRepoFailure, isCreateRepoPending } from "../modules/repos";
createRepo,
createRepoReset,
getCreateRepoFailure,
isCreateRepoPending
} from "../modules/repos";
import { getRepositoriesLink } from "../../modules/indexResource"; import { getRepositoriesLink } from "../../modules/indexResource";
import { import {
fetchNamespaceStrategiesIfNeeded, fetchNamespaceStrategiesIfNeeded,
@@ -77,7 +72,19 @@ type Props = WithTranslation &
history: History; history: History;
}; };
class AddRepository extends React.Component<Props> { type State = {
importPending: boolean;
};
class AddRepository extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
importPending: false
};
}
componentDidMount() { componentDidMount() {
this.props.resetForm(); this.props.resetForm();
this.props.fetchRepositoryTypesIfNeeded(); this.props.fetchRepositoryTypesIfNeeded();
@@ -102,6 +109,16 @@ class AddRepository extends React.Component<Props> {
isImportPage = () => this.resolveLocation() === "import"; isImportPage = () => this.resolveLocation() === "import";
isCreatePage = () => this.resolveLocation() === "create"; isCreatePage = () => this.resolveLocation() === "create";
getSubtitle = () => {
if (this.isCreatePage()) {
return "create.subtitle";
} else if (this.isImportPage() && this.state.importPending) {
return "import.pending.subtitle";
} else {
return "import.subtitle";
}
};
render() { render() {
const { const {
pageLoading, pageLoading,
@@ -118,13 +135,18 @@ class AddRepository extends React.Component<Props> {
return ( return (
<Page <Page
title={t("create.title")} title={t("create.title")}
subtitle={t("create.subtitle")} subtitle={t(this.getSubtitle())}
afterTitle={<RepositoryFormSwitcher creationMode={this.isImportPage() ? "IMPORT" : "CREATE"} />}
loading={pageLoading} loading={pageLoading}
error={error} error={error}
showContentOnError={true} showContentOnError={true}
> >
{!error && <RepositoryFormSwitcher creationMode={this.isImportPage() ? "IMPORT" : "CREATE"} />} {this.isImportPage() && (
{this.isImportPage() && <ImportRepository repositoryTypes={repositoryTypes} />} <ImportRepository
repositoryTypes={repositoryTypes}
setPending={(importPending: boolean) => this.setState({ importPending })}
/>
)}
{this.isCreatePage() && ( {this.isCreatePage() && (
<RepositoryForm <RepositoryForm
repositoryTypes={repositoryTypes} repositoryTypes={repositoryTypes}

View File

@@ -22,8 +22,8 @@
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { RepositoryType, Link } from "@scm-manager/ui-types"; import { Link, RepositoryType } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ImportRepositoryTypeSelect from "../components/ImportRepositoryTypeSelect"; import ImportRepositoryTypeSelect from "../components/ImportRepositoryTypeSelect";
@@ -33,14 +33,19 @@ import { Loading, Notification } from "@scm-manager/ui-components";
type Props = { type Props = {
repositoryTypes: RepositoryType[]; repositoryTypes: RepositoryType[];
setPending: (pending: boolean) => void;
}; };
const ImportRepository: FC<Props> = ({ repositoryTypes }) => { const ImportRepository: FC<Props> = ({ repositoryTypes, setPending }) => {
const [importPending, setImportPending] = useState(false); const [importPending, setImportPending] = useState(false);
const [repositoryType, setRepositoryType] = useState<RepositoryType | undefined>(); const [repositoryType, setRepositoryType] = useState<RepositoryType | undefined>();
const [importType, setImportType] = useState(""); const [importType, setImportType] = useState("");
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
useEffect(() => {
setPending(importPending);
}, [importPending]);
const changeRepositoryType = (repositoryType: RepositoryType) => { const changeRepositoryType = (repositoryType: RepositoryType) => {
setRepositoryType(repositoryType); setRepositoryType(repositoryType);
setImportType(repositoryType?._links ? ((repositoryType!._links?.import as Link[])[0] as Link).name! : ""); setImportType(repositoryType?._links ? ((repositoryType!._links?.import as Link[])[0] as Link).name! : "");
@@ -72,11 +77,17 @@ const ImportRepository: FC<Props> = ({ repositoryTypes }) => {
repositoryTypes={repositoryTypes} repositoryTypes={repositoryTypes}
repositoryType={repositoryType} repositoryType={repositoryType}
setRepositoryType={changeRepositoryType} setRepositoryType={changeRepositoryType}
disabled={importPending}
/> />
{repositoryType && ( {repositoryType && (
<> <>
<hr /> <hr />
<ImportTypeSelect repositoryType={repositoryType} importType={importType} setImportType={setImportType} /> <ImportTypeSelect
repositoryType={repositoryType}
importType={importType}
setImportType={setImportType}
disabled={importPending}
/>
<hr /> <hr />
</> </>
)} )}