use reflow to migrate from flow to typescript

This commit is contained in:
Sebastian Sdorra
2019-10-19 16:38:07 +02:00
parent f7b8050dfa
commit 6e7a08a3bb
495 changed files with 14239 additions and 13766 deletions

View File

@@ -12,6 +12,7 @@
"deploy": "ui-scripts publish" "deploy": "ui-scripts publish"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-reflow": "^0.2.7",
"lerna": "^3.17.0" "lerna": "^3.17.0"
}, },
"resolutions": { "resolutions": {

View File

@@ -2,7 +2,8 @@ module.exports = () => ({
presets: [ presets: [
require("@babel/preset-env"), require("@babel/preset-env"),
require("@babel/preset-flow"), require("@babel/preset-flow"),
require("@babel/preset-react") require("@babel/preset-react"),
require("@babel/preset-typescript")
], ],
plugins: [ plugins: [
require("@babel/plugin-proposal-class-properties"), require("@babel/plugin-proposal-class-properties"),

View File

@@ -13,7 +13,8 @@
"@babel/plugin-proposal-optional-chaining": "^7.6.0", "@babel/plugin-proposal-optional-chaining": "^7.6.0",
"@babel/preset-env": "^7.6.3", "@babel/preset-env": "^7.6.3",
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.6.3" "@babel/preset-react": "^7.6.3",
"@babel/preset-typescript": "^7.6.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -24,7 +24,7 @@ module.exports = {
path.join(root, "src") path.join(root, "src")
], ],
transform: { transform: {
"^.+\\.js$": "@scm-manager/jest-preset" "^.+\\.(ts|tsx|js)$": "@scm-manager/jest-preset"
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
"node_modules/(?!(@scm-manager)/)" "node_modules/(?!(@scm-manager)/)"
@@ -39,7 +39,7 @@ module.exports = {
setupFiles: [path.resolve(__dirname, "src", "setup.js")], setupFiles: [path.resolve(__dirname, "src", "setup.js")],
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [
"src/**/*.{js,jsx}" "src/**/*.{ts,tsx,js,jsx}"
], ],
coverageDirectory: path.join(reportDirectory, "coverage-" + name), coverageDirectory: path.join(reportDirectory, "coverage-" + name),
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [

View File

@@ -25,4 +25,4 @@ addDecorator(
}) })
); );
configure(require.context("../src", true, /\.stories\.js$/), module); configure(require.context("../src", true, /\.stories\.tsx?$/), module);

View File

@@ -2,7 +2,7 @@
"name": "@scm-manager/ui-components", "name": "@scm-manager/ui-components",
"version": "2.0.0-SNAPSHOT", "version": "2.0.0-SNAPSHOT",
"description": "UI Components for SCM-Manager and its plugins", "description": "UI Components for SCM-Manager and its plugins",
"main": "src/index.js", "main": "src/index.ts",
"files": [ "files": [
"dist", "dist",
"src" "src"
@@ -13,8 +13,8 @@
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"storybook": "start-storybook -s ../ui-webapp/public", "storybook": "start-storybook -s ../ui-webapp/public",
"storyshots": "jest --testPathPattern=\"storyshots.test.js\" --collectCoverage=false", "storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false",
"update-storyshots": "jest --testPathPattern=\"storyshots.test.js\" --collectCoverage=false -u" "update-storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false -u"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT", "@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",

View File

@@ -1,28 +1,27 @@
// @flow import React from 'react';
import React from "react"; import { Async, AsyncCreatable } from 'react-select';
import {Async, AsyncCreatable} from "react-select"; import { AutocompleteObject, SelectValue } from '@scm-manager/ui-types';
import type {AutocompleteObject, SelectValue} from "@scm-manager/ui-types"; import LabelWithHelpIcon from './forms/LabelWithHelpIcon';
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
type Props = { type Props = {
loadSuggestions: string => Promise<AutocompleteObject>, loadSuggestions: (p: string) => Promise<AutocompleteObject>;
valueSelected: SelectValue => void, valueSelected: (p: SelectValue) => void;
label: string, label: string;
helpText?: string, helpText?: string;
value?: SelectValue, value?: SelectValue;
placeholder: string, placeholder: string;
loadingMessage: string, loadingMessage: string;
noOptionsMessage: string, noOptionsMessage: string;
creatable?: boolean creatable?: boolean;
}; };
type State = {}; type State = {};
class Autocomplete extends React.Component<Props, State> { class Autocomplete extends React.Component<Props, State> {
static defaultProps = { static defaultProps = {
placeholder: "Type here", placeholder: 'Type here',
loadingMessage: "Loading...", loadingMessage: 'Loading...',
noOptionsMessage: "No suggestion available" noOptionsMessage: 'No suggestion available',
}; };
handleInputChange = (newValue: SelectValue) => { handleInputChange = (newValue: SelectValue) => {
@@ -33,12 +32,12 @@ class Autocomplete extends React.Component<Props, State> {
isValidNewOption = ( isValidNewOption = (
inputValue: string, inputValue: string,
selectValue: SelectValue, selectValue: SelectValue,
selectOptions: SelectValue[] selectOptions: SelectValue[],
) => { ) => {
const isNotDuplicated = !selectOptions const isNotDuplicated = !selectOptions
.map(option => option.label) .map(option => option.label)
.includes(inputValue); .includes(inputValue);
const isNotEmpty = inputValue !== ""; const isNotEmpty = inputValue !== '';
return isNotEmpty && isNotDuplicated; return isNotEmpty && isNotDuplicated;
}; };
@@ -51,7 +50,7 @@ class Autocomplete extends React.Component<Props, State> {
loadingMessage, loadingMessage,
noOptionsMessage, noOptionsMessage,
loadSuggestions, loadSuggestions,
creatable creatable,
} = this.props; } = this.props;
return ( return (
<div className="field"> <div className="field">
@@ -70,7 +69,10 @@ class Autocomplete extends React.Component<Props, State> {
onCreateOption={value => { onCreateOption={value => {
this.handleInputChange({ this.handleInputChange({
label: value, label: value,
value: { id: value, displayName: value } value: {
id: value,
displayName: value,
},
}); });
}} }}
/> />

View File

@@ -1,11 +1,13 @@
// @flow import React from 'react';
import React from "react"; import { BackendError } from './errors';
import { BackendError } from "./errors"; import Notification from './Notification';
import Notification from "./Notification";
import { translate } from "react-i18next"; import { translate } from 'react-i18next';
type Props = { error: BackendError, t: string => string }; type Props = {
error: BackendError;
t: (p: string) => string;
};
class BackendErrorNotification extends React.Component<Props> { class BackendErrorNotification extends React.Component<Props> {
constructor(props: Props) { constructor(props: Props) {
@@ -27,7 +29,7 @@ class BackendErrorNotification extends React.Component<Props> {
renderErrorName = () => { renderErrorName = () => {
const { error, t } = this.props; const { error, t } = this.props;
const translation = t("errors." + error.errorCode + ".displayName"); const translation = t('errors.' + error.errorCode + '.displayName');
if (translation === error.errorCode) { if (translation === error.errorCode) {
return error.message; return error.message;
} }
@@ -36,9 +38,9 @@ class BackendErrorNotification extends React.Component<Props> {
renderErrorDescription = () => { renderErrorDescription = () => {
const { error, t } = this.props; const { error, t } = this.props;
const translation = t("errors." + error.errorCode + ".description"); const translation = t('errors.' + error.errorCode + '.description');
if (translation === error.errorCode) { if (translation === error.errorCode) {
return ""; return '';
} }
return translation; return translation;
}; };
@@ -49,7 +51,7 @@ class BackendErrorNotification extends React.Component<Props> {
return ( return (
<> <>
<p> <p>
<strong>{t("errors.violations")}</strong> <strong>{t('errors.violations')}</strong>
</p> </p>
<ul> <ul>
{error.violations.map((violation, index) => { {error.violations.map((violation, index) => {
@@ -73,10 +75,10 @@ class BackendErrorNotification extends React.Component<Props> {
{this.renderMoreInformationLink()} {this.renderMoreInformationLink()}
<div className="level is-size-7"> <div className="level is-size-7">
<div className="left"> <div className="left">
{t("errors.transactionId")} {error.transactionId} {t('errors.transactionId')} {error.transactionId}
</div> </div>
<div className="right"> <div className="right">
{t("errors.errorCode")} {error.errorCode} {t('errors.errorCode')} {error.errorCode}
</div> </div>
</div> </div>
</> </>
@@ -84,12 +86,12 @@ class BackendErrorNotification extends React.Component<Props> {
}; };
renderContext = () => { renderContext = () => {
const { error, t} = this.props; const { error, t } = this.props;
if (error.context) { if (error.context) {
return ( return (
<> <>
<p> <p>
<strong>{t("errors.context")}</strong> <strong>{t('errors.context')}</strong>
</p> </p>
<ul> <ul>
{error.context.map((context, index) => { {error.context.map((context, index) => {
@@ -110,7 +112,7 @@ class BackendErrorNotification extends React.Component<Props> {
if (error.url) { if (error.url) {
return ( return (
<p> <p>
{t("errors.moreInfo")}{" "} {t('errors.moreInfo')}{' '}
<a href={error.url} target="_blank"> <a href={error.url} target="_blank">
{error.errorCode} {error.errorCode}
</a> </a>
@@ -120,4 +122,4 @@ class BackendErrorNotification extends React.Component<Props> {
}; };
} }
export default translate("plugins")(BackendErrorNotification); export default translate('plugins')(BackendErrorNotification);

View File

@@ -1,19 +1,20 @@
//@flow import React from 'react';
import React from "react"; import classNames from 'classnames';
import classNames from "classnames"; import styled from 'styled-components';
import styled from "styled-components"; import { Branch } from '@scm-manager/ui-types';
import type { Branch } from "@scm-manager/ui-types"; import DropDown from './forms/DropDown';
import DropDown from "./forms/DropDown";
type Props = { type Props = {
branches: Branch[], branches: Branch[];
selected: (branch?: Branch) => void, selected: (branch?: Branch) => void;
selectedBranch?: string, selectedBranch?: string;
label: string, label: string;
disabled?: boolean disabled?: boolean;
}; };
type State = { selectedBranch?: Branch }; type State = {
selectedBranch?: Branch;
};
const ZeroflexFieldLabel = styled.div` const ZeroflexFieldLabel = styled.div`
flex-basis: inherit; flex-basis: inherit;
@@ -38,9 +39,11 @@ export default class BranchSelector extends React.Component<Props, State> {
const { branches } = this.props; const { branches } = this.props;
if (branches) { if (branches) {
const selectedBranch = branches.find( const selectedBranch = branches.find(
branch => branch.name === this.props.selectedBranch branch => branch.name === this.props.selectedBranch,
); );
this.setState({ selectedBranch }); this.setState({
selectedBranch,
});
} }
} }
@@ -49,14 +52,14 @@ export default class BranchSelector extends React.Component<Props, State> {
if (branches) { if (branches) {
return ( return (
<div className={classNames("field", "is-horizontal")}> <div className={classNames('field', 'is-horizontal')}>
<ZeroflexFieldLabel <ZeroflexFieldLabel
className={classNames("field-label", "is-normal")} className={classNames('field-label', 'is-normal')}
> >
<label className={classNames("label", "is-size-6")}>{label}</label> <label className={classNames('label', 'is-size-6')}>{label}</label>
</ZeroflexFieldLabel> </ZeroflexFieldLabel>
<div className="field-body"> <div className="field-body">
<NoBottomMarginField className={classNames("field", "is-narrow")}> <NoBottomMarginField className={classNames('field', 'is-narrow')}>
<MinWidthControl className="control"> <MinWidthControl className="control">
<DropDown <DropDown
className="is-fullwidth" className="is-fullwidth"
@@ -66,7 +69,7 @@ export default class BranchSelector extends React.Component<Props, State> {
preselectedOption={ preselectedOption={
this.state.selectedBranch this.state.selectedBranch
? this.state.selectedBranch.name ? this.state.selectedBranch.name
: "" : ''
} }
/> />
</MinWidthControl> </MinWidthControl>
@@ -83,13 +86,17 @@ export default class BranchSelector extends React.Component<Props, State> {
const { branches, selected } = this.props; const { branches, selected } = this.props;
if (!branchName) { if (!branchName) {
this.setState({ selectedBranch: undefined }); this.setState({
selectedBranch: undefined,
});
selected(undefined); selected(undefined);
return; return;
} }
const branch = branches.find(b => b.name === branchName); const branch = branches.find(b => b.name === branchName);
selected(branch); selected(branch);
this.setState({ selectedBranch: branch }); this.setState({
selectedBranch: branch,
});
}; };
} }

View File

@@ -1,24 +1,23 @@
//@flow import React from 'react';
import React from "react"; import { Link } from 'react-router-dom';
import { Link } from "react-router-dom"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import classNames from 'classnames';
import classNames from "classnames"; import styled from 'styled-components';
import styled from "styled-components"; import { binder, ExtensionPoint } from '@scm-manager/ui-extensions';
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { Branch, Repository } from '@scm-manager/ui-types';
import type { Branch, Repository } from "@scm-manager/ui-types"; import Icon from './Icon';
import Icon from "./Icon";
type Props = { type Props = {
repository: Repository, repository: Repository;
branch: Branch, branch: Branch;
defaultBranch: Branch, defaultBranch: Branch;
branches: Branch[], branches: Branch[];
revision: string, revision: string;
path: string, path: string;
baseUrl: string, baseUrl: string;
// Context props // Context props
t: string => string t: (p: string) => string;
}; };
const FlexStartNav = styled.nav` const FlexStartNav = styled.nav`
@@ -39,9 +38,9 @@ class Breadcrumb extends React.Component<Props> {
const { revision, path, baseUrl } = this.props; const { revision, path, baseUrl } = this.props;
if (path) { if (path) {
const paths = path.split("/"); const paths = path.split('/');
const map = paths.map((path, index) => { const map = paths.map((path, index) => {
const currPath = paths.slice(0, index + 1).join("/"); const currPath = paths.slice(0, index + 1).join('/');
if (paths.length - 1 === index) { if (paths.length - 1 === index) {
return ( return (
<li className="is-active" key={index}> <li className="is-active" key={index}>
@@ -53,7 +52,7 @@ class Breadcrumb extends React.Component<Props> {
} }
return ( return (
<li key={index}> <li key={index}>
<Link to={baseUrl + "/" + revision + "/" + currPath}>{path}</Link> <Link to={baseUrl + '/' + revision + '/' + currPath}>{path}</Link>
</li> </li>
); );
}); });
@@ -71,21 +70,21 @@ class Breadcrumb extends React.Component<Props> {
revision, revision,
path, path,
repository, repository,
t t,
} = this.props; } = this.props;
return ( return (
<> <>
<div className="is-flex"> <div className="is-flex">
<FlexStartNav <FlexStartNav
className={classNames("breadcrumb", "sources-breadcrumb")} className={classNames('breadcrumb', 'sources-breadcrumb')}
aria-label="breadcrumbs" aria-label="breadcrumbs"
> >
<ul> <ul>
<li> <li>
<Link to={baseUrl + "/" + revision + "/"}> <Link to={baseUrl + '/' + revision + '/'}>
<HomeIcon <HomeIcon
title={t("breadcrumb.home")} title={t('breadcrumb.home')}
name="home" name="home"
color="inherit" color="inherit"
/> />
@@ -94,7 +93,7 @@ class Breadcrumb extends React.Component<Props> {
{this.renderPath()} {this.renderPath()}
</ul> </ul>
</FlexStartNav> </FlexStartNav>
{binder.hasExtension("repos.sources.actionbar") && ( {binder.hasExtension('repos.sources.actionbar') && (
<ActionWrapper> <ActionWrapper>
<ExtensionPoint <ExtensionPoint
name="repos.sources.actionbar" name="repos.sources.actionbar"
@@ -105,9 +104,9 @@ class Breadcrumb extends React.Component<Props> {
isBranchUrl: isBranchUrl:
branches && branches &&
branches.filter( branches.filter(
b => b.name.replace("/", "%2F") === revision b => b.name.replace('/', '%2F') === revision,
).length > 0, ).length > 0,
repository repository,
}} }}
renderAll={true} renderAll={true}
/> />
@@ -120,4 +119,4 @@ class Breadcrumb extends React.Component<Props> {
} }
} }
export default translate("commons")(Breadcrumb); export default translate('commons')(Breadcrumb);

View File

@@ -1,19 +1,18 @@
//@flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames"; import styled from 'styled-components';
import styled from "styled-components"; import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
type Props = { type Props = {
title: string, title: string;
description: string, description: string;
avatar: React.Node, avatar: React.Node;
contentRight?: React.Node, contentRight?: React.Node;
footerLeft: React.Node, footerLeft: React.Node;
footerRight: React.Node, footerRight: React.Node;
link?: string, link?: string;
action?: () => void, action?: () => void;
className?: string className?: string;
}; };
const NoEventWrapper = styled.article` const NoEventWrapper = styled.article`
@@ -74,16 +73,16 @@ export default class CardColumn extends React.Component<Props> {
contentRight, contentRight,
footerLeft, footerLeft,
footerRight, footerRight,
className className,
} = this.props; } = this.props;
const link = this.createLink(); const link = this.createLink();
return ( return (
<> <>
{link} {link}
<NoEventWrapper className={classNames("media", className)}> <NoEventWrapper className={classNames('media', className)}>
<AvatarWrapper className="media-left">{avatar}</AvatarWrapper> <AvatarWrapper className="media-left">{avatar}</AvatarWrapper>
<FlexFullHeight <FlexFullHeight
className={classNames("media-content", "text-box", "is-flex")} className={classNames('media-content', 'text-box', 'is-flex')}
> >
<div className="is-flex"> <div className="is-flex">
<ContentLeft className="content"> <ContentLeft className="content">
@@ -94,7 +93,7 @@ export default class CardColumn extends React.Component<Props> {
</ContentLeft> </ContentLeft>
<ContentRight>{contentRight}</ContentRight> <ContentRight>{contentRight}</ContentRight>
</div> </div>
<FooterWrapper className={classNames("level", "is-flex")}> <FooterWrapper className={classNames('level', 'is-flex')}>
<div className="level-left is-hidden-mobile">{footerLeft}</div> <div className="level-left is-hidden-mobile">{footerLeft}</div>
<div className="level-right is-mobile is-marginless"> <div className="level-right is-mobile is-marginless">
{footerRight} {footerRight}

View File

@@ -1,15 +1,14 @@
//@flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames"; import styled from 'styled-components';
import styled from "styled-components";
type Props = { type Props = {
name: string, name: string;
elements: React.Node[] elements: React.Node[];
}; };
type State = { type State = {
collapsed: boolean collapsed: boolean;
}; };
const Container = styled.div` const Container = styled.div`
@@ -24,13 +23,13 @@ export default class CardColumnGroup extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
collapsed: false collapsed: false,
}; };
} }
toggleCollapse = () => { toggleCollapse = () => {
this.setState(prevState => ({ this.setState(prevState => ({
collapsed: !prevState.collapsed collapsed: !prevState.collapsed,
})); }));
}; };
@@ -50,20 +49,20 @@ export default class CardColumnGroup extends React.Component<Props, State> {
const { name, elements } = this.props; const { name, elements } = this.props;
const { collapsed } = this.state; const { collapsed } = this.state;
const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; const icon = collapsed ? 'fa-angle-right' : 'fa-angle-down';
let content = null; let content = null;
if (!collapsed) { if (!collapsed) {
content = elements.map((entry, index) => { content = elements.map((entry, index) => {
const fullColumnWidth = this.isFullSize(elements, index); const fullColumnWidth = this.isFullSize(elements, index);
const sizeClass = fullColumnWidth ? "is-full" : "is-half"; const sizeClass = fullColumnWidth ? 'is-full' : 'is-half';
return ( return (
<div <div
className={classNames( className={classNames(
"box", 'box',
"box-link-shadow", 'box-link-shadow',
"column", 'column',
"is-clipped", 'is-clipped',
sizeClass sizeClass,
)} )}
key={index} key={index}
> >
@@ -76,14 +75,14 @@ export default class CardColumnGroup extends React.Component<Props, State> {
<Container> <Container>
<h2> <h2>
<span <span
className={classNames("is-size-4", "has-cursor-pointer")} className={classNames('is-size-4', 'has-cursor-pointer')}
onClick={this.toggleCollapse} onClick={this.toggleCollapse}
> >
<i className={classNames("fa", icon)} /> {name} <i className={classNames('fa', icon)} /> {name}
</span> </span>
</h2> </h2>
<hr /> <hr />
<Wrapper className={classNames("columns", "is-multiline")}> <Wrapper className={classNames('columns', 'is-multiline')}>
{content} {content}
</Wrapper> </Wrapper>
<div className="is-clearfix" /> <div className="is-clearfix" />

View File

@@ -1,13 +1,13 @@
import React from "react"; import React from 'react';
import DateFromNow from "./DateFromNow"; import DateFromNow from './DateFromNow';
import { storiesOf } from "@storybook/react"; import { storiesOf } from '@storybook/react';
const baseProps = { const baseProps = {
timeZone: "Europe/Berlin", timeZone: 'Europe/Berlin',
baseDate: "2019-10-12T13:56:42+02:00" baseDate: '2019-10-12T13:56:42+02:00',
}; };
storiesOf("DateFromNow", module).add("Default", () => ( storiesOf('DateFromNow', module).add('Default', () => (
<div> <div>
<p> <p>
<DateFromNow date="2009-06-30T18:30:00+02:00" {...baseProps} /> <DateFromNow date="2009-06-30T18:30:00+02:00" {...baseProps} />

View File

@@ -1,19 +1,18 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import { formatDistance, format, parseISO } from 'date-fns';
import { formatDistance, format, parseISO } from "date-fns"; import { enUS, de, es } from 'date-fns/locale';
import { enUS, de, es } from "date-fns/locale"; import styled from 'styled-components';
import styled from "styled-components";
const supportedLocales = { const supportedLocales = {
enUS, enUS,
de, de,
es es,
}; };
type Props = { type Props = {
date?: string, date?: string;
timeZone?: string, timeZone?: string;
/** /**
* baseDate is the date from which the distance is calculated, * baseDate is the date from which the distance is calculated,
@@ -21,10 +20,10 @@ type Props = {
* is required to keep snapshots tests green over the time on * is required to keep snapshots tests green over the time on
* ci server. * ci server.
*/ */
baseDate?: string, baseDate?: string;
// context props // context props
i18n: any i18n: any;
}; };
const DateElement = styled.time` const DateElement = styled.time`
@@ -44,7 +43,7 @@ class DateFromNow extends React.Component<Props> {
createOptions = () => { createOptions = () => {
const { timeZone } = this.props; const { timeZone } = this.props;
const options: Object = { const options: object = {
addSuffix: true, addSuffix: true,
locate: this.getLocale(), locate: this.getLocale(),
}; };
@@ -68,7 +67,7 @@ class DateFromNow extends React.Component<Props> {
const isoDate = parseISO(date); const isoDate = parseISO(date);
const options = this.createOptions(); const options = this.createOptions();
const distance = formatDistance(isoDate, this.getBaseDate(), options); const distance = formatDistance(isoDate, this.getBaseDate(), options);
const formatted = format(isoDate, "yyyy-MM-dd HH:mm:ss", options); const formatted = format(isoDate, 'yyyy-MM-dd HH:mm:ss', options);
return <DateElement title={formatted}>{distance}</DateElement>; return <DateElement title={formatted}>{distance}</DateElement>;
} }
return null; return null;

View File

@@ -1,22 +1,21 @@
// @flow import * as React from 'react';
import * as React from "react"; import ErrorNotification from './ErrorNotification';
import ErrorNotification from "./ErrorNotification";
type Props = { type Props = {
fallback?: React.ComponentType<any>, fallback?: React.ComponentType<any>;
children: React.Node children: React.Node;
}; };
type ErrorInfo = { type ErrorInfo = {
componentStack: string componentStack: string;
}; };
type State = { type State = {
error?: Error, error?: Error;
errorInfo?: ErrorInfo errorInfo?: ErrorInfo;
}; };
class ErrorBoundary extends React.Component<Props,State> { class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = {}; this.state = {};
@@ -26,7 +25,7 @@ class ErrorBoundary extends React.Component<Props,State> {
// Catch errors in any components below and re-render with error message // Catch errors in any components below and re-render with error message
this.setState({ this.setState({
error, error,
errorInfo errorInfo,
}); });
} }

View File

@@ -1,13 +1,12 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import {translate} from "react-i18next"; import { BackendError, ForbiddenError, UnauthorizedError } from './errors';
import {BackendError, ForbiddenError, UnauthorizedError} from "./errors"; import Notification from './Notification';
import Notification from "./Notification"; import BackendErrorNotification from './BackendErrorNotification';
import BackendErrorNotification from "./BackendErrorNotification";
type Props = { type Props = {
t: string => string, t: (p: string) => string;
error?: Error error?: Error;
}; };
class ErrorNotification extends React.Component<Props> { class ErrorNotification extends React.Component<Props> {
@@ -19,24 +18,24 @@ class ErrorNotification extends React.Component<Props> {
} else if (error instanceof UnauthorizedError) { } else if (error instanceof UnauthorizedError) {
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong>{" "} <strong>{t('errorNotification.prefix')}:</strong>{' '}
{t("errorNotification.timeout")}{" "} {t('errorNotification.timeout')}{' '}
<a href="javascript:window.location.reload(true)"> <a href="javascript:window.location.reload(true)">
{t("errorNotification.loginLink")} {t('errorNotification.loginLink')}
</a> </a>
</Notification> </Notification>
); );
} else if (error instanceof ForbiddenError) { } else if (error instanceof ForbiddenError) {
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong>{" "} <strong>{t('errorNotification.prefix')}:</strong>{' '}
{t("errorNotification.forbidden")} {t('errorNotification.forbidden')}
</Notification> </Notification>
); );
} else { } else {
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong> {error.message} <strong>{t('errorNotification.prefix')}:</strong> {error.message}
</Notification> </Notification>
); );
} }
@@ -45,4 +44,4 @@ class ErrorNotification extends React.Component<Props> {
} }
} }
export default translate("commons")(ErrorNotification); export default translate('commons')(ErrorNotification);

View File

@@ -1,12 +1,11 @@
//@flow import React from 'react';
import React from "react"; import ErrorNotification from './ErrorNotification';
import ErrorNotification from "./ErrorNotification"; import { BackendError, ForbiddenError } from './errors';
import { BackendError, ForbiddenError } from "./errors";
type Props = { type Props = {
error: Error, error: Error;
title: string, title: string;
subtitle: string subtitle: string;
}; };
class ErrorPage extends React.Component<Props> { class ErrorPage extends React.Component<Props> {
@@ -29,8 +28,8 @@ class ErrorPage extends React.Component<Props> {
if (error instanceof BackendError || error instanceof ForbiddenError) { if (error instanceof BackendError || error instanceof ForbiddenError) {
return null; return null;
} }
return <p className="subtitle">{subtitle}</p> return <p className="subtitle">{subtitle}</p>;
} };
} }
export default ErrorPage; export default ErrorPage;

View File

@@ -1,10 +0,0 @@
import FileSize from "./FileSize";
it("should format bytes", () => {
expect(FileSize.format(0)).toBe("0 B");
expect(FileSize.format(160)).toBe("160 B");
expect(FileSize.format(6304)).toBe("6.30 K");
expect(FileSize.format(28792588)).toBe("28.79 M");
expect(FileSize.format(1369510189)).toBe("1.37 G");
expect(FileSize.format(42949672960)).toBe("42.95 G");
});

View File

@@ -0,0 +1,10 @@
import FileSize from './FileSize';
it('should format bytes', () => {
expect(FileSize.format(0)).toBe('0 B');
expect(FileSize.format(160)).toBe('160 B');
expect(FileSize.format(6304)).toBe('6.30 K');
expect(FileSize.format(28792588)).toBe('28.79 M');
expect(FileSize.format(1369510189)).toBe('1.37 G');
expect(FileSize.format(42949672960)).toBe('42.95 G');
});

View File

@@ -1,17 +1,16 @@
// @flow import React from 'react';
import React from "react";
type Props = { type Props = {
bytes: number bytes: number;
}; };
class FileSize extends React.Component<Props> { class FileSize extends React.Component<Props> {
static format(bytes: number) { static format(bytes: number) {
if (!bytes) { if (!bytes) {
return "0 B"; return '0 B';
} }
const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"]; const units = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const i = Math.floor(Math.log(bytes) / Math.log(1000)); const i = Math.floor(Math.log(bytes) / Math.log(1000));
const size = i === 0 ? bytes : (bytes / 1000 ** i).toFixed(2); const size = i === 0 ? bytes : (bytes / 1000 ** i).toFixed(2);

View File

@@ -1,27 +0,0 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import type AutocompleteProps from "./UserGroupAutocomplete";
import UserGroupAutocomplete from "./UserGroupAutocomplete";
type Props = AutocompleteProps & {
// Context props
t: string => string
};
class GroupAutocomplete extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<UserGroupAutocomplete
label={t("autocomplete.group")}
noOptionsMessage={t("autocomplete.noGroupOptions")}
loadingMessage={t("autocomplete.loading")}
placeholder={t("autocomplete.groupPlaceholder")}
{...this.props}
/>
);
}
}
export default translate("commons")(GroupAutocomplete);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { translate } from 'react-i18next';
import AutocompleteProps from './UserGroupAutocomplete';
import UserGroupAutocomplete from './UserGroupAutocomplete';
type Props = AutocompleteProps & {
// Context props
t: (p: string) => string;
};
class GroupAutocomplete extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<UserGroupAutocomplete
label={t('autocomplete.group')}
noOptionsMessage={t('autocomplete.noGroupOptions')}
loadingMessage={t('autocomplete.loading')}
placeholder={t('autocomplete.groupPlaceholder')}
{...this.props}
/>
);
}
}
export default translate('commons')(GroupAutocomplete);

View File

@@ -1,27 +0,0 @@
//@flow
import React from "react";
import classNames from "classnames";
import styled from "styled-components";
import Tooltip from "./Tooltip";
import HelpIcon from "./HelpIcon";
type Props = {
message: string,
className?: string
};
const HelpTooltip = styled(Tooltip)`
position: absolute;
padding-left: 3px;
`;
export default class Help extends React.Component<Props> {
render() {
const { message, className } = this.props;
return (
<HelpTooltip className={classNames("is-inline-block", className)} message={message}>
<HelpIcon />
</HelpTooltip>
);
}
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import classNames from 'classnames';
import styled from 'styled-components';
import Tooltip from './Tooltip';
import HelpIcon from './HelpIcon';
type Props = {
message: string;
className?: string;
};
const HelpTooltip = styled(Tooltip)`
position: absolute;
padding-left: 3px;
`;
export default class Help extends React.Component<Props> {
render() {
const { message, className } = this.props;
return (
<HelpTooltip
className={classNames('is-inline-block', className)}
message={message}
>
<HelpIcon />
</HelpTooltip>
);
}
}

View File

@@ -1,9 +1,8 @@
//@flow import React from 'react';
import React from "react"; import Icon from './Icon';
import Icon from "./Icon";
type Props = { type Props = {
className?: string className?: string;
}; };
export default class HelpIcon extends React.Component<Props> { export default class HelpIcon extends React.Component<Props> {

View File

@@ -1,29 +0,0 @@
//@flow
import React from "react";
import classNames from "classnames";
type Props = {
title?: string,
name: string,
color: string,
className?: string
};
export default class Icon extends React.Component<Props> {
static defaultProps = {
color: "grey-light"
};
render() {
const { title, name, color, className } = this.props;
if (title) {
return (
<i
title={title}
className={classNames("fas", "fa-fw", "fa-" + name, `has-text-${color}`, className)}
/>
);
}
return <i className={classNames("fas", "fa-" + name, `has-text-${color}`, className)} />;
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import classNames from 'classnames';
type Props = {
title?: string;
name: string;
color: string;
className?: string;
};
export default class Icon extends React.Component<Props> {
static defaultProps = {
color: 'grey-light',
};
render() {
const { title, name, color, className } = this.props;
if (title) {
return (
<i
title={title}
className={classNames(
'fas',
'fa-fw',
'fa-' + name,
`has-text-${color}`,
className,
)}
/>
);
}
return (
<i
className={classNames(
'fas',
'fa-' + name,
`has-text-${color}`,
className,
)}
/>
);
}
}

View File

@@ -1,18 +1,16 @@
//@flow import React from 'react';
import React from "react"; import { withContextPath } from './urls';
import {withContextPath} from "./urls";
type Props = { type Props = {
src: string, src: string;
alt: string, alt: string;
className?: any className?: any;
}; };
class Image extends React.Component<Props> { class Image extends React.Component<Props> {
createImageSrc = () => { createImageSrc = () => {
const { src } = this.props; const { src } = this.props;
if (src.startsWith("http")) { if (src.startsWith('http')) {
return src; return src;
} }
return withContextPath(src); return withContextPath(src);

View File

@@ -1,16 +1,15 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import { PagedCollection } from '@scm-manager/ui-types';
import type { PagedCollection } from "@scm-manager/ui-types"; import { Button } from './buttons';
import { Button } from "./buttons";
type Props = { type Props = {
collection: PagedCollection, collection: PagedCollection;
page: number, page: number;
filter?: string, filter?: string;
// context props // context props
t: string => string t: (p: string) => string;
}; };
class LinkPaginator extends React.Component<Props> { class LinkPaginator extends React.Component<Props> {
@@ -26,9 +25,9 @@ class LinkPaginator extends React.Component<Props> {
return ( return (
<Button <Button
className="pagination-link" className="pagination-link"
label={"1"} label={'1'}
disabled={false} disabled={false}
link={this.addFilterToLink("1")} link={this.addFilterToLink('1')}
/> />
); );
} }
@@ -41,7 +40,7 @@ class LinkPaginator extends React.Component<Props> {
<Button <Button
className={className} className={className}
label={label ? label : previousPage.toString()} label={label ? label : previousPage.toString()}
disabled={!this.hasLink("prev")} disabled={!this.hasLink('prev')}
link={this.addFilterToLink(`${previousPage}`)} link={this.addFilterToLink(`${previousPage}`)}
/> />
); );
@@ -59,7 +58,7 @@ class LinkPaginator extends React.Component<Props> {
<Button <Button
className={className} className={className}
label={label ? label : nextPage.toString()} label={label ? label : nextPage.toString()}
disabled={!this.hasLink("next")} disabled={!this.hasLink('next')}
link={this.addFilterToLink(`${nextPage}`)} link={this.addFilterToLink(`${nextPage}`)}
/> />
); );
@@ -104,45 +103,40 @@ class LinkPaginator extends React.Component<Props> {
links.push(this.separator()); links.push(this.separator());
} }
if (page > 2) { if (page > 2) {
links.push(this.renderPreviousButton("pagination-link")); links.push(this.renderPreviousButton('pagination-link'));
} }
links.push(this.currentPage(page)); links.push(this.currentPage(page));
if (page + 1 < pageTotal) { if (page + 1 < pageTotal) {
links.push(this.renderNextButton("pagination-link")); links.push(this.renderNextButton('pagination-link'));
} }
if (page + 2 < pageTotal) if (page + 2 < pageTotal) links.push(this.separator());
//if there exists pages between next and last //if there exists pages between next and last
links.push(this.separator());
if (page < pageTotal) { if (page < pageTotal) {
links.push(this.renderLastButton()); links.push(this.renderLastButton());
} }
return links; return links;
} }
render() { render() {
const { collection, t } = this.props; const { collection, t } = this.props;
if (collection) {
if(collection) {
return ( return (
<nav className="pagination is-centered" aria-label="pagination"> <nav className="pagination is-centered" aria-label="pagination">
{this.renderPreviousButton( {this.renderPreviousButton(
"pagination-previous", 'pagination-previous',
t("paginator.previous") t('paginator.previous'),
)} )}
<ul className="pagination-list"> <ul className="pagination-list">
{this.pageLinks().map((link, index) => { {this.pageLinks().map((link, index) => {
return <li key={index}>{link}</li>; return <li key={index}>{link}</li>;
})} })}
</ul> </ul>
{this.renderNextButton("pagination-next", t("paginator.next"))} {this.renderNextButton('pagination-next', t('paginator.next'))}
</nav> </nav>
); );
} }
return null; return null;
} }
} }
export default translate('commons')(LinkPaginator);
export default translate("commons")(LinkPaginator);

View File

@@ -1,11 +0,0 @@
// @flow
import React from "react";
import { storiesOf } from "@storybook/react";
import Loading from './Loading';
storiesOf("Loading", module)
.add("Default", () => (
<div>
<Loading />
</div>
));

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import Loading from './Loading';
storiesOf('Loading', module).add('Default', () => (
<div>
<Loading />
</div>
));

View File

@@ -1,12 +1,11 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import styled from 'styled-components';
import styled from "styled-components"; import Image from './Image';
import Image from "./Image";
type Props = { type Props = {
t: string => string, t: (p: string) => string;
message?: string message?: string;
}; };
const Wrapper = styled.div` const Wrapper = styled.div`
@@ -27,11 +26,11 @@ class Loading extends React.Component<Props> {
const { message, t } = this.props; const { message, t } = this.props;
return ( return (
<Wrapper className="is-flex"> <Wrapper className="is-flex">
<FixedSizedImage src="/images/loading.svg" alt={t("loading.alt")} /> <FixedSizedImage src="/images/loading.svg" alt={t('loading.alt')} />
<p className="has-text-centered">{message}</p> <p className="has-text-centered">{message}</p>
</Wrapper> </Wrapper>
); );
} }
} }
export default translate("commons")(Loading); export default 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,17 +0,0 @@
// @flow
import React from "react";
import { storiesOf } from "@storybook/react";
import styled from "styled-components";
import Logo from "./Logo";
const Wrapper = styled.div`
padding: 2em;
background-color: black;
height: 100%;
`;
storiesOf("Logo", module).add("Default", () => (
<Wrapper>
<Logo />
</Wrapper>
));

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import styled from 'styled-components';
import Logo from './Logo';
const Wrapper = styled.div`
padding: 2em;
background-color: black;
height: 100%;
`;
storiesOf('Logo', module).add('Default', () => (
<Wrapper>
<Logo />
</Wrapper>
));

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { translate } from 'react-i18next';
import Image from './Image';
type Props = {
t: (p: 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,8 +1,7 @@
// @flow import React from 'react';
import React from "react";
type Props = { type Props = {
address?: string address?: string;
}; };
class MailLink extends React.Component<Props> { class MailLink extends React.Component<Props> {
@@ -11,7 +10,7 @@ class MailLink extends React.Component<Props> {
if (!address) { if (!address) {
return null; return null;
} }
return <a href={"mailto:" + address}>{address}</a>; return <a href={'mailto:' + address}>{address}</a>;
} }
} }

View File

@@ -1,18 +0,0 @@
// @flow
import React from "react";
import { headingToAnchorId } from "./MarkdownHeadingRenderer";
describe("headingToAnchorId tests", () => {
it("should lower case the text", () => {
expect(headingToAnchorId("Hello")).toBe("hello");
expect(headingToAnchorId("HeLlO")).toBe("hello");
expect(headingToAnchorId("HELLO")).toBe("hello");
});
it("should replace spaces with hyphen", () => {
expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff");
expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f");
});
});

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { headingToAnchorId } from './MarkdownHeadingRenderer';
describe('headingToAnchorId tests', () => {
it('should lower case the text', () => {
expect(headingToAnchorId('Hello')).toBe('hello');
expect(headingToAnchorId('HeLlO')).toBe('hello');
expect(headingToAnchorId('HELLO')).toBe('hello');
});
it('should replace spaces with hyphen', () => {
expect(headingToAnchorId('awesome stuff')).toBe('awesome-stuff');
expect(headingToAnchorId('a b c d e f')).toBe('a-b-c-d-e-f');
});
});

View File

@@ -1,7 +1,6 @@
// @flow import * as React from 'react';
import * as React from "react"; import { withRouter } from 'react-router-dom';
import { withRouter } from "react-router-dom"; import { withContextPath } from './urls';
import { withContextPath } from "./urls";
/** /**
* Adds anchor links to markdown headings. * Adds anchor links to markdown headings.
@@ -10,13 +9,13 @@ import { withContextPath } from "./urls";
*/ */
type Props = { type Props = {
children: React.Node, children: React.Node;
level: number, level: number;
location: any location: any;
}; };
function flatten(text: string, child: any) { function flatten(text: string, child: any) {
return typeof child === "string" return typeof child === 'string'
? text + child ? text + child
: React.Children.toArray(child.props.children).reduce(flatten, text); : React.Children.toArray(child.props.children).reduce(flatten, text);
} }
@@ -27,15 +26,19 @@ function flatten(text: string, child: any) {
* @VisibleForTesting * @VisibleForTesting
*/ */
export function headingToAnchorId(heading: string) { export function headingToAnchorId(heading: string) {
return heading.toLowerCase().replace(/\W/g, "-"); return heading.toLowerCase().replace(/\W/g, '-');
} }
function MarkdownHeadingRenderer(props: Props) { function MarkdownHeadingRenderer(props: Props) {
const children = React.Children.toArray(props.children); const children = React.Children.toArray(props.children);
const heading = children.reduce(flatten, ""); const heading = children.reduce(flatten, '');
const anchorId = headingToAnchorId(heading); const anchorId = headingToAnchorId(heading);
const headingElement = React.createElement("h" + props.level, {}, props.children); const headingElement = React.createElement(
const href = withContextPath(props.location.pathname + "#" + anchorId); 'h' + props.level,
{},
props.children,
);
const href = withContextPath(props.location.pathname + '#' + anchorId);
return ( return (
<a id={`${anchorId}`} className="anchor" href={href}> <a id={`${anchorId}`} className="anchor" href={href}>

View File

@@ -1,22 +0,0 @@
// @flow
import React from "react";
import {storiesOf} from "@storybook/react";
import MarkdownView from "./MarkdownView";
import styled from "styled-components";
import {MemoryRouter} from "react-router-dom";
import TestPage from "./__resources__/test-page.md";
const Spacing = styled.div`
padding: 2em;
`;
storiesOf("MarkdownView", module)
.addDecorator(story => (
<MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>
))
.add("Default", () => (
<Spacing>
<MarkdownView content={TestPage} skipHtml={false} />
</Spacing>
));

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import MarkdownView from './MarkdownView';
import styled from 'styled-components';
import { MemoryRouter } from 'react-router-dom';
import TestPage from './__resources__/test-page.md';
const Spacing = styled.div`
padding: 2em;
`;
storiesOf('MarkdownView', module)
.addDecorator(story => (
<MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>
))
.add('Default', () => (
<Spacing>
<MarkdownView content={TestPage} skipHtml={false} />
</Spacing>
));

View File

@@ -1,26 +1,30 @@
//@flow import React from 'react';
import React from "react"; import { withRouter } from 'react-router-dom';
import { withRouter } from "react-router-dom"; import Markdown from 'react-markdown/with-html';
import Markdown from "react-markdown/with-html"; import styled from 'styled-components';
import styled from "styled-components"; import { binder } from '@scm-manager/ui-extensions';
import { binder } from "@scm-manager/ui-extensions"; import SyntaxHighlighter from './SyntaxHighlighter';
import SyntaxHighlighter from "./SyntaxHighlighter"; import MarkdownHeadingRenderer from './MarkdownHeadingRenderer';
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
type Props = { type Props = {
content: string, content: string;
renderContext?: Object, renderContext?: object;
renderers?: Object, renderers?: object;
skipHtml?: boolean, skipHtml?: boolean;
enableAnchorHeadings: boolean, enableAnchorHeadings: boolean;
// context props // context props
location: any location: any;
}; };
const MarkdownWrapper = styled.div` const MarkdownWrapper = styled.div`
> .content { > .content {
> h1, h2, h3, h4, h5, h6 { > h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0.5rem 0; margin: 0.5rem 0;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -30,7 +34,10 @@ const MarkdownWrapper = styled.div`
> h2 { > h2 {
font-weight: 600; font-weight: 600;
} }
> h3, h4, h5, h6 { > h3,
h4,
h5,
h6 {
font-weight: 500; font-weight: 500;
} }
& strong { & strong {
@@ -42,10 +49,10 @@ const MarkdownWrapper = styled.div`
class MarkdownView extends React.Component<Props> { class MarkdownView extends React.Component<Props> {
static defaultProps = { static defaultProps = {
enableAnchorHeadings: false, enableAnchorHeadings: false,
skipHtml: false skipHtml: false,
}; };
contentRef: ?HTMLDivElement; contentRef: HTMLDivElement | null | undefined;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -72,10 +79,10 @@ class MarkdownView extends React.Component<Props> {
renderers, renderers,
renderContext, renderContext,
enableAnchorHeadings, enableAnchorHeadings,
skipHtml skipHtml,
} = this.props; } = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory"); const rendererFactory = binder.getExtension('markdown-renderer-factory');
let rendererList = renderers; let rendererList = renderers;
if (rendererFactory) { if (rendererFactory) {

View File

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

View File

@@ -1,19 +1,18 @@
//@flow import React from 'react';
import React from "react"; import { History } from 'history';
import type { History } from "history"; import { withRouter } from 'react-router-dom';
import { withRouter } from "react-router-dom"; import classNames from 'classnames';
import classNames from "classnames"; import { Button, urls } from './index';
import { Button, urls } from "./index"; import { FilterInput } from './forms';
import { FilterInput } from "./forms";
type Props = { type Props = {
showCreateButton: boolean, showCreateButton: boolean;
link: string, link: string;
label?: string, label?: string;
// context props // context props
history: History, history: History;
location: any location: any;
}; };
class OverviewPageActions extends React.Component<Props> { class OverviewPageActions extends React.Component<Props> {
@@ -36,7 +35,7 @@ class OverviewPageActions extends React.Component<Props> {
const { showCreateButton, link, label } = this.props; const { showCreateButton, link, label } = this.props;
if (showCreateButton) { if (showCreateButton) {
return ( return (
<div className={classNames("input-button", "control")}> <div className={classNames('input-button', 'control')}>
<Button label={label} link={`/${link}/create`} color="primary" /> <Button label={label} link={`/${link}/create`} color="primary" />
</div> </div>
); );

View File

@@ -1,49 +1,43 @@
// @flow import React from 'react';
import React from "react"; import { mount, shallow } from '@scm-manager/ui-tests/enzyme-router';
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router"; import '@scm-manager/ui-tests/i18n';
import "@scm-manager/ui-tests/i18n"; import Paginator from './Paginator';
import Paginator from "./Paginator";
xdescribe("paginator rendering tests", () => {
xdescribe('paginator rendering tests', () => {
const dummyLink = { const dummyLink = {
href: "https://dummy" href: 'https://dummy',
}; };
it("should render all buttons but disabled, without links", () => { it('should render all buttons but disabled, without links', () => {
const collection = { const collection = {
page: 10, page: 10,
pageTotal: 20, pageTotal: 20,
_links: {}, _links: {},
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
for (let button of buttons) { for (let button of buttons) {
expect(button.props.disabled).toBeTruthy(); expect(button.props.disabled).toBeTruthy();
} }
}); });
it("should render buttons for first page", () => { it('should render buttons for first page', () => {
const collection = { const collection = {
page: 0, page: 0,
pageTotal: 148, pageTotal: 148,
_links: { _links: {
first: dummyLink, first: dummyLink,
next: dummyLink, next: dummyLink,
last: dummyLink last: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); expect(buttons.length).toBe(5);
// previous button // previous button
@@ -58,15 +52,15 @@ xdescribe("paginator rendering tests", () => {
// next button // next button
const nextButton = buttons.get(3).props; const nextButton = buttons.get(3).props;
expect(nextButton.disabled).toBeFalsy(); expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("2"); expect(nextButton.label).toBe('2');
// last button // last button
const lastButton = buttons.get(4).props; const lastButton = buttons.get(4).props;
expect(lastButton.disabled).toBeFalsy(); expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148"); expect(lastButton.label).toBe('148');
}); });
it("should render buttons for second page", () => { it('should render buttons for second page', () => {
const collection = { const collection = {
page: 1, page: 1,
pageTotal: 148, pageTotal: 148,
@@ -74,15 +68,13 @@ xdescribe("paginator rendering tests", () => {
first: dummyLink, first: dummyLink,
prev: dummyLink, prev: dummyLink,
next: dummyLink, next: dummyLink,
last: dummyLink last: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); expect(buttons.length).toBe(6);
// previous button // previous button
@@ -92,7 +84,7 @@ xdescribe("paginator rendering tests", () => {
// first button // first button
const firstButton = buttons.get(2).props; const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy(); expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1"); expect(firstButton.label).toBe('1');
// current button // current button
const currentButton = buttons.get(3).props; const currentButton = buttons.get(3).props;
@@ -102,29 +94,27 @@ xdescribe("paginator rendering tests", () => {
// next button // next button
const nextButton = buttons.get(4).props; const nextButton = buttons.get(4).props;
expect(nextButton.disabled).toBeFalsy(); expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("3"); expect(nextButton.label).toBe('3');
// last button // last button
const lastButton = buttons.get(5).props; const lastButton = buttons.get(5).props;
expect(lastButton.disabled).toBeFalsy(); expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148"); expect(lastButton.label).toBe('148');
}); });
it("should render buttons for last page", () => { it('should render buttons for last page', () => {
const collection = { const collection = {
page: 147, page: 147,
pageTotal: 148, pageTotal: 148,
_links: { _links: {
first: dummyLink, first: dummyLink,
prev: dummyLink prev: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); expect(buttons.length).toBe(5);
// previous button // previous button
@@ -134,12 +124,12 @@ xdescribe("paginator rendering tests", () => {
// first button // first button
const firstButton = buttons.get(2).props; const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy(); expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1"); expect(firstButton.label).toBe('1');
// next button // next button
const nextButton = buttons.get(3).props; const nextButton = buttons.get(3).props;
expect(nextButton.disabled).toBeFalsy(); expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("147"); expect(nextButton.label).toBe('147');
// last button // last button
const lastButton = buttons.get(4).props; const lastButton = buttons.get(4).props;
@@ -147,7 +137,7 @@ xdescribe("paginator rendering tests", () => {
expect(lastButton.label).toBe(148); expect(lastButton.label).toBe(148);
}); });
it("should render buttons for penultimate page", () => { it('should render buttons for penultimate page', () => {
const collection = { const collection = {
page: 146, page: 146,
pageTotal: 148, pageTotal: 148,
@@ -155,15 +145,13 @@ xdescribe("paginator rendering tests", () => {
first: dummyLink, first: dummyLink,
prev: dummyLink, prev: dummyLink,
next: dummyLink, next: dummyLink,
last: dummyLink last: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); expect(buttons.length).toBe(6);
// previous button // previous button
@@ -174,11 +162,11 @@ xdescribe("paginator rendering tests", () => {
// first button // first button
const firstButton = buttons.get(2).props; const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy(); expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1"); expect(firstButton.label).toBe('1');
const currentButton = buttons.get(3).props; const currentButton = buttons.get(3).props;
expect(currentButton.disabled).toBeFalsy(); expect(currentButton.disabled).toBeFalsy();
expect(currentButton.label).toBe("146"); expect(currentButton.label).toBe('146');
// current button // current button
const nextButton = buttons.get(4).props; const nextButton = buttons.get(4).props;
@@ -188,10 +176,10 @@ xdescribe("paginator rendering tests", () => {
// last button // last button
const lastButton = buttons.get(5).props; const lastButton = buttons.get(5).props;
expect(lastButton.disabled).toBeFalsy(); expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148"); expect(lastButton.label).toBe('148');
}); });
it("should render buttons for a page in the middle", () => { it('should render buttons for a page in the middle', () => {
const collection = { const collection = {
page: 41, page: 41,
pageTotal: 148, pageTotal: 148,
@@ -199,15 +187,13 @@ xdescribe("paginator rendering tests", () => {
first: dummyLink, first: dummyLink,
prev: dummyLink, prev: dummyLink,
next: dummyLink, next: dummyLink,
last: dummyLink last: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
const paginator = shallow( const paginator = shallow(<Paginator collection={collection} />);
<Paginator collection={collection} /> const buttons = paginator.find('Button');
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
// previous button // previous button
@@ -218,12 +204,12 @@ xdescribe("paginator rendering tests", () => {
// first button // first button
const firstButton = buttons.get(2).props; const firstButton = buttons.get(2).props;
expect(firstButton.disabled).toBeFalsy(); expect(firstButton.disabled).toBeFalsy();
expect(firstButton.label).toBe("1"); expect(firstButton.label).toBe('1');
// previous Button // previous Button
const previousButton = buttons.get(3).props; const previousButton = buttons.get(3).props;
expect(previousButton.disabled).toBeFalsy(); expect(previousButton.disabled).toBeFalsy();
expect(previousButton.label).toBe("41"); expect(previousButton.label).toBe('41');
// current button // current button
const currentButton = buttons.get(4).props; const currentButton = buttons.get(4).props;
@@ -233,27 +219,27 @@ xdescribe("paginator rendering tests", () => {
// next button // next button
const nextButton = buttons.get(5).props; const nextButton = buttons.get(5).props;
expect(nextButton.disabled).toBeFalsy(); expect(nextButton.disabled).toBeFalsy();
expect(nextButton.label).toBe("43"); expect(nextButton.label).toBe('43');
// last button // last button
const lastButton = buttons.get(6).props; const lastButton = buttons.get(6).props;
expect(lastButton.disabled).toBeFalsy(); expect(lastButton.disabled).toBeFalsy();
expect(lastButton.label).toBe("148"); expect(lastButton.label).toBe('148');
}); });
it("should call the function with the last previous url", () => { it('should call the function with the last previous url', () => {
const collection = { const collection = {
page: 41, page: 41,
pageTotal: 148, pageTotal: 148,
_links: { _links: {
first: dummyLink, first: dummyLink,
prev: { prev: {
href: "https://www.scm-manager.org" href: 'https://www.scm-manager.org',
}, },
next: dummyLink, next: dummyLink,
last: dummyLink last: dummyLink,
}, },
_embedded: {} _embedded: {},
}; };
let urlToOpen; let urlToOpen;
@@ -262,10 +248,10 @@ xdescribe("paginator rendering tests", () => {
}; };
const paginator = mount( const paginator = mount(
<Paginator collection={collection} onPageChange={callMe} /> <Paginator collection={collection} onPageChange={callMe} />,
); );
paginator.find("Button.pagination-previous").simulate("click"); paginator.find('Button.pagination-previous').simulate('click');
expect(urlToOpen).toBe("https://www.scm-manager.org"); expect(urlToOpen).toBe('https://www.scm-manager.org');
}); });
}); });

View File

@@ -1,13 +1,12 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import { PagedCollection } from '@scm-manager/ui-types';
import type { PagedCollection } from "@scm-manager/ui-types"; import { Button } from './buttons';
import { Button } from "./buttons";
type Props = { type Props = {
collection: PagedCollection, collection: PagedCollection;
onPageChange?: string => void, onPageChange?: (p: string) => void;
t: string => string t: (p: string) => string;
}; };
class Paginator extends React.Component<Props> { class Paginator extends React.Component<Props> {
@@ -26,30 +25,30 @@ class Paginator extends React.Component<Props> {
}; };
renderFirstButton() { renderFirstButton() {
return this.renderPageButton(1, "first"); return this.renderPageButton(1, 'first');
} }
renderPreviousButton() { renderPreviousButton() {
const { t } = this.props; const { t } = this.props;
return this.renderButton( return this.renderButton(
"pagination-previous", 'pagination-previous',
t("paginator.previous"), t('paginator.previous'),
"prev" 'prev',
); );
} }
renderNextButton() { renderNextButton() {
const { t } = this.props; const { t } = this.props;
return this.renderButton("pagination-next", t("paginator.next"), "next"); return this.renderButton('pagination-next', t('paginator.next'), 'next');
} }
renderLastButton() { renderLastButton() {
const { collection } = this.props; const { collection } = this.props;
return this.renderPageButton(collection.pageTotal, "last"); return this.renderPageButton(collection.pageTotal, 'last');
} }
renderPageButton(page: number, linkType: string) { renderPageButton(page: number, linkType: string) {
return this.renderButton("pagination-link", page.toString(), linkType); return this.renderButton('pagination-link', page.toString(), linkType);
} }
renderButton(className: string, label: string, linkType: string) { renderButton(className: string, label: string, linkType: string) {
@@ -90,24 +89,21 @@ class Paginator extends React.Component<Props> {
links.push(this.seperator()); links.push(this.seperator());
} }
if (page > 2) { if (page > 2) {
links.push(this.renderPageButton(page - 1, "prev")); links.push(this.renderPageButton(page - 1, 'prev'));
} }
links.push(this.currentPage(page)); links.push(this.currentPage(page));
if (page + 1 < pageTotal) { if (page + 1 < pageTotal) {
links.push(this.renderPageButton(page + 1, "next")); links.push(this.renderPageButton(page + 1, 'next'));
} }
if (page + 2 < pageTotal) if (page + 2 < pageTotal) links.push(this.seperator());
//if there exists pages between next and last //if there exists pages between next and last
links.push(this.seperator());
if (page < pageTotal) { if (page < pageTotal) {
links.push(this.renderLastButton()); links.push(this.renderLastButton());
} }
return links; return links;
} }
render() { render() {
return ( return (
<nav className="pagination is-centered" aria-label="pagination"> <nav className="pagination is-centered" aria-label="pagination">
@@ -122,5 +118,4 @@ class Paginator extends React.Component<Props> {
); );
} }
} }
export default translate('commons')(Paginator);
export default translate("commons")(Paginator);

View File

@@ -1,10 +1,9 @@
//@flow import React, { Component } from 'react';
import React, { Component } from "react"; import { Route, Redirect, withRouter } from 'react-router-dom';
import { Route, Redirect, withRouter } from "react-router-dom";
type Props = { type Props = {
authenticated?: boolean, authenticated?: boolean;
component: Component<any, any> component: Component<any, any>;
}; };
class ProtectedRoute extends React.Component<Props> { class ProtectedRoute extends React.Component<Props> {
@@ -16,8 +15,10 @@ class ProtectedRoute extends React.Component<Props> {
return ( return (
<Redirect <Redirect
to={{ to={{
pathname: "/login", pathname: '/login',
state: { from: routeProps.location } state: {
from: routeProps.location,
},
}} }}
/> />
); );

View File

@@ -1,16 +1,15 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import { PagedCollection } from '@scm-manager/ui-types';
import type { PagedCollection } from "@scm-manager/ui-types"; import { Button } from './index';
import { Button } from "./index";
type Props = { type Props = {
collection: PagedCollection, collection: PagedCollection;
page: number, page: number;
updatePage: number => void, updatePage: (p: number) => void;
// context props // context props
t: string => string t: (p: string) => string;
}; };
class StatePaginator extends React.Component<Props> { class StatePaginator extends React.Component<Props> {
@@ -18,7 +17,7 @@ class StatePaginator extends React.Component<Props> {
return ( return (
<Button <Button
className="pagination-link" className="pagination-link"
label={"1"} label={'1'}
disabled={false} disabled={false}
action={() => this.updateCurrentPage(1)} action={() => this.updateCurrentPage(1)}
/> />
@@ -37,7 +36,7 @@ class StatePaginator extends React.Component<Props> {
<Button <Button
className="pagination-previous" className="pagination-previous"
label={label ? label : previousPage.toString()} label={label ? label : previousPage.toString()}
disabled={!this.hasLink("prev")} disabled={!this.hasLink('prev')}
action={() => this.updateCurrentPage(previousPage)} action={() => this.updateCurrentPage(previousPage)}
/> />
); );
@@ -55,7 +54,7 @@ class StatePaginator extends React.Component<Props> {
<Button <Button
className="pagination-next" className="pagination-next"
label={label ? label : nextPage.toString()} label={label ? label : nextPage.toString()}
disabled={!this.hasLink("next")} disabled={!this.hasLink('next')}
action={() => this.updateCurrentPage(nextPage)} action={() => this.updateCurrentPage(nextPage)}
/> />
); );
@@ -109,30 +108,26 @@ class StatePaginator extends React.Component<Props> {
if (page + 1 < pageTotal) { if (page + 1 < pageTotal) {
links.push(this.renderNextButton()); links.push(this.renderNextButton());
} }
if (page + 2 < pageTotal) if (page + 2 < pageTotal) links.push(this.separator());
//if there exists pages between next and last //if there exists pages between next and last
links.push(this.separator());
if (page < pageTotal) { if (page < pageTotal) {
links.push(this.renderLastButton()); links.push(this.renderLastButton());
} }
return links; return links;
} }
render() { render() {
const { t } = this.props; const { t } = this.props;
return ( return (
<nav className="pagination is-centered" aria-label="pagination"> <nav className="pagination is-centered" aria-label="pagination">
{this.renderPreviousButton(t("paginator.previous"))} {this.renderPreviousButton(t('paginator.previous'))}
<ul className="pagination-list"> <ul className="pagination-list">
{this.pageLinks().map((link, index) => { {this.pageLinks().map((link, index) => {
return <li key={index}>{link}</li>; return <li key={index}>{link}</li>;
})} })}
</ul> </ul>
{this.renderNextButton(t("paginator.next"))} {this.renderNextButton(t('paginator.next'))}
</nav> </nav>
); );
} }
} }
export default translate('commons')(StatePaginator);
export default translate("commons")(StatePaginator);

View File

@@ -1,36 +0,0 @@
// @flow
import React from "react";
import {storiesOf} from "@storybook/react";
import styled from "styled-components";
import SyntaxHighlighter from "./SyntaxHighlighter";
import JavaHttpServer from "./__resources__/HttpServer.java";
import GoHttpServer from "./__resources__/HttpServer.go";
import JsHttpServer from "./__resources__/HttpServer.js.js";
import PyHttpServer from "./__resources__/HttpServer.py";
const Spacing = styled.div`
padding: 1em;
`;
storiesOf("SyntaxHighlighter", module)
.add("Java", () => (
<Spacing>
<SyntaxHighlighter language="java" value={JavaHttpServer} />
</Spacing>
))
.add("Go", () => (
<Spacing>
<SyntaxHighlighter language="go" value={GoHttpServer} />
</Spacing>
))
.add("Javascript", () => (
<Spacing>
<SyntaxHighlighter language="javascript" value={JsHttpServer} />
</Spacing>
))
.add("Python", () => (
<Spacing>
<SyntaxHighlighter language="python" value={PyHttpServer} />
</Spacing>
));

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import styled from 'styled-components';
import SyntaxHighlighter from './SyntaxHighlighter';
import JavaHttpServer from './__resources__/HttpServer.java';
import GoHttpServer from './__resources__/HttpServer.go';
//import JsHttpServer from './__resources__/HttpServer.js';
import PyHttpServer from './__resources__/HttpServer.py';
const Spacing = styled.div`
padding: 1em;
`;
storiesOf('SyntaxHighlighter', module)
.add('Java', () => (
<Spacing>
<SyntaxHighlighter language="java" value={JavaHttpServer} />
</Spacing>
))
.add('Go', () => (
<Spacing>
<SyntaxHighlighter language="go" value={GoHttpServer} />
</Spacing>
))
/*.add('Javascript', () => (
<Spacing>
<SyntaxHighlighter language="javascript" value={JsHttpServer} />
</Spacing>
))*/
.add('Python', () => (
<Spacing>
<SyntaxHighlighter language="python" value={PyHttpServer} />
</Spacing>
));

View File

@@ -1,16 +1,14 @@
// @flow import React from 'react';
import React from "react";
import {LightAsync as ReactSyntaxHighlighter} from "react-syntax-highlighter"; import { LightAsync as ReactSyntaxHighlighter } from 'react-syntax-highlighter';
import {arduinoLight} from "react-syntax-highlighter/dist/cjs/styles/hljs"; import { arduinoLight } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
type Props = { type Props = {
language: string, language: string;
value: string value: string;
}; };
class SyntaxHighlighter extends React.Component<Props> { class SyntaxHighlighter extends React.Component<Props> {
render() { render() {
return ( return (
<ReactSyntaxHighlighter <ReactSyntaxHighlighter

View File

@@ -1,20 +1,19 @@
//@flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames";
type Props = { type Props = {
className?: string, className?: string;
color: string, color: string;
icon?: string, icon?: string;
label: string, label: string;
title?: string, title?: string;
onClick?: () => void, onClick?: () => void;
onRemove?: () => void onRemove?: () => void;
}; };
class Tag extends React.Component<Props> { class Tag extends React.Component<Props> {
static defaultProps = { static defaultProps = {
color: "light" color: 'light',
}; };
render() { render() {
@@ -25,13 +24,13 @@ class Tag extends React.Component<Props> {
label, label,
title, title,
onClick, onClick,
onRemove onRemove,
} = this.props; } = this.props;
let showIcon = null; let showIcon = null;
if (icon) { if (icon) {
showIcon = ( showIcon = (
<> <>
<i className={classNames("fas", `fa-${icon}`)} /> <i className={classNames('fas', `fa-${icon}`)} />
&nbsp; &nbsp;
</> </>
); );
@@ -44,7 +43,7 @@ class Tag extends React.Component<Props> {
return ( return (
<> <>
<span <span
className={classNames("tag", `is-${color}`, className)} className={classNames('tag', `is-${color}`, className)}
title={title} title={title}
onClick={onClick} onClick={onClick}
> >

View File

@@ -1,32 +0,0 @@
//@flow
import * as React from "react";
import classNames from "classnames";
type Props = {
message: string,
className?: string,
location: string,
children: React.Node
};
class Tooltip extends React.Component<Props> {
static defaultProps = {
location: "right"
};
render() {
const { className, message, location, children } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<span
className={classNames("tooltip", "is-tooltip-" + location, multiline, className)}
data-tooltip={message}
>
{children}
</span>
);
}
}
export default Tooltip;

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import classNames from 'classnames';
type Props = {
message: string;
className?: string;
location: string;
children: React.Node;
};
class Tooltip extends React.Component<Props> {
static defaultProps = {
location: 'right',
};
render() {
const { className, message, location, children } = this.props;
const multiline = message.length > 60 ? 'is-tooltip-multiline' : '';
return (
<span
className={classNames(
'tooltip',
'is-tooltip-' + location,
multiline,
className,
)}
data-tooltip={message}
>
{children}
</span>
);
}
}
export default Tooltip;

View File

@@ -1,27 +0,0 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import type AutocompleteProps from "./UserGroupAutocomplete";
import UserGroupAutocomplete from "./UserGroupAutocomplete";
type Props = AutocompleteProps & {
// Context props
t: string => string
};
class UserAutocomplete extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<UserGroupAutocomplete
label={t("autocomplete.user")}
noOptionsMessage={t("autocomplete.noUserOptions")}
loadingMessage={t("autocomplete.loading")}
placeholder={t("autocomplete.userPlaceholder")}
{...this.props}
/>
);
}
}
export default translate("commons")(UserAutocomplete);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { translate } from 'react-i18next';
import AutocompleteProps from './UserGroupAutocomplete';
import UserGroupAutocomplete from './UserGroupAutocomplete';
type Props = AutocompleteProps & {
// Context props
t: (p: string) => string;
};
class UserAutocomplete extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<UserGroupAutocomplete
label={t('autocomplete.user')}
noOptionsMessage={t('autocomplete.noUserOptions')}
loadingMessage={t('autocomplete.loading')}
placeholder={t('autocomplete.userPlaceholder')}
{...this.props}
/>
);
}
}
export default translate('commons')(UserAutocomplete);

View File

@@ -1,25 +1,24 @@
// @flow import React from 'react';
import React from "react"; import { SelectValue } from '@scm-manager/ui-types';
import type {SelectValue} from "@scm-manager/ui-types"; import Autocomplete from './Autocomplete';
import Autocomplete from "./Autocomplete";
export type AutocompleteProps = { export type AutocompleteProps = {
autocompleteLink: string, autocompleteLink: string;
valueSelected: SelectValue => void, valueSelected: (p: SelectValue) => void;
value?: SelectValue value?: SelectValue;
}; };
type Props = AutocompleteProps & { type Props = AutocompleteProps & {
label: string, label: string;
noOptionsMessage: string, noOptionsMessage: string;
loadingMessage: string, loadingMessage: string;
placeholder: string placeholder: string;
}; };
export default class UserGroupAutocomplete extends React.Component<Props> { export default class UserGroupAutocomplete extends React.Component<Props> {
loadSuggestions = (inputValue: string) => { loadSuggestions = (inputValue: string) => {
const url = this.props.autocompleteLink; const url = this.props.autocompleteLink;
const link = url + "?q="; const link = url + '?q=';
return fetch(link + inputValue) return fetch(link + inputValue)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(json => {
@@ -29,7 +28,7 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
: element.id; : element.id;
return { return {
value: element, value: element,
label label,
}; };
}); });
}); });

View File

@@ -1,7 +0,0 @@
export default `var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello World!');
res.end();
}).listen(8080);
`;

View File

@@ -343,7 +343,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
className="sc-EHOje lbpDzp" className="sc-EHOje lbpDzp"
> >
<div <div
className="sc-ifAKCX bndlBs" className="sc-ifAKCX frmxmY"
> >
<div <div
className="content" className="content"
@@ -2207,190 +2207,6 @@ exports[`Storyshots SyntaxHighlighter Java 1`] = `
</div> </div>
`; `;
exports[`Storyshots SyntaxHighlighter Javascript 1`] = `
<div
className="sc-bZQynM hAJIOS"
>
<pre
style={
Object {
"background": "#FFFFFF",
"color": "#434f54",
"display": "block",
"overflowX": "auto",
"padding": "0.5em",
}
}
>
<code
style={
Object {
"float": "left",
"paddingRight": "10px",
}
}
>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
1
</span>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
2
</span>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
3
</span>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
4
</span>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
5
</span>
<span
className="react-syntax-highlighter-line-number"
style={Object {}}
>
6
</span>
</code>
<code>
<span
style={
Object {
"color": "#00979D",
}
}
>
var
</span>
http =
<span
style={
Object {
"color": "#D35400",
}
}
>
require
</span>
(
<span
style={
Object {
"color": "#005C5F",
}
}
>
'http'
</span>
);
http.createServer(
<span
style={
Object {
"color": "#728E00",
}
}
>
<span
style={
Object {
"color": "#00979D",
}
}
>
function
</span>
(
<span
className="hljs-params"
style={Object {}}
>
req, res
</span>
)
</span>
{
res.writeHead(
<span
style={
Object {
"color": "#8A7B52",
}
}
>
200
</span>
, {
<span
style={
Object {
"color": "#005C5F",
}
}
>
'Content-Type'
</span>
:
<span
style={
Object {
"color": "#005C5F",
}
}
>
'text/plain'
</span>
});
res.write(
<span
style={
Object {
"color": "#005C5F",
}
}
>
'Hello World!'
</span>
);
res.end();
}).listen(
<span
style={
Object {
"color": "#8A7B52",
}
}
>
8080
</span>
);
</code>
</pre>
</div>
`;
exports[`Storyshots SyntaxHighlighter Python 1`] = ` exports[`Storyshots SyntaxHighlighter Python 1`] = `
<div <div
className="sc-bZQynM hAJIOS" className="sc-bZQynM hAJIOS"

View File

@@ -1,78 +0,0 @@
// @flow
import { apiClient, createUrl } from "./apiclient";
import fetchMock from "fetch-mock";
import { BackendError } from "./errors";
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/v2/users");
expect(createUrl("users")).toBe("/api/v2/users");
});
});
describe("error handling tests", () => {
const earthNotFoundError = {
transactionId: "42t",
errorCode: "42e",
message: "earth not found",
context: [{
type: "planet",
id: "earth"
}]
};
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should create a normal error, if the content type is not scmm-error", (done) => {
fetchMock.getOnce("/api/v2/error", {
status: 404
});
apiClient.get("/error")
.catch((err: Error) => {
expect(err.name).toEqual("Error");
expect(err.message).toContain("404");
done();
});
});
it("should create an backend error, if the content type is scmm-error", (done) => {
fetchMock.getOnce("/api/v2/error", {
status: 404,
headers: {
"Content-Type": "application/vnd.scmm-error+json;v=2"
},
body: earthNotFoundError
});
apiClient.get("/error")
.catch((err: BackendError) => {
expect(err).toBeInstanceOf(BackendError);
expect(err.message).toEqual("earth not found");
expect(err.statusCode).toBe(404);
expect(err.transactionId).toEqual("42t");
expect(err.errorCode).toEqual("42e");
expect(err.context).toEqual([{
type: "planet",
id: "earth"
}]);
done();
});
});
});

View File

@@ -0,0 +1,74 @@
import { apiClient, createUrl } from './apiclient';
import fetchMock from 'fetch-mock';
import { BackendError } from './errors';
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/v2/users');
expect(createUrl('users')).toBe('/api/v2/users');
});
});
describe('error handling tests', () => {
const earthNotFoundError = {
transactionId: '42t',
errorCode: '42e',
message: 'earth not found',
context: [
{
type: 'planet',
id: 'earth',
},
],
};
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it('should create a normal error, if the content type is not scmm-error', done => {
fetchMock.getOnce('/api/v2/error', {
status: 404,
});
apiClient.get('/error').catch((err: Error) => {
expect(err.name).toEqual('Error');
expect(err.message).toContain('404');
done();
});
});
it('should create an backend error, if the content type is scmm-error', done => {
fetchMock.getOnce('/api/v2/error', {
status: 404,
headers: {
'Content-Type': 'application/vnd.scmm-error+json;v=2',
},
body: earthNotFoundError,
});
apiClient.get('/error').catch((err: BackendError) => {
expect(err).toBeInstanceOf(BackendError);
expect(err.message).toEqual('earth not found');
expect(err.statusCode).toBe(404);
expect(err.transactionId).toEqual('42t');
expect(err.errorCode).toEqual('42e');
expect(err.context).toEqual([
{
type: 'planet',
id: 'earth',
},
]);
done();
});
});
});

View File

@@ -1,14 +1,18 @@
// @flow import { contextPath } from './urls';
import { contextPath } from "./urls"; import {
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors"; createBackendError,
import type { BackendErrorContent } from "./errors"; ForbiddenError,
isBackendError,
UnauthorizedError,
} from './errors';
import { BackendErrorContent } from './errors';
const applyFetchOptions: (RequestOptions) => RequestOptions = o => { const applyFetchOptions: (p: RequestOptions) => RequestOptions = o => {
o.credentials = "same-origin"; o.credentials = 'same-origin';
o.headers = { o.headers = {
Cache: "no-cache", Cache: 'no-cache',
// identify the request as ajax request // identify the request as ajax request
"X-Requested-With": "XMLHttpRequest" 'X-Requested-With': 'XMLHttpRequest',
}; };
return o; return o;
}; };
@@ -16,30 +20,29 @@ const applyFetchOptions: (RequestOptions) => RequestOptions = o => {
function handleFailure(response: Response) { function handleFailure(response: Response) {
if (!response.ok) { if (!response.ok) {
if (isBackendError(response)) { if (isBackendError(response)) {
return response.json() return response.json().then((content: BackendErrorContent) => {
.then((content: BackendErrorContent) => { throw createBackendError(content, response.status);
throw createBackendError(content, response.status); });
});
} else { } else {
if (response.status === 401) { if (response.status === 401) {
throw new UnauthorizedError("Unauthorized", 401); throw new UnauthorizedError('Unauthorized', 401);
} else if (response.status === 403) { } else if (response.status === 403) {
throw new ForbiddenError("Forbidden", 403); throw new ForbiddenError('Forbidden', 403);
} }
throw new Error("server returned status code " + response.status); throw new Error('server returned status code ' + response.status);
} }
} }
return response; return response;
} }
export function createUrl(url: string) { export function createUrl(url: string) {
if (url.includes("://")) { if (url.includes('://')) {
return url; return url;
} }
let urlWithStartingSlash = url; let urlWithStartingSlash = url;
if (url.indexOf("/") !== 0) { if (url.indexOf('/') !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash; urlWithStartingSlash = '/' + urlWithStartingSlash;
} }
return `${contextPath}/api/v2${urlWithStartingSlash}`; return `${contextPath}/api/v2${urlWithStartingSlash}`;
} }
@@ -49,28 +52,28 @@ class ApiClient {
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
} }
post(url: string, payload: any, contentType: string = "application/json") { post(url: string, payload: any, contentType: string = 'application/json') {
return this.httpRequestWithJSONBody("POST", url, contentType, payload); return this.httpRequestWithJSONBody('POST', url, contentType, payload);
} }
postBinary(url: string, fileAppender: FormData => void) { postBinary(url: string, fileAppender: (p: FormData) => void) {
let formData = new FormData(); let formData = new FormData();
fileAppender(formData); fileAppender(formData);
let options: RequestOptions = { let options: RequestOptions = {
method: "POST", method: 'POST',
body: formData body: formData,
}; };
return this.httpRequestWithBinaryBody(options, url); return this.httpRequestWithBinaryBody(options, url);
} }
put(url: string, payload: any, contentType: string = "application/json") { put(url: string, payload: any, contentType: string = 'application/json') {
return this.httpRequestWithJSONBody("PUT", url, contentType, payload); return this.httpRequestWithJSONBody('PUT', url, contentType, payload);
} }
head(url: string) { head(url: string) {
let options: RequestOptions = { let options: RequestOptions = {
method: "HEAD" method: 'HEAD',
}; };
options = applyFetchOptions(options); options = applyFetchOptions(options);
return fetch(createUrl(url), options).then(handleFailure); return fetch(createUrl(url), options).then(handleFailure);
@@ -78,7 +81,7 @@ class ApiClient {
delete(url: string): Promise<Response> { delete(url: string): Promise<Response> {
let options: RequestOptions = { let options: RequestOptions = {
method: "DELETE" method: 'DELETE',
}; };
options = applyFetchOptions(options); options = applyFetchOptions(options);
return fetch(createUrl(url), options).then(handleFailure); return fetch(createUrl(url), options).then(handleFailure);
@@ -88,20 +91,24 @@ class ApiClient {
method: string, method: string,
url: string, url: string,
contentType: string, contentType: string,
payload: any payload: any,
): Promise<Response> { ): Promise<Response> {
let options: RequestOptions = { let options: RequestOptions = {
method: method, method: method,
body: JSON.stringify(payload) body: JSON.stringify(payload),
}; };
return this.httpRequestWithBinaryBody(options, url, contentType); return this.httpRequestWithBinaryBody(options, url, contentType);
} }
httpRequestWithBinaryBody(options: RequestOptions, url: string, contentType?: string) { httpRequestWithBinaryBody(
options: RequestOptions,
url: string,
contentType?: string,
) {
options = applyFetchOptions(options); options = applyFetchOptions(options);
if (contentType) { if (contentType) {
// $FlowFixMe // $FlowFixMe
options.headers["Content-Type"] = contentType; options.headers['Content-Type'] = contentType;
} }
return fetch(createUrl(url), options).then(handleFailure); return fetch(createUrl(url), options).then(handleFailure);

View File

@@ -1,8 +0,0 @@
// @flow
export type Person = {
name: string,
mail?: string
};
export const EXTENSION_POINT = "avatar.factory";

View File

@@ -0,0 +1,6 @@
export type Person = {
name: string;
mail?: string;
};
export const EXTENSION_POINT = 'avatar.factory';

View File

@@ -1,13 +1,11 @@
//@flow import React from 'react';
import React from "react"; import { binder } from '@scm-manager/ui-extensions';
import {binder} from "@scm-manager/ui-extensions"; import { Image } from '..';
import {Image} from ".."; import { Person } from './Avatar';
import type { Person } from "./Avatar"; import { EXTENSION_POINT } from './Avatar';
import { EXTENSION_POINT } from "./Avatar";
type Props = { type Props = {
person: Person person: Person;
}; };
class AvatarImage extends React.Component<Props> { class AvatarImage extends React.Component<Props> {
@@ -19,11 +17,7 @@ class AvatarImage extends React.Component<Props> {
const avatar = avatarFactory(person); const avatar = avatarFactory(person);
return ( return (
<Image <Image className="has-rounded-border" src={avatar} alt={person.name} />
className="has-rounded-border"
src={avatar}
alt={person.name}
/>
); );
} }

View File

@@ -1,10 +1,9 @@
//@flow import * as React from 'react';
import * as React from "react"; import { binder } from '@scm-manager/ui-extensions';
import {binder} from "@scm-manager/ui-extensions"; import { EXTENSION_POINT } from './Avatar';
import { EXTENSION_POINT } from "./Avatar";
type Props = { type Props = {
children: React.Node children: React.Node;
}; };
class AvatarWrapper extends React.Component<Props> { class AvatarWrapper extends React.Component<Props> {

View File

@@ -1,4 +0,0 @@
// @flow
export { default as AvatarWrapper } from "./AvatarWrapper";
export { default as AvatarImage } from "./AvatarImage";

View File

@@ -0,0 +1,2 @@
export { default as AvatarWrapper } from './AvatarWrapper';
export { default as AvatarImage } from './AvatarImage';

View File

@@ -1,6 +1,5 @@
//@flow import React from 'react';
import React from "react"; import Button, { ButtonProps } from './Button';
import Button, { type ButtonProps } from "./Button";
class AddButton extends React.Component<ButtonProps> { class AddButton extends React.Component<ButtonProps> {
render() { render() {

View File

@@ -1,34 +1,33 @@
//@flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames"; import { withRouter } from 'react-router-dom';
import { withRouter } from "react-router-dom"; import Icon from '../Icon';
import Icon from "../Icon";
export type ButtonProps = { export type ButtonProps = {
label?: string, label?: string;
loading?: boolean, loading?: boolean;
disabled?: boolean, disabled?: boolean;
action?: (event: Event) => void, action?: (event: Event) => void;
link?: string, link?: string;
className?: string, className?: string;
icon?: string, icon?: string;
fullWidth?: boolean, fullWidth?: boolean;
reducedMobile?: boolean, reducedMobile?: boolean;
children?: React.Node children?: React.Node;
}; };
type Props = ButtonProps & { type Props = ButtonProps & {
type: string, type: string;
color: string, color: string;
// context prop // context prop
history: any history: any;
}; };
class Button extends React.Component<Props> { class Button extends React.Component<Props> {
static defaultProps = { static defaultProps = {
type: "button", type: 'button',
color: "default" color: 'default',
}; };
onClick = (event: Event) => { onClick = (event: Event) => {
@@ -51,11 +50,11 @@ class Button extends React.Component<Props> {
icon, icon,
fullWidth, fullWidth,
reducedMobile, reducedMobile,
children children,
} = this.props; } = this.props;
const loadingClass = loading ? "is-loading" : ""; const loadingClass = loading ? 'is-loading' : '';
const fullWidthClass = fullWidth ? "is-fullwidth" : ""; const fullWidthClass = fullWidth ? 'is-fullwidth' : '';
const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : ""; const reducedMobileClass = reducedMobile ? 'is-reduced-mobile' : '';
if (icon) { if (icon) {
return ( return (
<button <button
@@ -63,12 +62,12 @@ class Button extends React.Component<Props> {
disabled={disabled} disabled={disabled}
onClick={this.onClick} onClick={this.onClick}
className={classNames( className={classNames(
"button", 'button',
"is-" + color, 'is-' + color,
loadingClass, loadingClass,
fullWidthClass, fullWidthClass,
reducedMobileClass, reducedMobileClass,
className className,
)} )}
> >
<span className="icon is-medium"> <span className="icon is-medium">
@@ -87,11 +86,11 @@ class Button extends React.Component<Props> {
disabled={disabled} disabled={disabled}
onClick={this.onClick} onClick={this.onClick}
className={classNames( className={classNames(
"button", 'button',
"is-" + color, 'is-' + color,
loadingClass, loadingClass,
fullWidthClass, fullWidthClass,
className className,
)} )}
> >
{label} {children} {label} {children}

View File

@@ -1,7 +1,6 @@
// @flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames"; import styled from 'styled-components';
import styled from "styled-components";
const Flex = styled.div` const Flex = styled.div`
&.field:not(:last-child) { &.field:not(:last-child) {
@@ -10,8 +9,8 @@ const Flex = styled.div`
`; `;
type Props = { type Props = {
className?: string, className?: string;
children: React.Node children: React.Node;
}; };
class ButtonAddons extends React.Component<Props> { class ButtonAddons extends React.Component<Props> {
@@ -24,13 +23,13 @@ class ButtonAddons extends React.Component<Props> {
childWrapper.push( childWrapper.push(
<p className="control" key={childWrapper.length}> <p className="control" key={childWrapper.length}>
{child} {child}
</p> </p>,
); );
} }
}); });
return ( return (
<Flex className={classNames("field", "has-addons", className)}> <Flex className={classNames('field', 'has-addons', className)}>
{childWrapper} {childWrapper}
</Flex> </Flex>
); );

View File

@@ -1,10 +1,9 @@
// @flow import * as React from 'react';
import * as React from "react"; import classNames from 'classnames';
import classNames from "classnames";
type Props = { type Props = {
className?: string, className?: string;
children: React.Node children: React.Node;
}; };
class ButtonGroup extends React.Component<Props> { class ButtonGroup extends React.Component<Props> {
@@ -14,12 +13,16 @@ class ButtonGroup extends React.Component<Props> {
const childWrapper = []; const childWrapper = [];
React.Children.forEach(children, child => { React.Children.forEach(children, child => {
if (child) { if (child) {
childWrapper.push(<div className="control" key={childWrapper.length}>{child}</div>); childWrapper.push(
<div className="control" key={childWrapper.length}>
{child}
</div>,
);
} }
}); });
return ( return (
<div className={classNames("field", "is-grouped", className)}> <div className={classNames('field', 'is-grouped', className)}>
{childWrapper} {childWrapper}
</div> </div>
); );

View File

@@ -1,7 +1,6 @@
//@flow import React from 'react';
import React from "react"; import styled from 'styled-components';
import styled from "styled-components"; import Button, { ButtonProps } from './Button';
import Button, { type ButtonProps } from "./Button";
const Wrapper = styled.div` const Wrapper = styled.div`
margin-top: 2em; margin-top: 2em;

View File

@@ -1,6 +1,5 @@
//@flow import React from 'react';
import React from "react"; import Button, { ButtonProps } from './Button';
import Button, { type ButtonProps } from "./Button";
class DeleteButton extends React.Component<ButtonProps> { class DeleteButton extends React.Component<ButtonProps> {
render() { render() {

View File

@@ -1,11 +1,10 @@
//@flow import React from 'react';
import React from "react";
type Props = { type Props = {
displayName: string, displayName: string;
url: string, url: string;
disabled: boolean, disabled: boolean;
onClick?: () => void onClick?: () => void;
}; };
class DownloadButton extends React.Component<Props> { class DownloadButton extends React.Component<Props> {
@@ -13,7 +12,12 @@ class DownloadButton extends React.Component<Props> {
const { displayName, url, disabled, onClick } = this.props; const { displayName, url, disabled, onClick } = this.props;
const onClickOrDefault = !!onClick ? onClick : () => {}; const onClickOrDefault = !!onClick ? onClick : () => {};
return ( return (
<a className="button is-link" href={url} disabled={disabled} onClick={onClickOrDefault}> <a
className="button is-link"
href={url}
disabled={disabled}
onClick={onClickOrDefault}
>
<span className="icon is-medium"> <span className="icon is-medium">
<i className="fas fa-arrow-circle-down" /> <i className="fas fa-arrow-circle-down" />
</span> </span>

View File

@@ -1,6 +1,5 @@
//@flow import React from 'react';
import React from "react"; import Button, { ButtonProps } from './Button';
import Button, { type ButtonProps } from "./Button";
class EditButton extends React.Component<ButtonProps> { class EditButton extends React.Component<ButtonProps> {
render() { render() {

View File

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

View File

@@ -1,14 +1,13 @@
//@flow import React from 'react';
import React from "react"; import Button, { ButtonProps } from './Button';
import Button, { type ButtonProps } from "./Button";
type SubmitButtonProps = ButtonProps & { type SubmitButtonProps = ButtonProps & {
scrollToTop: boolean scrollToTop: boolean;
} };
class SubmitButton extends React.Component<SubmitButtonProps> { class SubmitButton extends React.Component<SubmitButtonProps> {
static defaultProps = { static defaultProps = {
scrollToTop: true scrollToTop: true,
}; };
render() { render() {
@@ -18,7 +17,7 @@ class SubmitButton extends React.Component<SubmitButtonProps> {
type="submit" type="submit"
color="primary" color="primary"
{...this.props} {...this.props}
action={(event) => { action={event => {
if (action) { if (action) {
action(event); action(event);
} }

View File

@@ -1,14 +0,0 @@
// @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 SubmitButton } from "./SubmitButton.js";
export { default as DownloadButton } from "./DownloadButton.js";
export { default as ButtonGroup } from "./ButtonGroup.js";
export { default as ButtonAddons } from "./ButtonAddons.js";
export {
default as RemoveEntryOfTableButton
} from "./RemoveEntryOfTableButton.js";

View File

@@ -1,75 +0,0 @@
// @flow
import React, { type Node } from "react";
import Button from "./Button";
import { storiesOf } from "@storybook/react";
import styled from "styled-components";
import { MemoryRouter } from "react-router-dom";
import AddButton from "./AddButton";
import CreateButton from "./CreateButton";
import DeleteButton from "./DeleteButton";
import DownloadButton from "./DownloadButton";
import EditButton from "./EditButton";
import SubmitButton from "./SubmitButton";
const colors = [
"primary",
"link",
"info",
"success",
"warning",
"danger",
"white",
"light",
"dark",
"black",
"text"
];
const Spacing = styled.div`
padding: 1em;
`;
const RoutingDecorator = story => (
<MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>
);
const SpacingDecorator = story => <Spacing>{story()}</Spacing>;
storiesOf("Buttons|Button", module)
.addDecorator(RoutingDecorator)
.add("Colors", () => (
<div>
{colors.map(color => (
<Spacing key={color}>
<Button color={color} label={color} />
</Spacing>
))}
</div>
))
.add("Loading", () => (
<Spacing>
<Button color={"primary"} loading={true}>
Loading Button
</Button>
</Spacing>
));
const buttonStory = (name: string, storyFn: () => Node) => {
return storiesOf("Buttons|" + name, module)
.addDecorator(RoutingDecorator)
.addDecorator(SpacingDecorator)
.add("Default", storyFn);
};
buttonStory("AddButton", () => <AddButton color={"primary"}>Add</AddButton>);
buttonStory("CreateButton", () => <CreateButton>Create</CreateButton>);
buttonStory("DeleteButton", () => <DeleteButton>Delete</DeleteButton>);
buttonStory("DownloadButton", () => (
<DownloadButton
displayName="Download"
disabled={false}
url=""
></DownloadButton>
));
buttonStory("EditButton", () => <EditButton>Edit</EditButton>);
buttonStory("SubmitButton", () => <SubmitButton>Submit</SubmitButton>);

View File

@@ -0,0 +1,74 @@
import React, { ReactNode } from 'react';
import Button from './Button';
import { storiesOf } from '@storybook/react';
import styled from 'styled-components';
import { MemoryRouter } from 'react-router-dom';
import AddButton from './AddButton';
import CreateButton from './CreateButton';
import DeleteButton from './DeleteButton';
import DownloadButton from './DownloadButton';
import EditButton from './EditButton';
import SubmitButton from './SubmitButton';
const colors = [
'primary',
'link',
'info',
'success',
'warning',
'danger',
'white',
'light',
'dark',
'black',
'text',
];
const Spacing = styled.div`
padding: 1em;
`;
const RoutingDecorator = story => (
<MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>
);
const SpacingDecorator = story => <Spacing>{story()}</Spacing>;
storiesOf('Buttons|Button', module)
.addDecorator(RoutingDecorator)
.add('Colors', () => (
<div>
{colors.map(color => (
<Spacing key={color}>
<Button color={color} label={color} />
</Spacing>
))}
</div>
))
.add('Loading', () => (
<Spacing>
<Button color={'primary'} loading={true}>
Loading Button
</Button>
</Spacing>
));
const buttonStory = (name: string, storyFn: () => ReactNode) => {
return storiesOf('Buttons|' + name, module)
.addDecorator(RoutingDecorator)
.addDecorator(SpacingDecorator)
.add('Default', storyFn);
};
buttonStory('AddButton', () => <AddButton color={'primary'}>Add</AddButton>);
buttonStory('CreateButton', () => <CreateButton>Create</CreateButton>);
buttonStory('DeleteButton', () => <DeleteButton>Delete</DeleteButton>);
buttonStory('DownloadButton', () => (
<DownloadButton
displayName="Download"
disabled={false}
url=""
></DownloadButton>
));
buttonStory('EditButton', () => <EditButton>Edit</EditButton>);
buttonStory('SubmitButton', () => <SubmitButton>Submit</SubmitButton>);

View File

@@ -0,0 +1,14 @@
// @create-index
export { default as AddButton } from './AddButton';
export { default as Button } from './Button';
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 DownloadButton } from './DownloadButton';
export { default as ButtonGroup } from './ButtonGroup';
export { default as ButtonAddons } from './ButtonAddons';
export {
default as RemoveEntryOfTableButton,
} from './RemoveEntryOfTableButton';

View File

@@ -1,37 +1,36 @@
//@flow import React from 'react';
import React from "react"; import { translate } from 'react-i18next';
import { translate } from "react-i18next"; import { Links } from '@scm-manager/ui-types';
import type { Links } from "@scm-manager/ui-types"; import { apiClient, SubmitButton, Loading, ErrorNotification } from '../';
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../";
type RenderProps = { type RenderProps = {
readOnly: boolean, readOnly: boolean;
initialConfiguration: ConfigurationType, initialConfiguration: ConfigurationType;
onConfigurationChange: (ConfigurationType, boolean) => void onConfigurationChange: (p1: ConfigurationType, p2: boolean) => void;
}; };
type Props = { type Props = {
link: string, link: string;
render: (props: RenderProps) => any, // ??? render: (props: RenderProps) => any; // ???
// context props // context props
t: string => string t: (p: string) => string;
}; };
type ConfigurationType = { type ConfigurationType = {
_links: Links _links: Links;
} & Object; } & object;
type State = { type State = {
error?: Error, error?: Error;
fetching: boolean, fetching: boolean;
modifying: boolean, modifying: boolean;
contentType?: string, contentType?: string;
configChanged: boolean, configChanged: boolean;
configuration?: ConfigurationType, configuration?: ConfigurationType;
modifiedConfiguration?: ConfigurationType, modifiedConfiguration?: ConfigurationType;
valid: boolean valid: boolean;
}; };
/** /**
@@ -45,7 +44,7 @@ class Configuration extends React.Component<Props, State> {
fetching: true, fetching: true,
modifying: false, modifying: false,
configChanged: false, configChanged: false,
valid: false valid: false,
}; };
} }
@@ -61,23 +60,23 @@ class Configuration extends React.Component<Props, State> {
} }
captureContentType = (response: Response) => { captureContentType = (response: Response) => {
const contentType = response.headers.get("Content-Type"); const contentType = response.headers.get('Content-Type');
this.setState({ this.setState({
contentType contentType,
}); });
return response; return response;
}; };
getContentType = (): string => { getContentType = (): string => {
const { contentType } = this.state; const { contentType } = this.state;
return contentType ? contentType : "application/json"; return contentType ? contentType : 'application/json';
}; };
handleError = (error: Error) => { handleError = (error: Error) => {
this.setState({ this.setState({
error, error,
fetching: false, fetching: false,
modifying: false modifying: false,
}); });
}; };
@@ -85,11 +84,11 @@ class Configuration extends React.Component<Props, State> {
this.setState({ this.setState({
configuration, configuration,
fetching: false, fetching: false,
error: undefined error: undefined,
}); });
}; };
getModificationUrl = (): ?string => { getModificationUrl = (): string | null | undefined => {
const { configuration } = this.state; const { configuration } = this.state;
if (configuration) { if (configuration) {
const links = configuration._links; const links = configuration._links;
@@ -107,14 +106,16 @@ class Configuration extends React.Component<Props, State> {
configurationChanged = (configuration: ConfigurationType, valid: boolean) => { configurationChanged = (configuration: ConfigurationType, valid: boolean) => {
this.setState({ this.setState({
modifiedConfiguration: configuration, modifiedConfiguration: configuration,
valid valid,
}); });
}; };
modifyConfiguration = (event: Event) => { modifyConfiguration = (event: Event) => {
event.preventDefault(); event.preventDefault();
this.setState({ modifying: true }); this.setState({
modifying: true,
});
const { modifiedConfiguration } = this.state; const { modifiedConfiguration } = this.state;
@@ -122,9 +123,15 @@ class Configuration extends React.Component<Props, State> {
.put( .put(
this.getModificationUrl(), this.getModificationUrl(),
modifiedConfiguration, modifiedConfiguration,
this.getContentType() this.getContentType(),
)
.then(() =>
this.setState({
modifying: false,
configChanged: true,
valid: false,
}),
) )
.then(() => this.setState({ modifying: false, configChanged: true, valid: false }))
.catch(this.handleError); .catch(this.handleError);
}; };
@@ -134,9 +141,13 @@ class Configuration extends React.Component<Props, State> {
<div className="notification is-primary"> <div className="notification is-primary">
<button <button
className="delete" className="delete"
onClick={() => this.setState({ configChanged: false })} onClick={() =>
this.setState({
configChanged: false,
})
}
/> />
{this.props.t("config.form.submit-success-notification")} {this.props.t('config.form.submit-success-notification')}
</div> </div>
); );
} }
@@ -157,7 +168,7 @@ class Configuration extends React.Component<Props, State> {
const renderProps: RenderProps = { const renderProps: RenderProps = {
readOnly, readOnly,
initialConfiguration: configuration, initialConfiguration: configuration,
onConfigurationChange: this.configurationChanged onConfigurationChange: this.configurationChanged,
}; };
return ( return (
@@ -167,7 +178,7 @@ class Configuration extends React.Component<Props, State> {
{this.props.render(renderProps)} {this.props.render(renderProps)}
<hr /> <hr />
<SubmitButton <SubmitButton
label={t("config.form.submit")} label={t('config.form.submit')}
disabled={!valid || readOnly} disabled={!valid || readOnly}
loading={modifying} loading={modifying}
/> />
@@ -178,4 +189,4 @@ class Configuration extends React.Component<Props, State> {
} }
} }
export default translate("config")(Configuration); export default translate('config')(Configuration);

View File

@@ -1,104 +0,0 @@
// @flow
import React from "react";
import { binder } from "@scm-manager/ui-extensions";
import { NavLink } from "../navigation";
import { Route } from "react-router-dom";
import { translate } from "react-i18next";
class ConfigurationBinder {
i18nNamespace: string = "plugins";
navLink(to: string, labelI18nKey: string, t: any){
return <NavLink to={to} label={t(labelI18nKey)} />;
}
route(path: string, Component: any){
return <Route path={path}
render={() => Component}
exact/>;
}
bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) {
// create predicate based on the link name of the index resource
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const configPredicate = (props: Object) => {
return props.links && props.links[linkName];
};
// create NavigationLink with translated label
const ConfigNavLink = translate(this.i18nNamespace)(({t}) => {
return this.navLink("/admin/settings" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("admin.setting", ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links, ...additionalProps }) => {
const link = links[linkName].href;
return this.route(url + "/settings" + to, <ConfigurationComponent link={link} {...additionalProps} />);
};
// bind config route to extension point
binder.bind("admin.route", ConfigRoute, configPredicate);
}
bindRepository(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.navigation", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + "/settings" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.setting", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + "/settings" + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { binder } from '@scm-manager/ui-extensions';
import { NavLink } from '../navigation';
import { Route } from 'react-router-dom';
import { translate } from 'react-i18next';
class ConfigurationBinder {
i18nNamespace: string = 'plugins';
navLink(to: string, labelI18nKey: string, t: any) {
return <NavLink to={to} label={t(labelI18nKey)} />;
}
route(path: string, Component: any) {
return <Route path={path} render={() => Component} exact />;
}
bindGlobal(
to: string,
labelI18nKey: string,
linkName: string,
ConfigurationComponent: any,
) {
// create predicate based on the link name of the index resource
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const configPredicate = (props: object) => {
return props.links && props.links[linkName];
};
// create NavigationLink with translated label
const ConfigNavLink = translate(this.i18nNamespace)(({ t }) => {
return this.navLink('/admin/settings' + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind('admin.setting', ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links, ...additionalProps }) => {
const link = links[linkName].href;
return this.route(
url + '/settings' + to,
<ConfigurationComponent link={link} {...additionalProps} />,
);
};
// bind config route to extension point
binder.bind('admin.route', ConfigRoute, configPredicate);
}
bindRepository(
to: string,
labelI18nKey: string,
linkName: string,
RepositoryComponent: any,
) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: object) => {
return (
props.repository &&
props.repository._links &&
props.repository._links[linkName]
);
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({ t, url }) => {
return this.navLink(url + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind('repository.navigation', RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({ url, repository, ...additionalProps }) => {
const link = repository._links[linkName].href;
return this.route(
url + to,
<RepositoryComponent
repository={repository}
link={link}
{...additionalProps}
/>,
);
};
// bind config route to extension point
binder.bind('repository.route', RepoRoute, repoPredicate);
}
bindRepositorySetting(
to: string,
labelI18nKey: string,
linkName: string,
RepositoryComponent: any,
) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: object) => {
return (
props.repository &&
props.repository._links &&
props.repository._links[linkName]
);
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({ t, url }) => {
return this.navLink(url + '/settings' + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind('repository.setting', RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({ url, repository, ...additionalProps }) => {
const link = repository._links[linkName].href;
return this.route(
url + '/settings' + to,
<RepositoryComponent
repository={repository}
link={link}
{...additionalProps}
/>,
);
};
// bind config route to extension point
binder.bind('repository.route', RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -1,3 +0,0 @@
// @flow
export { default as ConfigurationBinder } from "./ConfigurationBinder";
export { default as Configuration } from "./Configuration";

View File

@@ -0,0 +1,2 @@
export { default as ConfigurationBinder } from './ConfigurationBinder';
export { default as Configuration } from './Configuration';

View File

@@ -1,37 +0,0 @@
// @flow
import { BackendError, UnauthorizedError, createBackendError, NotFoundError } from "./errors";
describe("test createBackendError", () => {
const earthNotFoundError = {
transactionId: "42t",
errorCode: "42e",
message: "earth not found",
context: [{
type: "planet",
id: "earth"
}],
violations: []
};
it("should return a default backend error", () => {
const err = createBackendError(earthNotFoundError, 500);
expect(err).toBeInstanceOf(BackendError);
expect(err.name).toBe("BackendError");
});
// 403 is no backend error
xit("should return an unauthorized error for status code 403", () => {
const err = createBackendError(earthNotFoundError, 403);
expect(err).toBeInstanceOf(UnauthorizedError);
expect(err.name).toBe("UnauthorizedError");
});
it("should return an not found error for status code 404", () => {
const err = createBackendError(earthNotFoundError, 404);
expect(err).toBeInstanceOf(NotFoundError);
expect(err.name).toBe("NotFoundError");
});
});

View File

@@ -0,0 +1,40 @@
import {
BackendError,
UnauthorizedError,
createBackendError,
NotFoundError,
} from './errors';
describe('test createBackendError', () => {
const earthNotFoundError = {
transactionId: '42t',
errorCode: '42e',
message: 'earth not found',
context: [
{
type: 'planet',
id: 'earth',
},
],
violations: [],
};
it('should return a default backend error', () => {
const err = createBackendError(earthNotFoundError, 500);
expect(err).toBeInstanceOf(BackendError);
expect(err.name).toBe('BackendError');
});
// 403 is no backend error
xit('should return an unauthorized error for status code 403', () => {
const err = createBackendError(earthNotFoundError, 403);
expect(err).toBeInstanceOf(UnauthorizedError);
expect(err.name).toBe('UnauthorizedError');
});
it('should return an not found error for status code 404', () => {
const err = createBackendError(earthNotFoundError, 404);
expect(err).toBeInstanceOf(NotFoundError);
expect(err.name).toBe('NotFoundError');
});
});

View File

@@ -1,20 +1,25 @@
// @flow type Context = {
type Context = { type: string, id: string }[]; type: string;
type Violation = { path: string, message: string }; id: string;
}[];
type Violation = {
path: string;
message: string;
};
export type BackendErrorContent = { export type BackendErrorContent = {
transactionId: string, transactionId: string;
errorCode: string, errorCode: string;
message: string, message: string;
url?: string, url?: string;
context: Context, context: Context;
violations: Violation[] violations: Violation[];
}; };
export class BackendError extends Error { export class BackendError extends Error {
transactionId: string; transactionId: string;
errorCode: string; errorCode: string;
url: ?string; url: string | null | undefined;
context: Context = []; context: Context = [];
statusCode: number; statusCode: number;
violations: Violation[]; violations: Violation[];
@@ -49,19 +54,19 @@ export class ForbiddenError extends Error {
export class NotFoundError extends BackendError { export class NotFoundError extends BackendError {
constructor(content: BackendErrorContent, statusCode: number) { constructor(content: BackendErrorContent, statusCode: number) {
super(content, "NotFoundError", statusCode); super(content, 'NotFoundError', statusCode);
} }
} }
export class ConflictError extends BackendError { export class ConflictError extends BackendError {
constructor(content: BackendErrorContent, statusCode: number) { constructor(content: BackendErrorContent, statusCode: number) {
super(content, "ConflictError", statusCode); super(content, 'ConflictError', statusCode);
} }
} }
export function createBackendError( export function createBackendError(
content: BackendErrorContent, content: BackendErrorContent,
statusCode: number statusCode: number,
) { ) {
switch (statusCode) { switch (statusCode) {
case 404: case 404:
@@ -69,13 +74,13 @@ export function createBackendError(
case 409: case 409:
return new ConflictError(content, statusCode); return new ConflictError(content, statusCode);
default: default:
return new BackendError(content, "BackendError", statusCode); return new BackendError(content, 'BackendError', statusCode);
} }
} }
export function isBackendError(response: Response) { export function isBackendError(response: Response) {
return ( return (
response.headers.get("Content-Type") === response.headers.get('Content-Type') ===
"application/vnd.scmm-error+json;v=2" 'application/vnd.scmm-error+json;v=2'
); );
} }

View File

@@ -1,34 +1,37 @@
//@flow import React from 'react';
import React from "react";
import { AddButton } from "../buttons"; import { AddButton } from '../buttons';
import InputField from "./InputField"; import InputField from './InputField';
type Props = { type Props = {
addEntry: string => void, addEntry: (p: string) => void;
disabled: boolean, disabled: boolean;
buttonLabel: string, buttonLabel: string;
fieldLabel: string, fieldLabel: string;
errorMessage: string, errorMessage: string;
helpText?: string, helpText?: string;
validateEntry?: string => boolean validateEntry?: (p: string) => boolean;
}; };
type State = { type State = {
entryToAdd: string entryToAdd: string;
}; };
class AddEntryToTableField extends React.Component<Props, State> { class AddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
entryToAdd: "" entryToAdd: '',
}; };
} }
isValid = () => { isValid = () => {
const {validateEntry} = this.props; const { validateEntry } = this.props;
if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) { if (
!this.state.entryToAdd ||
this.state.entryToAdd === '' ||
!validateEntry
) {
return true; return true;
} else { } else {
return validateEntry(this.state.entryToAdd); return validateEntry(this.state.entryToAdd);
@@ -41,7 +44,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
buttonLabel, buttonLabel,
fieldLabel, fieldLabel,
errorMessage, errorMessage,
helpText helpText,
} = this.props; } = this.props;
return ( return (
<div className="field"> <div className="field">
@@ -58,7 +61,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
<AddButton <AddButton
label={buttonLabel} label={buttonLabel}
action={this.addButtonClicked} action={this.addButtonClicked}
disabled={disabled || this.state.entryToAdd ==="" || !this.isValid()} disabled={disabled || this.state.entryToAdd === '' || !this.isValid()}
/> />
</div> </div>
); );
@@ -72,13 +75,16 @@ class AddEntryToTableField extends React.Component<Props, State> {
appendEntry = () => { appendEntry = () => {
const { entryToAdd } = this.state; const { entryToAdd } = this.state;
this.props.addEntry(entryToAdd); this.props.addEntry(entryToAdd);
this.setState({ ...this.state, entryToAdd: "" }); this.setState({
...this.state,
entryToAdd: '',
});
}; };
handleAddEntryChange = (entryname: string) => { handleAddEntryChange = (entryname: string) => {
this.setState({ this.setState({
...this.state, ...this.state,
entryToAdd: entryname entryToAdd: entryname,
}); });
}; };
} }

View File

@@ -1,30 +1,31 @@
//@flow import React from 'react';
import React from "react";
import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; import { AutocompleteObject, SelectValue } from '@scm-manager/ui-types';
import Autocomplete from "../Autocomplete"; import Autocomplete from '../Autocomplete';
import AddButton from "../buttons/AddButton"; import AddButton from '../buttons/AddButton';
type Props = { type Props = {
addEntry: SelectValue => void, addEntry: (p: SelectValue) => void;
disabled: boolean, disabled: boolean;
buttonLabel: string, buttonLabel: string;
fieldLabel: string, fieldLabel: string;
helpText?: string, helpText?: string;
loadSuggestions: string => Promise<AutocompleteObject>, loadSuggestions: (p: string) => Promise<AutocompleteObject>;
placeholder?: string, placeholder?: string;
loadingMessage?: string, loadingMessage?: string;
noOptionsMessage?: string noOptionsMessage?: string;
}; };
type State = { type State = {
selectedValue?: SelectValue selectedValue?: SelectValue;
}; };
class AutocompleteAddEntryToTableField extends React.Component<Props, State> { class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { selectedValue: undefined }; this.state = {
selectedValue: undefined,
};
} }
render() { render() {
const { const {
@@ -35,7 +36,7 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
loadSuggestions, loadSuggestions,
placeholder, placeholder,
loadingMessage, loadingMessage,
noOptionsMessage noOptionsMessage,
} = this.props; } = this.props;
const { selectedValue } = this.state; const { selectedValue } = this.state;
@@ -73,15 +74,19 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
return; return;
} }
// $FlowFixMe null is needed to clear the selection; undefined does not work // $FlowFixMe null is needed to clear the selection; undefined does not work
this.setState({ ...this.state, selectedValue: null }, () => this.setState(
this.props.addEntry(selectedValue) {
...this.state,
selectedValue: null,
},
() => this.props.addEntry(selectedValue),
); );
}; };
handleAddEntryChange = (selection: SelectValue) => { handleAddEntryChange = (selection: SelectValue) => {
this.setState({ this.setState({
...this.state, ...this.state,
selectedValue: selection selectedValue: selection,
}); });
}; };
} }

View File

@@ -1,18 +1,16 @@
//@flow import React from 'react';
import React from "react"; import { Help } from '../index';
import { Help } from "../index";
type Props = { type Props = {
label?: string, label?: string;
name?: string, name?: string;
checked: boolean, checked: boolean;
onChange?: (value: boolean, name?: string) => void, onChange?: (value: boolean, name?: string) => void;
disabled?: boolean, disabled?: boolean;
helpText?: string helpText?: string;
}; };
class Checkbox extends React.Component<Props> { class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => { onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(event.target.checked, this.props.name); this.props.onChange(event.target.checked, this.props.name);
@@ -36,8 +34,7 @@ class Checkbox extends React.Component<Props> {
checked={this.props.checked} checked={this.props.checked}
onChange={this.onCheckboxChange} onChange={this.onCheckboxChange}
disabled={this.props.disabled} disabled={this.props.disabled}
/> />{' '}
{" "}
{this.props.label} {this.props.label}
{this.renderHelp()} {this.renderHelp()}
</label> </label>

View File

@@ -1,43 +0,0 @@
// @flow
import React from "react";
import classNames from "classnames";
type Props = {
options: string[],
optionValues?: string[],
optionSelected: string => void,
preselectedOption?: string,
className: any,
disabled?: boolean
};
class DropDown extends React.Component<Props> {
render() {
const { options, optionValues, preselectedOption, className, disabled } = this.props;
return (
<div className={classNames(className, "select", disabled ? "disabled": "")}>
<select
value={preselectedOption ? preselectedOption : ""}
onChange={this.change}
disabled={disabled}
>
<option key="" />
{options.map((option, index) => {
return (
<option key={option} value={optionValues && optionValues[index] ? optionValues[index] : option}>
{option}
</option>
);
})}
</select>
</div>
);
}
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.optionSelected(event.target.value);
};
}
export default DropDown;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import classNames from 'classnames';
type Props = {
options: string[];
optionValues?: string[];
optionSelected: (p: string) => void;
preselectedOption?: string;
className: any;
disabled?: boolean;
};
class DropDown extends React.Component<Props> {
render() {
const {
options,
optionValues,
preselectedOption,
className,
disabled,
} = this.props;
return (
<div
className={classNames(className, 'select', disabled ? 'disabled' : '')}
>
<select
value={preselectedOption ? preselectedOption : ''}
onChange={this.change}
disabled={disabled}
>
<option key="" />
{options.map((option, index) => {
return (
<option
key={option}
value={
optionValues && optionValues[index]
? optionValues[index]
: option
}
>
{option}
</option>
);
})}
</select>
</div>
);
}
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.optionSelected(event.target.value);
};
}
export default DropDown;

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