implemented repository create form

This commit is contained in:
Sebastian Sdorra
2018-08-02 16:09:58 +02:00
parent e675bcc0fd
commit 9b62d19df5
23 changed files with 838 additions and 129 deletions

View File

@@ -1,10 +1,28 @@
{ {
"repository": {
"name": "Name",
"type": "Type",
"contact": "Contact",
"description": "Description"
},
"validation": {
"name-invalid": "The repository name is invalid",
"contact-invalid": "Contact must be a valid mail address"
},
"overview": { "overview": {
"title": "Repositories", "title": "Repositories",
"subtitle": "Overview of available repositories" "subtitle": "Overview of available repositories",
"create-button": "Create"
}, },
"repository-root": { "repository-root": {
"error-title": "Error", "error-title": "Error",
"error-subtitle": "Unknown repository error" "error-subtitle": "Unknown repository error"
},
"create": {
"title": "Create Repository",
"subtitle": "Create a new repository"
},
"repository-form": {
"submit": "Speichern"
} }
} }

View File

@@ -1,11 +0,0 @@
//@flow
import React from "react";
import Button, { type ButtonProps } from "./Button";
class AddButton extends React.Component<ButtonProps> {
render() {
return <Button type="default" {...this.props} />;
}
}
export default AddButton;

View File

@@ -10,7 +10,8 @@ export type ButtonProps = {
action?: () => void, action?: () => void,
link?: string, link?: string,
fullWidth?: boolean, fullWidth?: boolean,
className?: string className?: string,
classes: any
}; };
type Props = ButtonProps & { type Props = ButtonProps & {

View File

@@ -0,0 +1,24 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import Button, { type ButtonProps } from "./Button";
import classNames from "classnames";
const styles = {
spacing: {
margin: "1em 0 0 1em"
}
};
class CreateButton extends React.Component<ButtonProps> {
render() {
const { classes } = this.props;
return (
<div className={classNames("is-pulled-right", classes.spacing)}>
<Button type="default" {...this.props} />
</div>
);
}
}
export default injectSheet(styles)(CreateButton);

View File

@@ -1,4 +1,4 @@
export { default as AddButton } from "./AddButton"; export { default as CreateButton } from "./CreateButton";
export { default as Button } from "./Button"; export { default as Button } from "./Button";
export { default as DeleteButton } from "./DeleteButton"; export { default as DeleteButton } from "./DeleteButton";
export { default as EditButton } from "./EditButton"; export { default as EditButton } from "./EditButton";

View File

@@ -0,0 +1,59 @@
//@flow
import React from "react";
export type SelectItem = {
value: string,
label: string
};
type Props = {
label?: string,
options: SelectItem[],
value?: SelectItem,
onChange: string => void
};
class Select extends React.Component<Props> {
field: ?HTMLSelectElement;
handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { options, value } = this.props;
return (
<div className="field">
{this.renderLabel()}
<div className="control select">
<select
ref={input => {
this.field = input;
}}
value={value}
onChange={this.handleInput}
>
{options.map(opt => {
return (
<option value={opt.value} key={opt.value}>
{opt.label}
</option>
);
})}
</select>
</div>
</div>
);
}
}
export default Select;

View File

@@ -1,2 +1,3 @@
export { default as Checkbox } from "./Checkbox"; export { default as Checkbox } from "./Checkbox";
export { default as InputField } from "./InputField"; export { default as InputField } from "./InputField";
export { default as Select } from "./Select";

View File

@@ -0,0 +1,12 @@
// @flow
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
export const isNameValid = (name: string) => {
return nameRegex.test(name);
};
const mailRegex = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-.]*\.[A-z0-9][A-z0-9-]+$/;
export const isMailValid = (mail: string) => {
return mailRegex.test(mail);
};

View File

@@ -0,0 +1,75 @@
import * as validator from "./validation";
describe("test name validation", () => {
it("should return false", () => {
// invalid names taken from ValidationUtilTest.java
const invalidNames = [
" test 123",
" test 123 ",
"test 123 ",
"test/123",
"test%123",
"test:123",
"t ",
" t",
" t ",
""
];
for (let name of invalidNames) {
expect(validator.isNameValid(name)).toBe(false);
}
});
it("should return true", () => {
// valid names taken from ValidationUtilTest.java
const validNames = [
"test",
"test.git",
"Test123.git",
"Test123-git",
"Test_user-123.git",
"test@scm-manager.de",
"test 123",
"tt",
"t"
];
for (let name of validNames) {
expect(validator.isNameValid(name)).toBe(true);
}
});
});
describe("test mail validation", () => {
it("should return false", () => {
// invalid taken from ValidationUtilTest.java
const invalid = [
"ostfalia.de",
"@ostfalia.de",
"s.sdorra@",
"s.sdorra@ostfalia",
"s.sdorra@@ostfalia.de",
"s.sdorra@ ostfalia.de",
"s.sdorra @ostfalia.de"
];
for (let mail of invalid) {
expect(validator.isMailValid(mail)).toBe(false);
}
});
it("should return true", () => {
// valid taken from ValidationUtilTest.java
const valid = [
"s.sdorra@ostfalia.de",
"sdorra@ostfalia.de",
"s.sdorra@hbk-bs.de",
"s.sdorra@gmail.com",
"s.sdorra@t.co",
"s.sdorra@ucla.college",
"s.sdorra@example.xn--p1ai",
"s.sdorra@scm.solutions"
];
for (let mail of valid) {
expect(validator.isMailValid(mail)).toBe(true);
}
});
});

View File

@@ -12,7 +12,8 @@ import { Switch } from "react-router-dom";
import ProtectedRoute from "../components/ProtectedRoute"; import ProtectedRoute from "../components/ProtectedRoute";
import AddUser from "../users/containers/AddUser"; import AddUser from "../users/containers/AddUser";
import SingleUser from "../users/containers/SingleUser"; import SingleUser from "../users/containers/SingleUser";
import RepositoryRoot from '../repos/containers/RepositoryRoot'; import RepositoryRoot from "../repos/containers/RepositoryRoot";
import Create from "../repos/containers/Create";
type Props = { type Props = {
authenticated?: boolean authenticated?: boolean
@@ -33,6 +34,12 @@ class Main extends React.Component<Props> {
component={Overview} component={Overview}
authenticated={authenticated} authenticated={authenticated}
/> />
<ProtectedRoute
exact
path="/repos/create"
component={Create}
authenticated={authenticated}
/>
<ProtectedRoute <ProtectedRoute
exact exact
path="/repos/:page" path="/repos/:page"

View File

@@ -6,6 +6,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
import users from "./users/modules/users"; import users from "./users/modules/users";
import repos from "./repos/modules/repos"; import repos from "./repos/modules/repos";
import repositoryTypes from "./repos/modules/repository-types";
import auth from "./modules/auth"; import auth from "./modules/auth";
import pending from "./modules/pending"; import pending from "./modules/pending";
import failure from "./modules/failure"; import failure from "./modules/failure";
@@ -22,6 +23,7 @@ function createReduxStore(history: BrowserHistory) {
failure, failure,
users, users,
repos, repos,
repositoryTypes,
auth auth
}); });

View File

@@ -0,0 +1,167 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { InputField, Select } from "../../components/forms";
import { SubmitButton } from "../../components/buttons";
import type { Repository } from "../types/Repositories";
import * as validator from "./repositoryValidation";
import type { RepositoryType } from "../types/RepositoryTypes";
type Props = {
submitForm: Repository => void,
repository?: Repository,
repositoryTypes: RepositoryType[],
loading?: boolean,
t: string => string
};
type State = {
repository: Repository,
nameValidationError: boolean,
contactValidationError: boolean
};
class RepositoryForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
repository: {
name: "",
namespace: "",
type: "",
contact: "",
description: "",
_links: {}
},
nameValidationError: false,
contactValidationError: false,
descriptionValidationError: false
};
}
componentDidMount() {
const { repository } = this.props;
if (repository) {
this.setState({ repository: { ...repository } });
}
}
isFalsy(value) {
if (!value) {
return true;
}
return false;
}
isValid = () => {
const repository = this.state.repository;
return !(
this.state.nameValidationError ||
this.state.contactValidationError ||
this.isFalsy(repository.name)
);
};
submit = (event: Event) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm(this.state.repository);
}
};
isCreateMode = () => {
return !this.props.repository;
};
render() {
const { loading, t } = this.props;
const repository = this.state.repository;
return (
<form onSubmit={this.submit}>
{this.renderCreateOnlyFields()}
<InputField
label={t("repository.contact")}
onChange={this.handleContactChange}
value={repository ? repository.contact : ""}
validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")}
/>
<InputField
label={t("repository.description")}
onChange={this.handleDescriptionChange}
value={repository ? repository.description : ""}
/>
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("repository-form.submit")}
/>
</form>
);
}
createSelectOptions(repositoryTypes: RepositoryType[]) {
return repositoryTypes.map(repositoryType => {
return {
label: repositoryType.displayName,
value: repositoryType.name
};
});
}
renderCreateOnlyFields() {
if (!this.isCreateMode()) {
return null;
}
const { repositoryTypes, t } = this.props;
const repository = this.state.repository;
return (
<div>
<InputField
label={t("repository.name")}
onChange={this.handleNameChange}
value={repository ? repository.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
/>
<Select
label={t("repository.type")}
onChange={this.handleTypeChange}
value={repository ? repository.type : ""}
options={this.createSelectOptions(repositoryTypes)}
/>
</div>
);
}
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
repository: { ...this.state.repository, name }
});
};
handleTypeChange = (type: string) => {
this.setState({
repository: { ...this.state.repository, type }
});
};
handleContactChange = (contact: string) => {
this.setState({
contactValidationError: !validator.isContactValid(contact),
repository: { ...this.state.repository, contact }
});
};
handleDescriptionChange = (description: string) => {
this.setState({
repository: { ...this.state.repository, description }
});
};
}
export default translate("repos")(RepositoryForm);

View File

@@ -0,0 +1,10 @@
// @flow
import * as generalValidator from "../../components/validation";
export const isNameValid = (name: string) => {
return generalValidator.isNameValid(name);
};
export function isContactValid(mail: string) {
return "" === mail || generalValidator.isMailValid(mail);
}

View File

@@ -0,0 +1,31 @@
import * as validator from "./repositoryValidation";
describe("repository name validation", () => {
// we don't need rich tests, because they are in validation.test.js
it("should validate the name", () => {
expect(validator.isNameValid("scm-manager")).toBe(true);
});
it("should fail for old nested repository names", () => {
// in v2 this is not allowed
expect(validator.isNameValid("scm/manager")).toBe(false);
expect(validator.isNameValid("scm/ma/nager")).toBe(false);
});
});
describe("repository contact validation", () => {
it("should allow empty contact", () => {
expect(validator.isContactValid("")).toBe(true);
});
// we don't need rich tests, because they are in validation.test.js
it("should allow real mail addresses", () => {
expect(validator.isContactValid("trici.mcmillian@hitchhiker.com")).toBe(
true
);
});
it("should fail on invalid mail addresses", () => {
expect(validator.isContactValid("tricia")).toBe(false);
});
});

View File

@@ -0,0 +1,71 @@
// @flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { Page } from "../../components/layout";
import RepositoryForm from "../components/RepositoryForm";
import type { RepositoryType } from "../types/RepositoryTypes";
import {
fetchRepositoryTypesIfNeeded,
getFetchRepositoryTypesFailure,
getRepositoryTypes,
isFetchRepositoryTypesPending
} from "../modules/repository-types";
type Props = {
repositoryTypes: RepositoryType[],
typesLoading: boolean,
error: Error,
// dispatch functions
fetchRepositoryTypesIfNeeded: () => void,
// context props
t: string => string
};
class Create extends React.Component<Props> {
componentDidMount() {
this.props.fetchRepositoryTypesIfNeeded();
}
render() {
const { typesLoading, repositoryTypes, error } = this.props;
const { t } = this.props;
return (
<Page
title={t("create.title")}
subtitle={t("create.subtitle")}
loading={typesLoading}
error={error}
>
<RepositoryForm repositoryTypes={repositoryTypes} />
</Page>
);
}
}
const mapStateToProps = state => {
const repositoryTypes = getRepositoryTypes(state);
const typesLoading = isFetchRepositoryTypesPending(state);
const error = getFetchRepositoryTypesFailure(state);
return {
repositoryTypes,
typesLoading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
fetchRepositoryTypesIfNeeded: () => {
dispatch(fetchRepositoryTypesIfNeeded());
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(Create));

View File

@@ -4,13 +4,21 @@ import React from "react";
import type { RepositoryCollection } from "../types/Repositories"; import type { RepositoryCollection } from "../types/Repositories";
import { connect } from "react-redux"; import { connect } from "react-redux";
import {fetchRepos, fetchReposByLink, fetchReposByPage, getFetchReposFailure, getRepositoryCollection, isFetchReposPending} from "../modules/repos"; import {
fetchRepos,
fetchReposByLink,
fetchReposByPage,
getFetchReposFailure,
getRepositoryCollection,
isFetchReposPending
} from "../modules/repos";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Page } from "../../components/layout"; import { Page } from "../../components/layout";
import RepositoryList from "../components/RepositoryList"; import RepositoryList from "../components/RepositoryList";
import Paginator from '../../components/Paginator'; import Paginator from "../../components/Paginator";
import {withRouter} from 'react-router-dom'; import { withRouter } from "react-router-dom";
import type { History } from "history"; import type { History } from "history";
import CreateButton from "../../components/buttons/CreateButton";
type Props = { type Props = {
page: number, page: number,
@@ -44,24 +52,33 @@ class Overview extends React.Component<Props> {
this.props.history.push(`/repos/${statePage}`); this.props.history.push(`/repos/${statePage}`);
} }
} }
}; }
render() { render() {
const { error, loading, t } = this.props; const { error, loading, t } = this.props;
return ( return (
<Page title={t("overview.title")} subtitle={t("overview.subtitle")} loading={loading} error={error}> <Page
title={t("overview.title")}
subtitle={t("overview.subtitle")}
loading={loading}
error={error}
>
{this.renderList()} {this.renderList()}
</Page> </Page>
); );
} }
renderList() { renderList() {
const { collection, fetchReposByLink } = this.props; const { collection, fetchReposByLink, t } = this.props;
if (collection) { if (collection) {
return ( return (
<div> <div>
<RepositoryList repositories={collection._embedded.repositories} /> <RepositoryList repositories={collection._embedded.repositories} />
<Paginator collection={collection} onPageChange={fetchReposByLink} /> <Paginator collection={collection} onPageChange={fetchReposByLink} />
<CreateButton
label={t("overview.create-button")}
link="/repos/create"
/>
</div> </div>
); );
} }
@@ -98,10 +115,10 @@ const mapDispatchToProps = dispatch => {
dispatch(fetchRepos()); dispatch(fetchRepos());
}, },
fetchReposByPage: (page: number) => { fetchReposByPage: (page: number) => {
dispatch(fetchReposByPage(page)) dispatch(fetchReposByPage(page));
}, },
fetchReposByLink: (link: string) => { fetchReposByLink: (link: string) => {
dispatch(fetchReposByLink(link)) dispatch(fetchReposByLink(link));
} }
}; };
}; };

View File

@@ -0,0 +1,107 @@
// @flow
import * as types from "../../modules/types";
import type { Action } from "../../types/Action";
import type {
RepositoryType,
RepositoryTypeCollection
} from "../types/RepositoryTypes";
import { apiClient } from "../../apiclient";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
export const FETCH_REPOSITORY_TYPES = "scm/repos/FETCH_REPOSITORY_TYPES";
export const FETCH_REPOSITORY_TYPES_PENDING = `${FETCH_REPOSITORY_TYPES}_${
types.PENDING_SUFFIX
}`;
export const FETCH_REPOSITORY_TYPES_SUCCESS = `${FETCH_REPOSITORY_TYPES}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_REPOSITORY_TYPES_FAILURE = `${FETCH_REPOSITORY_TYPES}_${
types.FAILURE_SUFFIX
}`;
export function fetchRepositoryTypesIfNeeded() {
return function(dispatch: any, getState: () => Object) {
if (shouldFetchRepositoryTypes(getState())) {
return fetchRepositoryTypes(dispatch);
}
};
}
function fetchRepositoryTypes(dispatch: any) {
dispatch(fetchRepositoryTypesPending());
return apiClient
.get("repository-types")
.then(response => response.json())
.then(repositoryTypes => {
dispatch(fetchRepositoryTypesSuccess(repositoryTypes));
})
.catch(err => {
dispatch(fetchRepositoryTypesFailure(err));
});
}
export function shouldFetchRepositoryTypes(state: Object) {
if (
isFetchRepositoryTypesPending(state) ||
getFetchRepositoryTypesFailure(state)
) {
return false;
}
if (state.repositoryTypes && state.repositoryTypes.length > 0) {
return false;
}
return true;
}
export function fetchRepositoryTypesPending(): Action {
return {
type: FETCH_REPOSITORY_TYPES_PENDING
};
}
export function fetchRepositoryTypesSuccess(
repositoryTypes: RepositoryTypeCollection
): Action {
return {
type: FETCH_REPOSITORY_TYPES_SUCCESS,
payload: repositoryTypes
};
}
export function fetchRepositoryTypesFailure(error: Error): Action {
return {
type: FETCH_REPOSITORY_TYPES_FAILURE,
payload: error
};
}
// reducers
export default function reducer(
state: RepositoryType[] = [],
action: Action = { type: "UNKNOWN" }
): RepositoryType[] {
if (action.type === FETCH_REPOSITORY_TYPES_SUCCESS && action.payload) {
return action.payload._embedded["repository-types"];
}
return state;
}
// selectors
export function getRepositoryTypes(state: Object) {
if (state.repositoryTypes) {
return state.repositoryTypes;
}
return [];
}
export function isFetchRepositoryTypesPending(state: Object) {
return isPending(state, FETCH_REPOSITORY_TYPES);
}
export function getFetchRepositoryTypesFailure(state: Object) {
return getFailure(state, FETCH_REPOSITORY_TYPES);
}

View File

@@ -0,0 +1,198 @@
// @flow
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import {
FETCH_REPOSITORY_TYPES,
FETCH_REPOSITORY_TYPES_FAILURE,
FETCH_REPOSITORY_TYPES_PENDING,
FETCH_REPOSITORY_TYPES_SUCCESS,
fetchRepositoryTypesIfNeeded,
fetchRepositoryTypesSuccess,
getFetchRepositoryTypesFailure,
getRepositoryTypes,
isFetchRepositoryTypesPending,
shouldFetchRepositoryTypes
} from "./repository-types";
import reducer from "./repository-types";
const git = {
name: "git",
displayName: "Git",
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/repository-types/git"
}
}
};
const hg = {
name: "hg",
displayName: "Mercurial",
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/repository-types/hg"
}
}
};
const svn = {
name: "svn",
displayName: "Subversion",
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/repository-types/svn"
}
}
};
const collection = {
_embedded: {
"repository-types": [git, hg, svn]
},
_links: {
self: {
href: "http://localhost:8081/scm/api/rest/v2/repository-types"
}
}
};
describe("repository types caching", () => {
it("should fetch repository types, on empty state", () => {
expect(shouldFetchRepositoryTypes({})).toBe(true);
});
it("should fetch repository types, if the state contains an empty array", () => {
const state = {
repositoryTypes: []
};
expect(shouldFetchRepositoryTypes(state)).toBe(true);
});
it("should not fetch repository types, on pending state", () => {
const state = {
pending: {
[FETCH_REPOSITORY_TYPES]: true
}
};
expect(shouldFetchRepositoryTypes(state)).toBe(false);
});
it("should not fetch repository types, on failure state", () => {
const state = {
failure: {
[FETCH_REPOSITORY_TYPES]: new Error("no...")
}
};
expect(shouldFetchRepositoryTypes(state)).toBe(false);
});
it("should not fetch repository types, if they are already fetched", () => {
const state = {
repositoryTypes: [git, hg, svn]
};
expect(shouldFetchRepositoryTypes(state)).toBe(false);
});
});
describe("repository types fetch", () => {
const URL = "/scm/api/rest/v2/repository-types";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should successfully fetch repository types", () => {
fetchMock.getOnce(URL, collection);
const expectedActions = [
{ type: FETCH_REPOSITORY_TYPES_PENDING },
{
type: FETCH_REPOSITORY_TYPES_SUCCESS,
payload: collection
}
];
const store = mockStore({});
return store.dispatch(fetchRepositoryTypesIfNeeded()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_REPOSITORY_TYPES_FAILURE on server error", () => {
fetchMock.getOnce(URL, {
status: 500
});
const store = mockStore({});
return store.dispatch(fetchRepositoryTypesIfNeeded()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toBe(FETCH_REPOSITORY_TYPES_PENDING);
expect(actions[1].type).toBe(FETCH_REPOSITORY_TYPES_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should dispatch not dispatch any action, if the repository types are already fetched", () => {
const store = mockStore({
repositoryTypes: [git, hg, svn]
});
store.dispatch(fetchRepositoryTypesIfNeeded());
expect(store.getActions().length).toBe(0);
});
});
describe("repository types reducer", () => {
it("should return unmodified state on unknown action", () => {
const state = [];
expect(reducer(state)).toBe(state);
});
it("should store the repository types on FETCH_REPOSITORY_TYPES_SUCCESS", () => {
const newState = reducer([], fetchRepositoryTypesSuccess(collection));
expect(newState).toEqual([git, hg, svn]);
});
});
describe("repository types selectors", () => {
const error = new Error("The end of the universe");
it("should return an emtpy array", () => {
expect(getRepositoryTypes({})).toEqual([]);
});
it("should return the repository types", () => {
const state = {
repositoryTypes: [git, hg, svn]
};
expect(getRepositoryTypes(state)).toEqual([git, hg, svn]);
});
it("should return true, when fetch repository types is pending", () => {
const state = {
pending: {
[FETCH_REPOSITORY_TYPES]: true
}
};
expect(isFetchRepositoryTypesPending(state)).toEqual(true);
});
it("should return false, when fetch repos is not pending", () => {
expect(isFetchRepositoryTypesPending({})).toEqual(false);
});
it("should return error when fetch repository types did fail", () => {
const state = {
failure: {
[FETCH_REPOSITORY_TYPES]: error
}
};
expect(getFetchRepositoryTypesFailure(state)).toEqual(error);
});
it("should return undefined when fetch repos did not fail", () => {
expect(getFetchRepositoryTypesFailure({})).toBe(undefined);
});
});

View File

@@ -0,0 +1,14 @@
// @flow
import type { Collection } from "../../types/Collection";
export type RepositoryType = {
name: string,
displayName: string
};
export type RepositoryTypeCollection = Collection & {
_embedded: {
"repository-types": RepositoryType[]
}
};

View File

@@ -4,7 +4,8 @@ import { translate } from "react-i18next";
import type { User } from "../types/User"; import type { User } from "../types/User";
import { Checkbox, InputField } from "../../components/forms"; import { Checkbox, InputField } from "../../components/forms";
import { SubmitButton } from "../../components/buttons"; import { SubmitButton } from "../../components/buttons";
import * as validator from "./userValidation"; import * as validator from "../../components/validation";
import * as userValidator from "./userValidation";
type Props = { type Props = {
submitForm: User => void, submitForm: User => void,
@@ -157,7 +158,9 @@ class UserForm extends React.Component<Props, State> {
handleDisplayNameChange = (displayName: string) => { handleDisplayNameChange = (displayName: string) => {
this.setState({ this.setState({
displayNameValidationError: !validator.isDisplayNameValid(displayName), displayNameValidationError: !userValidator.isDisplayNameValid(
displayName
),
user: { ...this.state.user, displayName } user: { ...this.state.user, displayName }
}); });
}; };
@@ -175,7 +178,7 @@ class UserForm extends React.Component<Props, State> {
this.state.validatePassword this.state.validatePassword
); );
this.setState({ this.setState({
validatePasswordError: !validator.isPasswordValid(password), validatePasswordError: !userValidator.isPasswordValid(password),
passwordValidationError: validatePasswordError, passwordValidationError: validatePasswordError,
user: { ...this.state.user, password } user: { ...this.state.user, password }
}); });

View File

@@ -1,30 +1,20 @@
//@flow //@flow
import React from "react"; import React from "react";
import injectSheet from "react-jss";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { AddButton } from "../../../components/buttons"; import { CreateButton } from "../../../components/buttons";
import classNames from "classnames";
const styles = {
spacing: {
margin: "1em 0 0 1em"
}
};
// TODO remove
type Props = { type Props = {
t: string => string, t: string => string
classes: any
}; };
class CreateUserButton extends React.Component<Props> { class CreateUserButton extends React.Component<Props> {
render() { render() {
const { classes, t } = this.props; const { t } = this.props;
return ( return (
<div className={classNames("is-pulled-right", classes.spacing)}> <CreateButton label={t("create-user-button.label")} link="/users/add" />
<AddButton label={t("create-user-button.label")} link="/users/add" />
</div>
); );
} }
} }
export default translate("users")(injectSheet(styles)(CreateUserButton)); export default translate("users")(CreateUserButton);

View File

@@ -1,24 +1,11 @@
// @flow // @flow
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
export const isNameValid = (name: string) => {
return nameRegex.test(name);
};
export const isDisplayNameValid = (displayName: string) => { export const isDisplayNameValid = (displayName: string) => {
if (displayName) { if (displayName) {
return true; return true;
} }
return false; return false;
}; };
const mailRegex = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-.]*\.[A-z0-9][A-z0-9-]+$/;
export const isMailValid = (mail: string) => {
return mailRegex.test(mail);
};
export const isPasswordValid = (password: string) => { export const isPasswordValid = (password: string) => {
return password.length > 6 && password.length < 32; return password.length > 6 && password.length < 32;
}; };

View File

@@ -1,45 +1,6 @@
// @flow // @flow
import * as validator from "./userValidation"; import * as validator from "./userValidation";
describe("test name validation", () => {
it("should return false", () => {
// invalid names taken from ValidationUtilTest.java
const invalidNames = [
" test 123",
" test 123 ",
"test 123 ",
"test/123",
"test%123",
"test:123",
"t ",
" t",
" t ",
""
];
for (let name of invalidNames) {
expect(validator.isNameValid(name)).toBe(false);
}
});
it("should return true", () => {
// valid names taken from ValidationUtilTest.java
const validNames = [
"test",
"test.git",
"Test123.git",
"Test123-git",
"Test_user-123.git",
"test@scm-manager.de",
"test 123",
"tt",
"t"
];
for (let name of validNames) {
expect(validator.isNameValid(name)).toBe(true);
}
});
});
describe("test displayName validation", () => { describe("test displayName validation", () => {
it("should return false", () => { it("should return false", () => {
expect(validator.isDisplayNameValid("")).toBe(false); expect(validator.isDisplayNameValid("")).toBe(false);
@@ -60,41 +21,6 @@ describe("test displayName validation", () => {
}); });
}); });
describe("test mail validation", () => {
it("should return false", () => {
// invalid taken from ValidationUtilTest.java
const invalid = [
"ostfalia.de",
"@ostfalia.de",
"s.sdorra@",
"s.sdorra@ostfalia",
"s.sdorra@@ostfalia.de",
"s.sdorra@ ostfalia.de",
"s.sdorra @ostfalia.de"
];
for (let mail of invalid) {
expect(validator.isMailValid(mail)).toBe(false);
}
});
it("should return true", () => {
// valid taken from ValidationUtilTest.java
const valid = [
"s.sdorra@ostfalia.de",
"sdorra@ostfalia.de",
"s.sdorra@hbk-bs.de",
"s.sdorra@gmail.com",
"s.sdorra@t.co",
"s.sdorra@ucla.college",
"s.sdorra@example.xn--p1ai",
"s.sdorra@scm.solutions"
];
for (let mail of valid) {
expect(validator.isMailValid(mail)).toBe(true);
}
});
});
describe("test password validation", () => { describe("test password validation", () => {
it("should return false", () => { it("should return false", () => {
// invalid taken from ValidationUtilTest.java // invalid taken from ValidationUtilTest.java