mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-23 16:59:48 +01:00
restructure scm-ui and use ui-components and ui-types
This commit is contained in:
32
scm-ui-components/packages/ui-components/src/DateFromNow.js
Normal file
32
scm-ui-components/packages/ui-components/src/DateFromNow.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@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);
|
||||
@@ -0,0 +1,25 @@
|
||||
//@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);
|
||||
27
scm-ui-components/packages/ui-components/src/ErrorPage.js
Normal file
27
scm-ui-components/packages/ui-components/src/ErrorPage.js
Normal file
@@ -0,0 +1,27 @@
|
||||
//@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;
|
||||
18
scm-ui-components/packages/ui-components/src/Image.js
Normal file
18
scm-ui-components/packages/ui-components/src/Image.js
Normal file
@@ -0,0 +1,18 @@
|
||||
//@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;
|
||||
51
scm-ui-components/packages/ui-components/src/Loading.js
Normal file
51
scm-ui-components/packages/ui-components/src/Loading.js
Normal file
@@ -0,0 +1,51 @@
|
||||
//@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));
|
||||
17
scm-ui-components/packages/ui-components/src/Logo.js
Normal file
17
scm-ui-components/packages/ui-components/src/Logo.js
Normal file
@@ -0,0 +1,17 @@
|
||||
//@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);
|
||||
18
scm-ui-components/packages/ui-components/src/MailLink.js
Normal file
18
scm-ui-components/packages/ui-components/src/MailLink.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// @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;
|
||||
37
scm-ui-components/packages/ui-components/src/Notification.js
Normal file
37
scm-ui-components/packages/ui-components/src/Notification.js
Normal file
@@ -0,0 +1,37 @@
|
||||
//@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;
|
||||
124
scm-ui-components/packages/ui-components/src/Paginator.js
Normal file
124
scm-ui-components/packages/ui-components/src/Paginator.js
Normal file
@@ -0,0 +1,124 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { PagedCollection } from "@scm-manager/ui-types";
|
||||
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;
|
||||
if (onPageChange) {
|
||||
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">…</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);
|
||||
253
scm-ui-components/packages/ui-components/src/Paginator.test.js
Normal file
253
scm-ui-components/packages/ui-components/src/Paginator.test.js
Normal file
@@ -0,0 +1,253 @@
|
||||
// @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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
//@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);
|
||||
77
scm-ui-components/packages/ui-components/src/apiclient.js
Normal file
77
scm-ui-components/packages/ui-components/src/apiclient.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// @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();
|
||||
@@ -0,0 +1,15 @@
|
||||
// @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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,69 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,24 @@
|
||||
//@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);
|
||||
@@ -0,0 +1,11 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,11 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,33 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,11 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,10 @@
|
||||
// @create-index
|
||||
|
||||
export { default as AddButton } from "./AddButton.js";
|
||||
export { default as Button } from "./Button.js";
|
||||
export { default as CreateButton } from "./CreateButton.js";
|
||||
export { default as DeleteButton } from "./DeleteButton.js";
|
||||
export { default as EditButton } from "./EditButton.js";
|
||||
export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton.js";
|
||||
export { default as SubmitButton } from "./SubmitButton.js";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,36 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,94 @@
|
||||
//@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;
|
||||
67
scm-ui-components/packages/ui-components/src/forms/Select.js
Normal file
67
scm-ui-components/packages/ui-components/src/forms/Select.js
Normal file
@@ -0,0 +1,67 @@
|
||||
//@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;
|
||||
|
||||
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 } = 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;
|
||||
@@ -0,0 +1,53 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,8 @@
|
||||
// @create-index
|
||||
|
||||
export { default as AddEntryToTableField } from "./AddEntryToTableField.js";
|
||||
export { default as Checkbox } from "./Checkbox.js";
|
||||
export { default as InputField } from "./InputField.js";
|
||||
export { default as Select } from "./Select.js";
|
||||
export { default as Textarea } from "./Textarea.js";
|
||||
|
||||
25
scm-ui-components/packages/ui-components/src/index.js
Normal file
25
scm-ui-components/packages/ui-components/src/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @create-index
|
||||
|
||||
import * as validation from "./validation.js";
|
||||
import * as urls from "./urls";
|
||||
|
||||
export { validation, urls };
|
||||
|
||||
export { default as DateFromNow } from "./DateFromNow.js";
|
||||
export { default as ErrorNotification } from "./ErrorNotification.js";
|
||||
export { default as ErrorPage } from "./ErrorPage.js";
|
||||
export { default as Image } from "./Image.js";
|
||||
export { default as Loading } from "./Loading.js";
|
||||
export { default as Logo } from "./Logo.js";
|
||||
export { default as MailLink } from "./MailLink.js";
|
||||
export { default as Notification } from "./Notification.js";
|
||||
export { default as Paginator } from "./Paginator.js";
|
||||
export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
|
||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||
|
||||
export * from "./buttons";
|
||||
export * from "./forms";
|
||||
export * from "./layout";
|
||||
export * from "./modals";
|
||||
export * from "./navigation";
|
||||
@@ -0,0 +1,25 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,31 @@
|
||||
//@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;
|
||||
44
scm-ui-components/packages/ui-components/src/layout/Page.js
Normal file
44
scm-ui-components/packages/ui-components/src/layout/Page.js
Normal file
@@ -0,0 +1,44 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,18 @@
|
||||
// @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;
|
||||
18
scm-ui-components/packages/ui-components/src/layout/Title.js
Normal file
18
scm-ui-components/packages/ui-components/src/layout/Title.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// @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;
|
||||
@@ -0,0 +1,8 @@
|
||||
// @create-index
|
||||
|
||||
export { default as Footer } from "./Footer.js";
|
||||
export { default as Header } from "./Header.js";
|
||||
export { default as Page } from "./Page.js";
|
||||
export { default as Subtitle } from "./Subtitle.js";
|
||||
export { default as Title } from "./Title.js";
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// @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;
|
||||
@@ -0,0 +1,4 @@
|
||||
// @create-index
|
||||
|
||||
export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert.js";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,37 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,14 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,45 @@
|
||||
//@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);
|
||||
@@ -0,0 +1,35 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,21 @@
|
||||
//@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;
|
||||
@@ -0,0 +1,9 @@
|
||||
// @create-index
|
||||
|
||||
export { default as NavAction } from "./NavAction.js";
|
||||
export { default as NavLink } from "./NavLink.js";
|
||||
export { default as Navigation } from "./Navigation.js";
|
||||
export { default as PrimaryNavigation } from "./PrimaryNavigation.js";
|
||||
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js";
|
||||
export { default as Section } from "./Section.js";
|
||||
|
||||
12
scm-ui-components/packages/ui-components/src/tests/enzyme.js
Normal file
12
scm-ui-components/packages/ui-components/src/tests/enzyme.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import "raf/polyfill";
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
// Temporary hack to suppress error
|
||||
// https://github.com/facebook/create-react-app/issues/3199#issuecomment-345024029
|
||||
window.requestAnimationFrame = function(callback) {
|
||||
setTimeout(callback, 0);
|
||||
return 0;
|
||||
};
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
@@ -0,0 +1,7 @@
|
||||
jest.mock("react-i18next", () => ({
|
||||
// this mock makes sure any components using the translate HoC receive the t function as a prop
|
||||
translate: () => Component => {
|
||||
Component.defaultProps = { ...Component.defaultProps, t: key => key };
|
||||
return Component;
|
||||
}
|
||||
}));
|
||||
6
scm-ui-components/packages/ui-components/src/urls.js
Normal file
6
scm-ui-components/packages/ui-components/src/urls.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
export const contextPath = window.ctxPath || "";
|
||||
|
||||
export function withContextPath(path: string) {
|
||||
return contextPath + path;
|
||||
}
|
||||
16
scm-ui-components/packages/ui-components/src/validation.js
Normal file
16
scm-ui-components/packages/ui-components/src/validation.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// @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);
|
||||
};
|
||||
102
scm-ui-components/packages/ui-components/src/validation.test.js
Normal file
102
scm-ui-components/packages/ui-components/src/validation.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// @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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user