merge brachn 2.0.0-m3

This commit is contained in:
Maren Süwer
2018-09-11 08:53:58 +02:00
234 changed files with 38690 additions and 1880 deletions

View File

@@ -1,77 +0,0 @@
// @flow
import { contextPath } from "./urls";
export const NOT_FOUND_ERROR = Error("not found");
export const UNAUTHORIZED_ERROR = Error("unauthorized");
const fetchOptions: RequestOptions = {
credentials: "same-origin",
headers: {
Cache: "no-cache"
}
};
function handleStatusCode(response: Response) {
if (!response.ok) {
if (response.status === 401) {
throw UNAUTHORIZED_ERROR;
}
if (response.status === 404) {
throw NOT_FOUND_ERROR;
}
throw new Error("server returned status code " + response.status);
}
return response;
}
export function createUrl(url: string) {
if (url.includes("://")) {
return url;
}
let urlWithStartingSlash = url;
if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash;
}
return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
}
class ApiClient {
get(url: string): Promise<Response> {
return fetch(createUrl(url), fetchOptions).then(handleStatusCode);
}
post(url: string, payload: any, contentType: string = "application/json") {
return this.httpRequestWithJSONBody("POST", url, contentType, payload);
}
put(url: string, payload: any, contentType: string = "application/json") {
return this.httpRequestWithJSONBody("PUT", url, contentType, payload);
}
delete(url: string): Promise<Response> {
let options: RequestOptions = {
method: "DELETE"
};
options = Object.assign(options, fetchOptions);
return fetch(createUrl(url), options).then(handleStatusCode);
}
httpRequestWithJSONBody(
method: string,
url: string,
contentType: string,
payload: any
): Promise<Response> {
let options: RequestOptions = {
method: method,
body: JSON.stringify(payload)
};
options = Object.assign(options, fetchOptions);
// $FlowFixMe
options.headers["Content-Type"] = contentType;
return fetch(createUrl(url), options).then(handleStatusCode);
}
}
export let apiClient = new ApiClient();

View File

@@ -1,15 +0,0 @@
// @flow
import { createUrl } from "./apiclient";
describe("create url", () => {
it("should not change absolute urls", () => {
expect(createUrl("https://www.scm-manager.org")).toBe(
"https://www.scm-manager.org"
);
});
it("should add prefix for api", () => {
expect(createUrl("/users")).toBe("/api/rest/v2/users");
expect(createUrl("users")).toBe("/api/rest/v2/users");
});
});

View File

@@ -1,32 +0,0 @@
//@flow
import React from "react";
import moment from "moment";
import { translate } from "react-i18next";
type Props = {
date?: string,
// context props
i18n: any
};
class DateFromNow extends React.Component<Props> {
static format(locale: string, date?: string) {
let fromNow = "";
if (date) {
fromNow = moment(date)
.locale(locale)
.fromNow();
}
return fromNow;
}
render() {
const { i18n, date } = this.props;
const fromNow = DateFromNow.format(i18n.language, date);
return <span>{fromNow}</span>;
}
}
export default translate()(DateFromNow);

View File

@@ -1,25 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import Notification from "./Notification";
type Props = {
t: string => string,
error?: Error
};
class ErrorNotification extends React.Component<Props> {
render() {
const { t, error } = this.props;
if (error) {
return (
<Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong> {error.message}
</Notification>
);
}
return "";
}
}
export default translate("commons")(ErrorNotification);

View File

@@ -1,27 +0,0 @@
//@flow
import React from "react";
import ErrorNotification from "./ErrorNotification";
type Props = {
error: Error,
title: string,
subtitle: string
};
class ErrorPage extends React.Component<Props> {
render() {
const { title, subtitle, error } = this.props;
return (
<section className="section">
<div className="box column is-4 is-offset-4 container">
<h1 className="title">{title}</h1>
<p className="subtitle">{subtitle}</p>
<ErrorNotification error={error} />
</div>
</section>
);
}
}
export default ErrorPage;

View File

@@ -1,18 +0,0 @@
//@flow
import React from "react";
import { withContextPath } from "../urls";
type Props = {
src: string,
alt: string,
className?: any
};
class Image extends React.Component<Props> {
render() {
const { src, alt, className } = this.props;
return <img className={className} src={withContextPath(src)} alt={alt} />;
}
}
export default Image;

View File

@@ -1,51 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import Image from "./Image";
const styles = {
wrapper: {
position: "relative"
},
loading: {
width: "128px",
height: "128px",
position: "absolute",
top: "50%",
left: "50%",
margin: "64px 0 0 -64px"
},
image: {
width: "128px",
height: "128px"
}
};
type Props = {
t: string => string,
message?: string,
classes: any
};
class Loading extends React.Component<Props> {
render() {
const { message, t, classes } = this.props;
return (
<div className={classes.wrapper}>
<div className={classes.loading}>
<Image
className={classes.image}
src="/images/loading.svg"
alt={t("loading.alt")}
/>
<p className="has-text-centered">{message}</p>
</div>
</div>
);
}
}
export default injectSheet(styles)(translate("commons")(Loading));

View File

@@ -1,17 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import Image from "./Image";
type Props = {
t: string => string
};
class Logo extends React.Component<Props> {
render() {
const { t } = this.props;
return <Image src="/images/logo.png" alt={t("logo.alt")} />;
}
}
export default translate("commons")(Logo);

View File

@@ -1,18 +0,0 @@
// @flow
import React from "react";
type Props = {
address?: string
};
class MailLink extends React.Component<Props> {
render() {
const { address } = this.props;
if (!address) {
return null;
}
return <a href={"mailto: " + address}>{address}</a>;
}
}
export default MailLink;

View File

@@ -1,37 +0,0 @@
//@flow
import * as React from "react";
import classNames from "classnames";
type NotificationType = "primary" | "info" | "success" | "warning" | "danger";
type Props = {
type: NotificationType,
onClose?: () => void,
children?: React.Node
};
class Notification extends React.Component<Props> {
static defaultProps = {
type: "info"
};
renderCloseButton() {
const { onClose } = this.props;
if (onClose) {
return <button className="delete" onClick={onClose} />;
}
return "";
}
render() {
const { type, children } = this.props;
return (
<div className={classNames("notification", "is-" + type)}>
{this.renderCloseButton()}
{children}
</div>
);
}
}
export default Notification;

View File

@@ -1,121 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { PagedCollection } from "../types/Collection";
import { Button } from "./buttons";
type Props = {
collection: PagedCollection,
onPageChange: string => void,
t: string => string
};
class Paginator extends React.Component<Props> {
isLinkUnavailable(linkType: string) {
return !this.props.collection || !this.props.collection._links[linkType];
}
createAction = (linkType: string) => () => {
const { collection, onPageChange } = this.props;
const link = collection._links[linkType].href;
onPageChange(link);
};
renderFirstButton() {
return this.renderPageButton(1, "first");
}
renderPreviousButton() {
const { t } = this.props;
return this.renderButton(
"pagination-previous",
t("paginator.previous"),
"prev"
);
}
renderNextButton() {
const { t } = this.props;
return this.renderButton("pagination-next", t("paginator.next"), "next");
}
renderLastButton() {
const { collection } = this.props;
return this.renderPageButton(collection.pageTotal, "last");
}
renderPageButton(page: number, linkType: string) {
return this.renderButton("pagination-link", page.toString(), linkType);
}
renderButton(className: string, label: string, linkType: string) {
return (
<Button
className={className}
label={label}
disabled={this.isLinkUnavailable(linkType)}
action={this.createAction(linkType)}
/>
);
}
seperator() {
return <span className="pagination-ellipsis">&hellip;</span>;
}
currentPage(page: number) {
return (
<Button
className="pagination-link is-current"
label={page}
disabled={true}
/>
);
}
pageLinks() {
const { collection } = this.props;
const links = [];
const page = collection.page + 1;
const pageTotal = collection.pageTotal;
if (page > 1) {
links.push(this.renderFirstButton());
}
if (page > 3) {
links.push(this.seperator());
}
if (page > 2) {
links.push(this.renderPageButton(page - 1, "prev"));
}
links.push(this.currentPage(page));
if (page + 1 < pageTotal) {
links.push(this.renderPageButton(page + 1, "next"));
}
if(page+2 < pageTotal) //if there exists pages between next and last
links.push(this.seperator());
if (page < pageTotal) {
links.push(this.renderLastButton());
}
return links;
}
render() {
return (
<nav className="pagination is-centered" aria-label="pagination">
{this.renderPreviousButton()}
{this.renderNextButton()}
<ul className="pagination-list">
{this.pageLinks().map((link, index) => {
return <li key={index}>{link}</li>;
})}
</ul>
</nav>
);
}
}
export default translate("commons")(Paginator);

View File

@@ -1,253 +0,0 @@
// @flow
import React from "react";
import { mount, shallow } from "enzyme";
import "../tests/enzyme";
import "../tests/i18n";
import Paginator from "./Paginator";
describe("paginator rendering tests", () => {
const dummyLink = {
href: "https://dummy"
};
it("should render all buttons but disabled, without links", () => {
const collection = {
page: 10,
pageTotal: 20,
_links: {}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7);
for (let button of buttons) {
expect(button.props.disabled).toBeTruthy();
}
});
it("should render buttons for first page", () => {
const collection = {
page: 0,
pageTotal: 148,
_links: {
first: dummyLink,
next: dummyLink,
last: dummyLink
}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5);
// previous button
expect(buttons.get(0).props.disabled).toBeTruthy();
// last button
expect(buttons.get(1).props.disabled).toBeFalsy();
// first button
const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeTruthy();
expect(firstButton.label).toBe(1);
// next button
const nextButton = buttons.get(3).props;
expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("2");
// last button
const lastButton = buttons.get(4).props;
expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148");
});
it("should render buttons for second page", () => {
const collection = {
page: 1,
pageTotal: 148,
_links: {
first: dummyLink,
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6);
// previous button
expect(buttons.get(0).props.disabled).toBeFalsy();
// last button
expect(buttons.get(1).props.disabled).toBeFalsy();
// first button
const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1");
// current button
const currentButton = buttons.get(3).props;
expect(currentButton.disabled).toBeTruthy();
expect(currentButton.label).toBe(2);
// next button
const nextButton = buttons.get(4).props;
expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("3");
// last button
const lastButton = buttons.get(5).props;
expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148");
});
it("should render buttons for last page", () => {
const collection = {
page: 147,
pageTotal: 148,
_links: {
first: dummyLink,
prev: dummyLink
}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5);
// previous button
expect(buttons.get(0).props.disabled).toBeFalsy();
// last button
expect(buttons.get(1).props.disabled).toBeTruthy();
// first button
const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1");
// next button
const nextButton = buttons.get(3).props;
expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("147");
// last button
const lastButton = buttons.get(4).props;
expect(lastButton.disabled).toBeTruthy();
expect(lastButton.label).toBe(148);
});
it("should render buttons for penultimate page", () => {
const collection = {
page: 146,
pageTotal: 148,
_links: {
first: dummyLink,
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6);
// previous button
expect(buttons.get(0).props.disabled).toBeFalsy();
// last button
expect(buttons.get(1).props.disabled).toBeFalsy();
// first button
const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1");
const currentButton = buttons.get(3).props;
expect(currentButton.disabled).toBeFalsy();
expect(currentButton.label).toBe("146");
// current button
const nextButton = buttons.get(4).props;
expect(nextButton.disabled).toBeTruthy();
expect(nextButton.label).toBe(147);
// last button
const lastButton = buttons.get(5).props;
expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148");
});
it("should render buttons for a page in the middle", () => {
const collection = {
page: 41,
pageTotal: 148,
_links: {
first: dummyLink,
prev: dummyLink,
next: dummyLink,
last: dummyLink
}
};
const paginator = shallow(<Paginator collection={collection} />);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7);
// previous button
expect(buttons.get(0).props.disabled).toBeFalsy();
// next button
expect(buttons.get(1).props.disabled).toBeFalsy();
// first button
const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1");
// previous Button
const previousButton = buttons.get(3).props;
expect(previousButton.disabled).toBeFalsy();
expect(previousButton.label).toBe("41");
// current button
const currentButton = buttons.get(4).props;
expect(currentButton.disabled).toBeTruthy();
expect(currentButton.label).toBe(42);
// next button
const nextButton = buttons.get(5).props;
expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("43");
// last button
const lastButton = buttons.get(6).props;
expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148");
});
it("should call the function with the last previous url", () => {
const collection = {
page: 41,
pageTotal: 148,
_links: {
first: dummyLink,
prev: {
href: "https://www.scm-manager.org"
},
next: dummyLink,
last: dummyLink
}
};
let urlToOpen;
const callMe = (url: string) => {
urlToOpen = url;
};
const paginator = mount(
<Paginator collection={collection} onPageChange={callMe} />
);
paginator.find("Button.pagination-previous").simulate("click");
expect(urlToOpen).toBe("https://www.scm-manager.org");
});
});

View File

@@ -1,39 +0,0 @@
//@flow
import React, { Component } from "react";
import { Route, Redirect, withRouter } from "react-router-dom";
type Props = {
authenticated?: boolean,
component: Component<any, any>
};
class ProtectedRoute extends React.Component<Props> {
renderRoute = (Component: any, authenticated?: boolean) => {
return (routeProps: any) => {
if (authenticated) {
return <Component {...routeProps} />;
} else {
return (
<Redirect
to={{
pathname: "/login",
state: { from: routeProps.location }
}}
/>
);
}
};
};
render() {
const { component, authenticated, ...routeProps } = this.props;
return (
<Route
{...routeProps}
render={this.renderRoute(component, authenticated)}
/>
);
}
}
export default withRouter(ProtectedRoute);

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 color="default" {...this.props} />;
}
}
export default AddButton;

View File

@@ -1,69 +0,0 @@
//@flow
import React from "react";
import classNames from "classnames";
import { Link } from "react-router-dom";
export type ButtonProps = {
label: string,
loading?: boolean,
disabled?: boolean,
action?: (event: Event) => void,
link?: string,
fullWidth?: boolean,
className?: string,
classes: any
};
type Props = ButtonProps & {
type: string,
color: string
};
class Button extends React.Component<Props> {
static defaultProps = {
type: "button",
color: "default"
};
renderButton = () => {
const {
label,
loading,
disabled,
type,
color,
action,
fullWidth,
className
} = this.props;
const loadingClass = loading ? "is-loading" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
return (
<button
type={type}
disabled={disabled}
onClick={action ? action : (event: Event) => {}}
className={classNames(
"button",
"is-" + color,
loadingClass,
fullWidthClass,
className
)}
>
{label}
</button>
);
};
render() {
const { link } = this.props;
if (link) {
return <Link to={link}>{this.renderButton()}</Link>;
} else {
return this.renderButton();
}
}
}
export default Button;

View File

@@ -1,24 +0,0 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import AddButton, { 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)}>
<AddButton {...this.props} />
</div>
);
}
}
export default injectSheet(styles)(CreateButton);

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
//@flow
import React from "react";
import { DeleteButton } from ".";
import classNames from "classnames";
type Props = {
entryname: string,
removeEntry: string => void,
disabled: boolean,
label: string
};
type State = {};
class RemoveEntryOfTableButton extends React.Component<Props, State> {
render() {
const { label, entryname, removeEntry, disabled } = this.props;
return (
<div className={classNames("is-pulled-right")}>
<DeleteButton
label={label}
action={(event: Event) => {
event.preventDefault();
removeEntry(entryname);
}}
disabled={disabled}
/>
</div>
);
}
}
export default RemoveEntryOfTableButton;

View File

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

View File

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

View File

@@ -1,68 +0,0 @@
//@flow
import React from "react";
import { AddButton } from "../buttons";
import InputField from "./InputField";
type Props = {
addEntry: string => void,
disabled: boolean,
buttonLabel: string,
fieldLabel: string,
errorMessage: string
};
type State = {
entryToAdd: string
};
class AddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
entryToAdd: ""
};
}
render() {
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
return (
<div className="field">
<InputField
label={fieldLabel}
errorMessage={errorMessage}
onChange={this.handleAddEntryChange}
validationError={false}
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
/>
<AddButton
label={buttonLabel}
action={this.addButtonClicked}
disabled={disabled}
/>
</div>
);
}
addButtonClicked = (event: Event) => {
event.preventDefault();
this.appendEntry();
};
appendEntry = () => {
const { entryToAdd } = this.state;
this.props.addEntry(entryToAdd);
this.setState({ ...this.state, entryToAdd: "" });
};
handleAddEntryChange = (entryname: string) => {
this.setState({
...this.state,
entryToAdd: entryname
});
};
}
export default AddEntryToTableField;

View File

@@ -1,36 +0,0 @@
//@flow
import React from "react";
type Props = {
label?: string,
checked: boolean,
onChange?: boolean => void,
disabled?: boolean
};
class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
if (this.props.onChange) {
this.props.onChange(event.target.checked);
}
};
render() {
return (
<div className="field">
<div className="control">
<label className="checkbox" disabled={this.props.disabled}>
<input
type="checkbox"
checked={this.props.checked}
onChange={this.onCheckboxChange}
disabled={this.props.disabled}
/>
{this.props.label}
</label>
</div>
</div>
);
}
}
export default Checkbox;

View File

@@ -1,94 +0,0 @@
//@flow
import React from "react";
import classNames from "classnames";
type Props = {
label?: string,
placeholder?: string,
value?: string,
type?: string,
autofocus?: boolean,
onChange: string => void,
onReturnPressed?: () => void,
validationError: boolean,
errorMessage: string,
disabled?: boolean
};
class InputField extends React.Component<Props> {
static defaultProps = {
type: "text",
placeholder: ""
};
field: ?HTMLInputElement;
componentDidMount() {
if (this.props.autofocus && this.field) {
this.field.focus();
}
}
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
const onReturnPressed = this.props.onReturnPressed;
if (!onReturnPressed) {
return;
}
if (event.key === "Enter") {
event.preventDefault();
onReturnPressed();
}
};
render() {
const {
type,
placeholder,
value,
validationError,
errorMessage,
disabled
} = this.props;
const errorView = validationError ? "is-danger" : "";
const helper = validationError ? (
<p className="help is-danger">{errorMessage}</p>
) : (
""
);
return (
<div className="field">
{this.renderLabel()}
<div className="control">
<input
ref={input => {
this.field = input;
}}
className={classNames("input", errorView)}
type={type}
placeholder={placeholder}
value={value}
onChange={this.handleInput}
onKeyPress={this.handleKeyPress}
disabled={disabled}
/>
</div>
{helper}
</div>
);
}
}
export default InputField;

View File

@@ -1,72 +0,0 @@
//@flow
import React from "react";
import classNames from "classnames";
export type SelectItem = {
value: string,
label: string
};
type Props = {
label?: string,
options: SelectItem[],
value?: SelectItem,
onChange: string => void,
loading?: boolean
};
class Select extends React.Component<Props> {
field: ?HTMLSelectElement;
componentDidMount() {
// trigger change after render, if value is null to set it to the first value
// of the given options.
if (!this.props.value && this.field && this.field.value) {
this.props.onChange(this.field.value);
}
}
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, loading } = this.props;
const loadingClass = loading ? "is-loading" : "";
return (
<div className="field">
{this.renderLabel()}
<div className={classNames(
"control select",
loadingClass
)}>
<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,53 +0,0 @@
//@flow
import React from "react";
export type SelectItem = {
value: string,
label: string
};
type Props = {
label?: string,
placeholder?: SelectItem[],
value?: string,
onChange: string => void
};
class Textarea extends React.Component<Props> {
field: ?HTMLTextAreaElement;
handleInput = (event: SyntheticInputEvent<HTMLTextAreaElement>) => {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { placeholder, value } = this.props;
return (
<div className="field">
{this.renderLabel()}
<div className="control">
<textarea
className="textarea"
ref={input => {
this.field = input;
}}
placeholder={placeholder}
onChange={this.handleInput}
value={value}
/>
</div>
</div>
);
}
}
export default Textarea;

View File

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

View File

@@ -1,25 +0,0 @@
//@flow
import React from "react";
import type { Me } from "../../types/Me";
type Props = {
me?: Me
};
class Footer extends React.Component<Props> {
render() {
const { me } = this.props;
if (!me) {
return "";
}
return (
<footer className="footer">
<div className="container is-centered">
<p className="has-text-centered">{me.displayName}</p>
</div>
</footer>
);
}
}
export default Footer;

View File

@@ -1,31 +0,0 @@
//@flow
import * as React from "react";
import Logo from "./../Logo";
type Props = {
children?: React.Node
};
class Header extends React.Component<Props> {
render() {
const { children } = this.props;
return (
<section className="hero is-dark is-small">
<div className="hero-body">
<div className="container">
<div className="columns is-vcentered">
<div className="column">
<Logo />
</div>
</div>
</div>
</div>
<div className="hero-foot">
<div className="container">{children}</div>
</div>
</section>
);
}
}
export default Header;

View File

@@ -1,44 +0,0 @@
//@flow
import * as React from "react";
import Loading from "./../Loading";
import ErrorNotification from "./../ErrorNotification";
import Title from "./Title";
import Subtitle from "./Subtitle";
type Props = {
title?: string,
subtitle?: string,
loading?: boolean,
error?: Error,
showContentOnError?: boolean,
children: React.Node
};
class Page extends React.Component<Props> {
render() {
const { title, error, subtitle } = this.props;
return (
<section className="section">
<div className="container">
<Title title={title} />
<Subtitle subtitle={subtitle} />
<ErrorNotification error={error} />
{this.renderContent()}
</div>
</section>
);
}
renderContent() {
const { loading, children, showContentOnError, error } = this.props;
if (error && !showContentOnError) {
return null;
}
if (loading) {
return <Loading />;
}
return children;
}
}
export default Page;

View File

@@ -1,18 +0,0 @@
// @flow
import React from "react";
type Props = {
subtitle?: string
};
class Subtitle extends React.Component<Props> {
render() {
const { subtitle } = this.props;
if (subtitle) {
return <h1 className="subtitle">{subtitle}</h1>;
}
return null;
}
}
export default Subtitle;

View File

@@ -1,18 +0,0 @@
// @flow
import React from "react";
type Props = {
title?: string
};
class Title extends React.Component<Props> {
render() {
const { title } = this.props;
if (title) {
return <h1 className="title">{title}</h1>;
}
return null;
}
}
export default Title;

View File

@@ -1,3 +0,0 @@
export { default as Footer } from "./Footer";
export { default as Header } from "./Header";
export { default as Page } from "./Page";

View File

@@ -1,102 +0,0 @@
/*modified from https://github.com/GA-MO/react-confirm-alert*/
.react-confirm-alert-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
background: rgba(255, 255, 255, 0.9);
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
justify-content: center;
-ms-align-items: center;
align-items: center;
opacity: 0;
-webkit-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
-moz-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
-o-animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
animation: react-confirm-alert-fadeIn 0.5s 0.2s forwards;
}
.react-confirm-alert-body {
font-family: Arial, Helvetica, sans-serif;
width: 400px;
padding: 30px;
text-align: left;
background: #fff;
border-radius: 10px;
box-shadow: 0 20px 75px rgba(0, 0, 0, 0.13);
color: #666;
}
.react-confirm-alert-body > h1 {
margin-top: 0;
}
.react-confirm-alert-body > h3 {
margin: 0;
font-size: 16px;
}
.react-confirm-alert-button-group {
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
justify-content: flex-start;
margin-top: 20px;
}
.react-confirm-alert-button-group > button {
outline: none;
background: #333;
border: none;
display: inline-block;
padding: 6px 18px;
color: #eee;
margin-right: 10px;
border-radius: 5px;
font-size: 12px;
cursor: pointer;
}
@-webkit-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-moz-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-o-keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes react-confirm-alert-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -1,82 +0,0 @@
// @flow
//modified from https://github.com/GA-MO/react-confirm-alert
import * as React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import "./ConfirmAlert.css";
type Button = {
label: string,
onClick: () => void | null
};
type Props = {
title: string,
message: string,
buttons: Button[]
};
class ConfirmAlert extends React.Component<Props> {
handleClickButton = (button: Button) => {
if (button.onClick) {
button.onClick();
}
this.close();
};
close = () => {
removeElementReconfirm();
};
render() {
const { title, message, buttons } = this.props;
return (
<div className="react-confirm-alert-overlay">
<div className="react-confirm-alert">
{
<div className="react-confirm-alert-body">
{title && <h1>{title}</h1>}
{message}
<div className="react-confirm-alert-button-group">
{buttons.map((button, i) => (
<button
key={i}
onClick={() => this.handleClickButton(button)}
>
{button.label}
</button>
))}
</div>
</div>
}
</div>
</div>
);
}
}
function createElementReconfirm(properties: Props) {
const divTarget = document.createElement("div");
divTarget.id = "react-confirm-alert";
if (document.body) {
document.body.appendChild(divTarget);
render(<ConfirmAlert {...properties} />, divTarget);
}
}
function removeElementReconfirm() {
const target = document.getElementById("react-confirm-alert");
if (target) {
unmountComponentAtNode(target);
if (target.parentNode) {
target.parentNode.removeChild(target);
}
}
}
export function confirmAlert(properties: Props) {
createElementReconfirm(properties);
}
export default ConfirmAlert;

View File

@@ -1 +0,0 @@
export { default as ConfirmAlert } from "./ConfirmAlert";

View File

@@ -1,20 +0,0 @@
//@flow
import React from "react";
type Props = {
label: string,
action: () => void
};
class NavAction extends React.Component<Props> {
render() {
const { label, action } = this.props;
return (
<li>
<a onClick={action}>{label}</a>
</li>
);
}
}
export default NavAction;

View File

@@ -1,37 +0,0 @@
//@flow
import * as React from "react";
import { Route, Link } from "react-router-dom";
// TODO mostly copy of PrimaryNavigationLink
type Props = {
to: string,
label: string,
activeOnlyWhenExact?: boolean
};
class NavLink extends React.Component<Props> {
static defaultProps = {
activeOnlyWhenExact: true
};
renderLink = (route: any) => {
const { to, label } = this.props;
return (
<li>
<Link className={route.match ? "is-active" : ""} to={to}>
{label}
</Link>
</li>
);
};
render() {
const { to, activeOnlyWhenExact } = this.props;
return (
<Route path={to} exact={activeOnlyWhenExact} children={this.renderLink} />
);
}
}
export default NavLink;

View File

@@ -1,14 +0,0 @@
//@flow
import * as React from "react";
type Props = {
children?: React.Node
};
class Navigation extends React.Component<Props> {
render() {
return <aside className="menu">{this.props.children}</aside>;
}
}
export default Navigation;

View File

@@ -1,45 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import PrimaryNavigationLink from "./PrimaryNavigationLink";
type Props = {
t: string => string
};
class PrimaryNavigation extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<nav className="tabs is-boxed">
<ul>
<PrimaryNavigationLink
to="/repos"
match="/(repo|repos)"
label={t("primary-navigation.repositories")}
/>
<PrimaryNavigationLink
to="/users"
match="/(user|users)"
label={t("primary-navigation.users")}
/>
<PrimaryNavigationLink
to="/groups"
match="/(group|groups)"
label={t("primary-navigation.groups")}
/>
<PrimaryNavigationLink
to="/config"
label={t("primary-navigation.config")}
/>
<PrimaryNavigationLink
to="/logout"
label={t("primary-navigation.logout")}
/>
</ul>
</nav>
);
}
}
export default translate("commons")(PrimaryNavigation);

View File

@@ -1,35 +0,0 @@
//@flow
import * as React from "react";
import { Route, Link } from "react-router-dom";
type Props = {
to: string,
label: string,
match?: string,
activeOnlyWhenExact?: boolean
};
class PrimaryNavigationLink extends React.Component<Props> {
renderLink = (route: any) => {
const { to, label } = this.props;
return (
<li className={route.match ? "is-active" : ""}>
<Link to={to}>{label}</Link>
</li>
);
};
render() {
const { to, match, activeOnlyWhenExact } = this.props;
const path = match ? match : to;
return (
<Route
path={path}
exact={activeOnlyWhenExact}
children={this.renderLink}
/>
);
}
}
export default PrimaryNavigationLink;

View File

@@ -1,21 +0,0 @@
//@flow
import * as React from "react";
type Props = {
label: string,
children?: React.Node
};
class Section extends React.Component<Props> {
render() {
const { label, children } = this.props;
return (
<div>
<p className="menu-label">{label}</p>
<ul className="menu-list">{children}</ul>
</div>
);
}
}
export default Section;

View File

@@ -1,8 +0,0 @@
//primary Navigation
export { default as PrimaryNavigation } from "./PrimaryNavigation";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink";
//secondary Navigation
export { default as Navigation } from "./Navigation";
export { default as Section } from "./Section";
export { default as NavLink } from "./NavLink";
export { default as NavAction } from "./NavAction";

View File

@@ -1,16 +0,0 @@
// @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);
};
export const isNumberValid = (number: string) => {
return !isNaN(number);
};

View File

@@ -1,102 +0,0 @@
// @flow
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 ",
"",
" invalid_name",
"another%one",
"!!!",
"!_!"
];
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",
"valid_name",
"another1",
"stillValid",
"this.one_as-well",
"and@this"
];
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);
}
});
});
describe("test number validation", () => {
it("should return false", () => {
const invalid = ["1a", "35gu", "dj6", "45,5", "test"];
for (let number of invalid) {
expect(validator.isNumberValid(number)).toBe(false);
}
});
it("should return true", () => {
const valid = ["1", "35", "2", "235", "34.4"];
for (let number of valid) {
expect(validator.isNumberValid(number)).toBe(true);
}
});
});

View File

@@ -1,10 +1,9 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import Subtitle from "../../../components/layout/Subtitle";
import { Subtitle, AddEntryToTableField } from "@scm-manager/ui-components";
import AdminGroupTable from "../table/AdminGroupTable";
import AdminUserTable from "../table/AdminUserTable";
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
type Props = {
adminGroups: string[],

View File

@@ -1,8 +1,7 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "../../../components/forms/index";
import Subtitle from "../../../components/layout/Subtitle";
import { Checkbox, InputField, Subtitle } from "@scm-manager/ui-components";
type Props = {
baseUrl: string,

View File

@@ -1,13 +1,12 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { SubmitButton } from "../../../components/buttons/index";
import type { Config } from "../../types/Config";
import { SubmitButton, Notification } from "@scm-manager/ui-components";
import type { Config } from "@scm-manager/ui-types";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
import BaseUrlSettings from "./BaseUrlSettings";
import AdminSettings from "./AdminSettings";
import Notification from "../../../components/Notification";
import LoginAttempt from "./LoginAttempt";
type Props = {

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "../../../components/forms/index";
import { Checkbox, InputField } from "@scm-manager/ui-components";
type Props = {
realmDescription: string,

View File

@@ -1,9 +1,11 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { InputField } from "../../../components/forms/index";
import Subtitle from "../../../components/layout/Subtitle";
import * as validator from "../../../components/validation";
import {
InputField,
Subtitle,
validation as validator
} from "@scm-manager/ui-components";
type Props = {
loginAttemptLimit: number,

View File

@@ -1,10 +1,13 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "../../../components/forms/index";
import Subtitle from "../../../components/layout/Subtitle";
import {
Checkbox,
InputField,
Subtitle,
AddEntryToTableField
} from "@scm-manager/ui-components";
import ProxyExcludesTable from "../table/ProxyExcludesTable";
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
type Props = {
proxyPassword: string,

View File

@@ -1,6 +1,6 @@
//@flow
import React from "react";
import { RemoveEntryOfTableButton } from "../../../components/buttons";
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
type Props = {
items: string[],

View File

@@ -3,8 +3,7 @@ import React from "react";
import { translate } from "react-i18next";
import { Route } from "react-router";
import { Page } from "../../components/layout";
import { Navigation, NavLink, Section } from "../../components/navigation";
import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components";
import GlobalConfig from "./GlobalConfig";
import type { History } from "history";

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import Title from "../../components/layout/Title";
import { Title, ErrorPage, Loading } from "@scm-manager/ui-components";
import {
fetchConfig,
getFetchConfigFailure,
@@ -14,10 +14,8 @@ import {
modifyConfigReset
} from "../modules/config";
import { connect } from "react-redux";
import ErrorPage from "../../components/ErrorPage";
import type { Config } from "../types/Config";
import type { Config } from "@scm-manager/ui-types";
import ConfigForm from "../components/form/ConfigForm";
import Loading from "../../components/Loading";
type Props = {
loading: boolean,

View File

@@ -1,11 +1,11 @@
// @flow
import { apiClient } from "../../apiclient";
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../modules/types";
import type { Action } from "../../types/Action";
import type { Action } from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import { Dispatch } from "redux";
import type { Config } from "../types/Config";
import type { Config } from "@scm-manager/ui-types";
export const FETCH_CONFIG = "scm/config/FETCH_CONFIG";
export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`;

View File

@@ -1,27 +0,0 @@
//@flow
import type { Links } from "../../types/hal";
export type Config = {
proxyPassword: string | null,
proxyPort: number,
proxyServer: string,
proxyUser: string | null,
enableProxy: boolean,
realmDescription: string,
enableRepositoryArchive: boolean,
disableGroupingGrid: boolean,
dateFormat: string,
anonymousAccessEnabled: boolean,
adminGroups: string[],
adminUsers: string[],
baseUrl: string,
forceBaseUrl: boolean,
loginAttemptLimit: number,
proxyExcludes: string[],
skipFailedAuthenticators: boolean,
pluginUrl: string,
loginAttemptLimitTimeout: number,
enabledXsrfProtection: boolean,
defaultNamespaceStrategy: string,
_links: Links
};

View File

@@ -12,11 +12,14 @@ import {
getFetchMeFailure
} from "../modules/auth";
import { PrimaryNavigation } from "../components/navigation";
import Loading from "../components/Loading";
import ErrorPage from "../components/ErrorPage";
import { Footer, Header } from "../components/layout";
import type { Me } from "../types/Me";
import {
PrimaryNavigation,
Loading,
ErrorPage,
Footer,
Header
} from "@scm-manager/ui-components";
import type { Me } from "@scm-manager/ui-types";
type Props = {
me: Me,

View File

@@ -11,12 +11,13 @@ import {
} from "../modules/auth";
import { connect } from "react-redux";
import { InputField } from "../components/forms";
import { SubmitButton } from "../components/buttons";
import {
InputField,
SubmitButton,
ErrorNotification,
Image
} from "@scm-manager/ui-components";
import classNames from "classnames";
import ErrorNotification from "../components/ErrorNotification";
import Image from "../components/Image";
const styles = {
avatar: {

View File

@@ -10,8 +10,7 @@ import {
isLogoutPending,
getLogoutFailure
} from "../modules/auth";
import ErrorPage from "../components/ErrorPage";
import Loading from "../components/Loading";
import { Loading, ErrorPage } from "@scm-manager/ui-components";
type Props = {
authenticated: boolean,

View File

@@ -9,7 +9,7 @@ import Login from "../containers/Login";
import Logout from "../containers/Logout";
import { Switch } from "react-router-dom";
import ProtectedRoute from "../components/ProtectedRoute";
import { ProtectedRoute } from "@scm-manager/ui-components";
import AddUser from "../users/containers/AddUser";
import SingleUser from "../users/containers/SingleUser";
import RepositoryRoot from "../repos/containers/RepositoryRoot";

View File

@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import Loading from "./Loading";
import { apiClient } from "../apiclient";
import { apiClient, Loading } from "@scm-manager/ui-components";
type Props = {
children: React.Node

View File

@@ -1,14 +1,16 @@
//@flow
import React from "react";
import InputField from "../../components/forms/InputField";
import { SubmitButton } from "../../components/buttons";
import { translate } from "react-i18next";
import type { Group } from "../types/Group";
import {
InputField,
SubmitButton,
Textarea,
AddEntryToTableField
} from "@scm-manager/ui-components";
import type { Group } from "@scm-manager/ui-types";
import * as validator from "./groupValidation";
import MemberNameTable from "./MemberNameTable";
import Textarea from "../../components/forms/Textarea";
import AddEntryToTableField from "../../components/forms/AddEntryToTableField";
type Props = {
t: string => string,

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { RemoveEntryOfTableButton } from "../../components/buttons";
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
type Props = {
members: string[],

View File

@@ -1,7 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { CreateButton } from "../../../components/buttons";
import { CreateButton } from "@scm-manager/ui-components";
type Props = {
t: string => string

View File

@@ -1,5 +1,7 @@
// @flow
import { isNameValid } from "../../components/validation";
import { validation } from "@scm-manager/ui-components";
const isNameValid = validation.isNameValid;
export { isNameValid };

View File

@@ -1,9 +1,8 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { Group } from "../../types/Group";
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
import { NavAction } from "../../../components/navigation";
import type { Group } from "@scm-manager/ui-types";
import { NavAction, confirmAlert } from "@scm-manager/ui-components";
type Props = {
group: Group,

View File

@@ -4,8 +4,11 @@ import "../../../tests/enzyme";
import "../../../tests/i18n";
import DeleteGroupNavLink from "./DeleteGroupNavLink";
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
jest.mock("../../../components/modals/ConfirmAlert");
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
NavAction: require.requireActual("@scm-manager/ui-components").NavAction
}));
describe("DeleteGroupNavLink", () => {
it("should render nothing, if the delete link is missing", () => {
@@ -67,7 +70,7 @@ describe("DeleteGroupNavLink", () => {
const navLink = mount(
<DeleteGroupNavLink
group={group}
group={group}
confirmDialog={false}
deleteGroup={capture}
/>

View File

@@ -1,20 +1,18 @@
//@flow
import React from "react";
import NavLink from "../../../components/navigation/NavLink";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Group } from "../../types/Group";
import type { Group } from "@scm-manager/ui-types";
type Props = {
t: string => string,
editUrl: string,
group: Group
}
};
type State = {
}
type State = {};
class EditGroupNavLink extends React.Component<Props, State> {
render() {
const { t, editUrl } = this.props;
if (!this.isEditable()) {
@@ -25,7 +23,7 @@ class EditGroupNavLink extends React.Component<Props, State> {
isEditable = () => {
return this.props.group._links.update;
}
};
}
export default translate("groups")(EditGroupNavLink);
export default translate("groups")(EditGroupNavLink);

View File

@@ -1,9 +1,9 @@
//@flow
import React from "react";
import type { Group } from "../../types/Group";
import type { Group } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import GroupMember from "./GroupMember";
import DateFromNow from "../../../components/DateFromNow";
import { DateFromNow } from "@scm-manager/ui-components";
type Props = {
group: Group,

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { Member } from "../../types/Group";
import type { Member } from "@scm-manager/ui-types";
type Props = {
member: Member

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { Group } from "../../types/Group";
import type { Group } from "@scm-manager/ui-types";
type Props = {
group: Group

View File

@@ -2,7 +2,7 @@
import React from "react";
import { translate } from "react-i18next";
import GroupRow from "./GroupRow";
import type { Group } from "../../types/Group";
import type { Group } from "@scm-manager/ui-types";
type Props = {
t: string => string,

View File

@@ -1,12 +1,17 @@
//@flow
import React from "react";
import Page from "../../components/layout/Page";
import { Page } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import GroupForm from "../components/GroupForm";
import { connect } from "react-redux";
import { createGroup, isCreateGroupPending, getCreateGroupFailure, createGroupReset } from "../modules/groups";
import type { Group } from "../types/Group";
import {
createGroup,
isCreateGroupPending,
getCreateGroupFailure,
createGroupReset
} from "../modules/groups";
import type { Group } from "@scm-manager/ui-types";
import type { History } from "history";
type Props = {
@@ -15,22 +20,28 @@ type Props = {
history: History,
loading?: boolean,
error?: Error,
resetForm: () => void,
resetForm: () => void
};
type State = {};
class AddGroup extends React.Component<Props, State> {
componentDidMount() {
this.props.resetForm();
}
render() {
const { t, loading, error } = this.props;
return (
<Page title={t("add-group.title")} subtitle={t("add-group.subtitle")} error={error}>
<Page
title={t("add-group.title")}
subtitle={t("add-group.subtitle")}
error={error}
>
<div>
<GroupForm submitForm={group => this.createGroup(group)} loading={loading}/>
<GroupForm
submitForm={group => this.createGroup(group)}
loading={loading}
/>
</div>
</Page>
);
@@ -48,9 +59,9 @@ const mapDispatchToProps = dispatch => {
return {
createGroup: (group: Group, callback?: () => void) =>
dispatch(createGroup(group, callback)),
resetForm: () => {
dispatch(createGroupReset());
}
resetForm: () => {
dispatch(createGroupReset());
}
};
};

View File

@@ -5,9 +5,9 @@ import GroupForm from "../components/GroupForm";
import { modifyGroup, fetchGroup } from "../modules/groups";
import type { History } from "history";
import { withRouter } from "react-router-dom";
import type { Group } from "../types/Group";
import type { Group } from "@scm-manager/ui-types";
import { isModifyGroupPending, getModifyGroupFailure } from "../modules/groups";
import ErrorNotification from "../../components/ErrorNotification";
import { ErrorNotification } from "@scm-manager/ui-components";
type Props = {
group: Group,

View File

@@ -2,12 +2,11 @@
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { Group } from "../types/Group.js";
import type { PagedCollection } from "../../types/Collection";
import type { Group } from "@scm-manager/ui-types";
import type { PagedCollection } from "@scm-manager/ui-types";
import type { History } from "history";
import { Page } from "../../components/layout";
import { Page, Paginator } from "@scm-manager/ui-components";
import { GroupTable } from "./../components/table";
import Paginator from "../../components/Paginator";
import CreateGroupButton from "../components/buttons/CreateGroupButton";
import {

View File

@@ -1,11 +1,18 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import { Page } from "../../components/layout";
import {
Page,
ErrorPage,
Loading,
Navigation,
Section,
NavLink
} from "@scm-manager/ui-components";
import { Route } from "react-router";
import { Details } from "./../components/table";
import { DeleteGroupNavLink, EditGroupNavLink } from "./../components/navLinks";
import type { Group } from "../types/Group";
import type { Group } from "@scm-manager/ui-types";
import type { History } from "history";
import {
deleteGroup,
@@ -14,12 +21,9 @@ import {
isFetchGroupPending,
getFetchGroupFailure,
getDeleteGroupFailure,
isDeleteGroupPending,
isDeleteGroupPending
} from "../modules/groups";
import Loading from "../../components/Loading";
import { Navigation, Section, NavLink } from "../../components/navigation";
import ErrorPage from "../../components/ErrorPage";
import { translate } from "react-i18next";
import EditGroup from "./EditGroup";
@@ -86,8 +90,16 @@ class SingleGroup extends React.Component<Props> {
<Page title={group.name}>
<div className="columns">
<div className="column is-three-quarters">
<Route path={url} exact component={() => <Details group={group} />} />
<Route path={`${url}/edit`} exact component={() => <EditGroup group={group} />} />
<Route
path={url}
exact
component={() => <Details group={group} />}
/>
<Route
path={`${url}/edit`}
exact
component={() => <EditGroup group={group} />}
/>
</div>
<div className="column">
<Navigation>
@@ -98,8 +110,11 @@ class SingleGroup extends React.Component<Props> {
/>
</Section>
<Section label={t("single-group.actions-label")}>
<DeleteGroupNavLink group={group} deleteGroup={this.deleteGroup} />
<EditGroupNavLink group={group} editUrl={`${url}/edit`}/>
<DeleteGroupNavLink
group={group}
deleteGroup={this.deleteGroup}
/>
<EditGroupNavLink group={group} editUrl={`${url}/edit`} />
<NavLink to="/groups" label={t("single-group.back-label")} />
</Section>
</Navigation>

View File

@@ -1,12 +1,10 @@
// @flow
import { apiClient } from "../../apiclient";
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import * as types from "../../modules/types";
import { combineReducers, Dispatch } from "redux";
import type { Action } from "../../types/Action";
import type { PagedCollection } from "../../types/Collection";
import type { Group } from "../types/Group";
import type { Action, PagedCollection, Group } from "@scm-manager/ui-types";
export const FETCH_GROUPS = "scm/groups/FETCH_GROUPS";
export const FETCH_GROUPS_PENDING = `${FETCH_GROUPS}_${types.PENDING_SUFFIX}`;

View File

@@ -1,20 +0,0 @@
//@flow
import type { Collection } from "../../types/Collection";
import type { Links } from "../../types/hal";
export type Member = {
name: string,
_links: Links
};
export type Group = Collection & {
name: string,
description: string,
type: string,
members: string[],
_embedded: {
members: Member[]
},
creationDate?: string,
lastModified?: string
};

View File

@@ -2,9 +2,9 @@ import i18n from "i18next";
import Backend from "i18next-fetch-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { reactI18nextModule } from "react-i18next";
import { withContextPath } from "./urls";
import { urls } from "@scm-manager/ui-components";
const loadPath = withContextPath("/locales/{{lng}}/{{ns}}.json");
const loadPath = urls.withContextPath("/locales/{{lng}}/{{ns}}.json");
// TODO load locales for moment

View File

@@ -14,13 +14,13 @@ import type { BrowserHistory } from "history/createBrowserHistory";
import createReduxStore from "./createReduxStore";
import { ConnectedRouter } from "react-router-redux";
import PluginLoader from "./components/PluginLoader";
import PluginLoader from "./containers/PluginLoader";
import { contextPath } from "./urls";
import { urls } from "@scm-manager/ui-components";
// Create a history of your choosing (we're using a browser history in this case)
const history: BrowserHistory = createHistory({
basename: contextPath
basename: urls.contextPath
});
// Add the reducer to your store on the `router` key

View File

@@ -1,8 +1,8 @@
// @flow
import type { Me } from "../types/Me";
import type { Me } from "@scm-manager/ui-components";
import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "../apiclient";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
import { isPending } from "./pending";
import { getFailure } from "./failure";

View File

@@ -1,5 +1,5 @@
// @flow
import type { Action } from "../types/Action";
import type { Action } from "@scm-manager/ui-types";
const FAILURE_SUFFIX = "_FAILURE";
const RESET_PATTERN = /^(.*)_(SUCCESS|RESET)$/;

View File

@@ -1,5 +1,5 @@
// @flow
import type { Action } from "../types/Action";
import type { Action } from "@scm-manager/ui-types";
import * as types from "./types";
const PENDING_SUFFIX = "_" + types.PENDING_SUFFIX;

View File

@@ -1,13 +1,16 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "../../components/forms";
import {
Checkbox,
InputField,
SubmitButton
} from "@scm-manager/ui-components";
import TypeSelector from "./TypeSelector";
import type {
PermissionCollection,
PermissionEntry
} from "../types/Permissions";
import { SubmitButton } from "../../components/buttons";
type Props = {
t: string => string,

View File

@@ -1,7 +1,9 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Select } from "../../components/forms";
import {
Select
} from "@scm-manager/ui-components";
type Props = {
t: string => string,

View File

@@ -2,8 +2,10 @@
import React from "react";
import { translate } from "react-i18next";
import type { Permission } from "../../types/Permissions";
import { confirmAlert } from "../../../components/modals/ConfirmAlert";
import { DeleteButton } from "../../../components/buttons/index";
import {
confirmAlert,
DeleteButton
} from "@scm-manager/ui-components";
type Props = {
permission: Permission,

View File

@@ -14,8 +14,10 @@ import {
createPermissionReset,
getDeletePermissionsFailure
} from "../modules/permissions";
import Loading from "../../components/Loading";
import ErrorPage from "../../components/ErrorPage";
import {
Loading,
ErrorPage
} from "@scm-manager/ui-components";
import type {
Permission,
PermissionCollection,

View File

@@ -1,7 +1,6 @@
// @flow
import React from "react";
import type { Permission } from "../types/Permissions";
import { Checkbox } from "../../components/forms/index";
import { translate } from "react-i18next";
import {
modifyPermission,
@@ -14,9 +13,11 @@ import {
deletePermissionReset
} from "../modules/permissions";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
import ErrorNotification from "../../components/ErrorNotification";
import {
ErrorNotification,
Checkbox
} from "@scm-manager/ui-components";
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import TypeSelector from "../components/TypeSelector";

View File

@@ -1,7 +1,10 @@
// @flow
import { apiClient } from "../../apiclient";
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../modules/types";
import type { Action } from "../../types/Action";
import type {
Action
} from "@scm-manager/ui-types";
import type {
PermissionCollection,
Permission,

View File

@@ -1,9 +1,8 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { confirmAlert } from "../../components/modals/ConfirmAlert";
import { NavAction } from "../../components/navigation";
import type { Repository } from "../types/Repositories";
import { NavAction, confirmAlert } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
repository: Repository,

View File

@@ -4,8 +4,11 @@ import "../../tests/enzyme";
import "../../tests/i18n";
import DeleteNavAction from "./DeleteNavAction";
import { confirmAlert } from "../../components/modals/ConfirmAlert";
jest.mock("../../components/modals/ConfirmAlert");
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
NavAction: require.requireActual("@scm-manager/ui-components").NavAction
}));
describe("DeleteNavAction", () => {
it("should render nothing, if the delete link is missing", () => {

View File

@@ -1,8 +1,8 @@
//@flow
import React from "react";
import { NavLink } from "../../components/navigation";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Repository } from "../types/Repositories";
import type { Repository } from "@scm-manager/ui-types";
type Props = { editUrl: string, t: string => string, repository: Repository };

View File

@@ -1,19 +1,22 @@
import React from "react";
import { mount, shallow } from "enzyme";
import { shallow, mount } from "enzyme";
import "../../tests/enzyme";
import "../../tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import EditNavLink from "./EditNavLink";
jest.mock("../../components/modals/ConfirmAlert");
jest.mock("../../components/navigation/NavLink", () => () => <div>foo</div>);
describe("EditNavLink", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {}
};
const navLink = shallow(<EditNavLink repository={repository} editUrl="" />);
const navLink = shallow(
<EditNavLink repository={repository} editUrl="" />,
options.get()
);
expect(navLink.text()).toBe("");
});
@@ -26,7 +29,10 @@ describe("EditNavLink", () => {
}
};
const navLink = mount(<EditNavLink repository={repository} editUrl="" />);
expect(navLink.text()).toBe("foo");
const navLink = mount(
<EditNavLink repository={repository} editUrl="" />,
options.get()
);
expect(navLink.text()).toBe("edit-nav-link.label");
});
});

View File

@@ -1,10 +1,14 @@
//@flow
import React from "react";
import { NavLink } from "../../components/navigation";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Repository } from "../types/Repositories";
import type { Repository } from "@scm-manager/ui-types";
type Props = { permissionUrl: string, t: string => string, repository: Repository };
type Props = {
permissionUrl: string,
t: string => string,
repository: Repository
};
class PermissionsNavLink extends React.Component<Props> {
hasPermissionsLink = () => {
@@ -15,8 +19,9 @@ class PermissionsNavLink extends React.Component<Props> {
return null;
}
const { permissionUrl, t } = this.props;
return <NavLink to={permissionUrl} label={t("repository-root.permissions")}
/>;
return (
<NavLink to={permissionUrl} label={t("repository-root.permissions")} />
);
}
}

View File

@@ -1,8 +1,7 @@
//@flow
import React from "react";
import type { Repository } from "../types/Repositories";
import MailLink from "../../components/MailLink";
import DateFromNow from "../../components/DateFromNow";
import type { Repository } from "@scm-manager/ui-types";
import { MailLink, DateFromNow } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {

View File

@@ -1,6 +1,6 @@
//@flow
import React from "react";
import type { Repository } from "../types/Repositories";
import type { Repository } from "@scm-manager/ui-types";
import RepositoryDetailTable from "./RepositoryDetailTable";
import { ExtensionPoint } from "@scm-manager/ui-extensions";

View File

@@ -1,12 +1,14 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { InputField, Select } from "../../../components/forms/index";
import { SubmitButton } from "../../../components/buttons/index";
import type { Repository } from "../../types/Repositories";
import {
InputField,
Select,
SubmitButton,
Textarea
} from "@scm-manager/ui-components";
import type { Repository, RepositoryType } from "@scm-manager/ui-types";
import * as validator from "./repositoryValidation";
import type { RepositoryType } from "../../types/RepositoryTypes";
import Textarea from "../../../components/forms/Textarea";
type Props = {
submitForm: Repository => void,

View File

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

View File

@@ -1,8 +1,8 @@
//@flow
import React from "react";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Repository } from "../../types/Repositories";
import Image from "../../../components/Image";
import type { Repository } from "@scm-manager/ui-types";
import { Image } from "@scm-manager/ui-components";
type Props = {
repository: Repository

View File

@@ -2,8 +2,8 @@
import React from "react";
import { Link } from "react-router-dom";
import injectSheet from "react-jss";
import type { Repository } from "../../types/Repositories";
import DateFromNow from "../../../components/DateFromNow";
import type { Repository } from "@scm-manager/ui-types";
import { DateFromNow } from "@scm-manager/ui-components";
import RepositoryEntryLink from "./RepositoryEntryLink";
import classNames from "classnames";
import RepositoryAvatar from "./RepositoryAvatar";

Some files were not shown because too many files have changed in this diff Show More