mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 11:35:57 +01:00
use reflow to migrate from flow to typescript
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"deploy": "ui-scripts publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-reflow": "^0.2.7",
|
||||
"lerna": "^3.17.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -2,7 +2,8 @@ module.exports = () => ({
|
||||
presets: [
|
||||
require("@babel/preset-env"),
|
||||
require("@babel/preset-flow"),
|
||||
require("@babel/preset-react")
|
||||
require("@babel/preset-react"),
|
||||
require("@babel/preset-typescript")
|
||||
],
|
||||
plugins: [
|
||||
require("@babel/plugin-proposal-class-properties"),
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@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": {
|
||||
"access": "public"
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
path.join(root, "src")
|
||||
],
|
||||
transform: {
|
||||
"^.+\\.js$": "@scm-manager/jest-preset"
|
||||
"^.+\\.(ts|tsx|js)$": "@scm-manager/jest-preset"
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"node_modules/(?!(@scm-manager)/)"
|
||||
@@ -39,7 +39,7 @@ module.exports = {
|
||||
setupFiles: [path.resolve(__dirname, "src", "setup.js")],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{js,jsx}"
|
||||
"src/**/*.{ts,tsx,js,jsx}"
|
||||
],
|
||||
coverageDirectory: path.join(reportDirectory, "coverage-" + name),
|
||||
coveragePathIgnorePatterns: [
|
||||
|
||||
@@ -25,4 +25,4 @@ addDecorator(
|
||||
})
|
||||
);
|
||||
|
||||
configure(require.context("../src", true, /\.stories\.js$/), module);
|
||||
configure(require.context("../src", true, /\.stories\.tsx?$/), module);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@scm-manager/ui-components",
|
||||
"version": "2.0.0-SNAPSHOT",
|
||||
"description": "UI Components for SCM-Manager and its plugins",
|
||||
"main": "src/index.js",
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
@@ -13,8 +13,8 @@
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"storybook": "start-storybook -s ../ui-webapp/public",
|
||||
"storyshots": "jest --testPathPattern=\"storyshots.test.js\" --collectCoverage=false",
|
||||
"update-storyshots": "jest --testPathPattern=\"storyshots.test.js\" --collectCoverage=false -u"
|
||||
"storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false",
|
||||
"update-storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false -u"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {Async, AsyncCreatable} from "react-select";
|
||||
import type {AutocompleteObject, SelectValue} from "@scm-manager/ui-types";
|
||||
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
||||
import React from 'react';
|
||||
import { Async, AsyncCreatable } from 'react-select';
|
||||
import { AutocompleteObject, SelectValue } from '@scm-manager/ui-types';
|
||||
import LabelWithHelpIcon from './forms/LabelWithHelpIcon';
|
||||
|
||||
type Props = {
|
||||
loadSuggestions: string => Promise<AutocompleteObject>,
|
||||
valueSelected: SelectValue => void,
|
||||
label: string,
|
||||
helpText?: string,
|
||||
value?: SelectValue,
|
||||
placeholder: string,
|
||||
loadingMessage: string,
|
||||
noOptionsMessage: string,
|
||||
creatable?: boolean
|
||||
loadSuggestions: (p: string) => Promise<AutocompleteObject>;
|
||||
valueSelected: (p: SelectValue) => void;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
value?: SelectValue;
|
||||
placeholder: string;
|
||||
loadingMessage: string;
|
||||
noOptionsMessage: string;
|
||||
creatable?: boolean;
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
class Autocomplete extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
placeholder: "Type here",
|
||||
loadingMessage: "Loading...",
|
||||
noOptionsMessage: "No suggestion available"
|
||||
placeholder: 'Type here',
|
||||
loadingMessage: 'Loading...',
|
||||
noOptionsMessage: 'No suggestion available',
|
||||
};
|
||||
|
||||
handleInputChange = (newValue: SelectValue) => {
|
||||
@@ -33,12 +32,12 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
isValidNewOption = (
|
||||
inputValue: string,
|
||||
selectValue: SelectValue,
|
||||
selectOptions: SelectValue[]
|
||||
selectOptions: SelectValue[],
|
||||
) => {
|
||||
const isNotDuplicated = !selectOptions
|
||||
.map(option => option.label)
|
||||
.includes(inputValue);
|
||||
const isNotEmpty = inputValue !== "";
|
||||
const isNotEmpty = inputValue !== '';
|
||||
return isNotEmpty && isNotDuplicated;
|
||||
};
|
||||
|
||||
@@ -51,7 +50,7 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
loadingMessage,
|
||||
noOptionsMessage,
|
||||
loadSuggestions,
|
||||
creatable
|
||||
creatable,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
@@ -70,7 +69,10 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
onCreateOption={value => {
|
||||
this.handleInputChange({
|
||||
label: value,
|
||||
value: { id: value, displayName: value }
|
||||
value: {
|
||||
id: value,
|
||||
displayName: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { BackendError } from "./errors";
|
||||
import Notification from "./Notification";
|
||||
import React from 'react';
|
||||
import { BackendError } from './errors';
|
||||
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> {
|
||||
constructor(props: Props) {
|
||||
@@ -27,7 +29,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
|
||||
renderErrorName = () => {
|
||||
const { error, t } = this.props;
|
||||
const translation = t("errors." + error.errorCode + ".displayName");
|
||||
const translation = t('errors.' + error.errorCode + '.displayName');
|
||||
if (translation === error.errorCode) {
|
||||
return error.message;
|
||||
}
|
||||
@@ -36,9 +38,9 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
|
||||
renderErrorDescription = () => {
|
||||
const { error, t } = this.props;
|
||||
const translation = t("errors." + error.errorCode + ".description");
|
||||
const translation = t('errors.' + error.errorCode + '.description');
|
||||
if (translation === error.errorCode) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
@@ -49,7 +51,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<strong>{t("errors.violations")}</strong>
|
||||
<strong>{t('errors.violations')}</strong>
|
||||
</p>
|
||||
<ul>
|
||||
{error.violations.map((violation, index) => {
|
||||
@@ -73,10 +75,10 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
{this.renderMoreInformationLink()}
|
||||
<div className="level is-size-7">
|
||||
<div className="left">
|
||||
{t("errors.transactionId")} {error.transactionId}
|
||||
{t('errors.transactionId')} {error.transactionId}
|
||||
</div>
|
||||
<div className="right">
|
||||
{t("errors.errorCode")} {error.errorCode}
|
||||
{t('errors.errorCode')} {error.errorCode}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -89,7 +91,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<strong>{t("errors.context")}</strong>
|
||||
<strong>{t('errors.context')}</strong>
|
||||
</p>
|
||||
<ul>
|
||||
{error.context.map((context, index) => {
|
||||
@@ -110,7 +112,7 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
if (error.url) {
|
||||
return (
|
||||
<p>
|
||||
{t("errors.moreInfo")}{" "}
|
||||
{t('errors.moreInfo')}{' '}
|
||||
<a href={error.url} target="_blank">
|
||||
{error.errorCode}
|
||||
</a>
|
||||
@@ -120,4 +122,4 @@ class BackendErrorNotification extends React.Component<Props> {
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("plugins")(BackendErrorNotification);
|
||||
export default translate('plugins')(BackendErrorNotification);
|
||||
@@ -1,19 +1,20 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import DropDown from "./forms/DropDown";
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { Branch } from '@scm-manager/ui-types';
|
||||
import DropDown from './forms/DropDown';
|
||||
|
||||
type Props = {
|
||||
branches: Branch[],
|
||||
selected: (branch?: Branch) => void,
|
||||
selectedBranch?: string,
|
||||
label: string,
|
||||
disabled?: boolean
|
||||
branches: Branch[];
|
||||
selected: (branch?: Branch) => void;
|
||||
selectedBranch?: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type State = { selectedBranch?: Branch };
|
||||
type State = {
|
||||
selectedBranch?: Branch;
|
||||
};
|
||||
|
||||
const ZeroflexFieldLabel = styled.div`
|
||||
flex-basis: inherit;
|
||||
@@ -38,9 +39,11 @@ export default class BranchSelector extends React.Component<Props, State> {
|
||||
const { branches } = this.props;
|
||||
if (branches) {
|
||||
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) {
|
||||
return (
|
||||
<div className={classNames("field", "is-horizontal")}>
|
||||
<div className={classNames('field', 'is-horizontal')}>
|
||||
<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>
|
||||
<div className="field-body">
|
||||
<NoBottomMarginField className={classNames("field", "is-narrow")}>
|
||||
<NoBottomMarginField className={classNames('field', 'is-narrow')}>
|
||||
<MinWidthControl className="control">
|
||||
<DropDown
|
||||
className="is-fullwidth"
|
||||
@@ -66,7 +69,7 @@ export default class BranchSelector extends React.Component<Props, State> {
|
||||
preselectedOption={
|
||||
this.state.selectedBranch
|
||||
? this.state.selectedBranch.name
|
||||
: ""
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</MinWidthControl>
|
||||
@@ -83,13 +86,17 @@ export default class BranchSelector extends React.Component<Props, State> {
|
||||
const { branches, selected } = this.props;
|
||||
|
||||
if (!branchName) {
|
||||
this.setState({ selectedBranch: undefined });
|
||||
this.setState({
|
||||
selectedBranch: undefined,
|
||||
});
|
||||
selected(undefined);
|
||||
return;
|
||||
}
|
||||
const branch = branches.find(b => b.name === branchName);
|
||||
|
||||
selected(branch);
|
||||
this.setState({ selectedBranch: branch });
|
||||
this.setState({
|
||||
selectedBranch: branch,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { translate } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import Icon from "./Icon";
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { translate } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { binder, ExtensionPoint } from '@scm-manager/ui-extensions';
|
||||
import { Branch, Repository } from '@scm-manager/ui-types';
|
||||
import Icon from './Icon';
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch,
|
||||
defaultBranch: Branch,
|
||||
branches: Branch[],
|
||||
revision: string,
|
||||
path: string,
|
||||
baseUrl: string,
|
||||
repository: Repository;
|
||||
branch: Branch;
|
||||
defaultBranch: Branch;
|
||||
branches: Branch[];
|
||||
revision: string;
|
||||
path: string;
|
||||
baseUrl: string;
|
||||
|
||||
// Context props
|
||||
t: string => string
|
||||
t: (p: string) => string;
|
||||
};
|
||||
|
||||
const FlexStartNav = styled.nav`
|
||||
@@ -39,9 +38,9 @@ class Breadcrumb extends React.Component<Props> {
|
||||
const { revision, path, baseUrl } = this.props;
|
||||
|
||||
if (path) {
|
||||
const paths = path.split("/");
|
||||
const paths = path.split('/');
|
||||
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) {
|
||||
return (
|
||||
<li className="is-active" key={index}>
|
||||
@@ -53,7 +52,7 @@ class Breadcrumb extends React.Component<Props> {
|
||||
}
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link to={baseUrl + "/" + revision + "/" + currPath}>{path}</Link>
|
||||
<Link to={baseUrl + '/' + revision + '/' + currPath}>{path}</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
@@ -71,21 +70,21 @@ class Breadcrumb extends React.Component<Props> {
|
||||
revision,
|
||||
path,
|
||||
repository,
|
||||
t
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="is-flex">
|
||||
<FlexStartNav
|
||||
className={classNames("breadcrumb", "sources-breadcrumb")}
|
||||
className={classNames('breadcrumb', 'sources-breadcrumb')}
|
||||
aria-label="breadcrumbs"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to={baseUrl + "/" + revision + "/"}>
|
||||
<Link to={baseUrl + '/' + revision + '/'}>
|
||||
<HomeIcon
|
||||
title={t("breadcrumb.home")}
|
||||
title={t('breadcrumb.home')}
|
||||
name="home"
|
||||
color="inherit"
|
||||
/>
|
||||
@@ -94,7 +93,7 @@ class Breadcrumb extends React.Component<Props> {
|
||||
{this.renderPath()}
|
||||
</ul>
|
||||
</FlexStartNav>
|
||||
{binder.hasExtension("repos.sources.actionbar") && (
|
||||
{binder.hasExtension('repos.sources.actionbar') && (
|
||||
<ActionWrapper>
|
||||
<ExtensionPoint
|
||||
name="repos.sources.actionbar"
|
||||
@@ -105,9 +104,9 @@ class Breadcrumb extends React.Component<Props> {
|
||||
isBranchUrl:
|
||||
branches &&
|
||||
branches.filter(
|
||||
b => b.name.replace("/", "%2F") === revision
|
||||
b => b.name.replace('/', '%2F') === revision,
|
||||
).length > 0,
|
||||
repository
|
||||
repository,
|
||||
}}
|
||||
renderAll={true}
|
||||
/>
|
||||
@@ -120,4 +119,4 @@ class Breadcrumb extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(Breadcrumb);
|
||||
export default translate('commons')(Breadcrumb);
|
||||
@@ -1,19 +1,18 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
description: string,
|
||||
avatar: React.Node,
|
||||
contentRight?: React.Node,
|
||||
footerLeft: React.Node,
|
||||
footerRight: React.Node,
|
||||
link?: string,
|
||||
action?: () => void,
|
||||
className?: string
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: React.Node;
|
||||
contentRight?: React.Node;
|
||||
footerLeft: React.Node;
|
||||
footerRight: React.Node;
|
||||
link?: string;
|
||||
action?: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const NoEventWrapper = styled.article`
|
||||
@@ -74,16 +73,16 @@ export default class CardColumn extends React.Component<Props> {
|
||||
contentRight,
|
||||
footerLeft,
|
||||
footerRight,
|
||||
className
|
||||
className,
|
||||
} = this.props;
|
||||
const link = this.createLink();
|
||||
return (
|
||||
<>
|
||||
{link}
|
||||
<NoEventWrapper className={classNames("media", className)}>
|
||||
<NoEventWrapper className={classNames('media', className)}>
|
||||
<AvatarWrapper className="media-left">{avatar}</AvatarWrapper>
|
||||
<FlexFullHeight
|
||||
className={classNames("media-content", "text-box", "is-flex")}
|
||||
className={classNames('media-content', 'text-box', 'is-flex')}
|
||||
>
|
||||
<div className="is-flex">
|
||||
<ContentLeft className="content">
|
||||
@@ -94,7 +93,7 @@ export default class CardColumn extends React.Component<Props> {
|
||||
</ContentLeft>
|
||||
<ContentRight>{contentRight}</ContentRight>
|
||||
</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-right is-mobile is-marginless">
|
||||
{footerRight}
|
||||
@@ -1,15 +1,14 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
elements: React.Node[]
|
||||
name: string;
|
||||
elements: React.Node[];
|
||||
};
|
||||
|
||||
type State = {
|
||||
collapsed: boolean
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
@@ -24,13 +23,13 @@ export default class CardColumnGroup extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: false
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleCollapse = () => {
|
||||
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 { collapsed } = this.state;
|
||||
|
||||
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
|
||||
const icon = collapsed ? 'fa-angle-right' : 'fa-angle-down';
|
||||
let content = null;
|
||||
if (!collapsed) {
|
||||
content = elements.map((entry, index) => {
|
||||
const fullColumnWidth = this.isFullSize(elements, index);
|
||||
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
|
||||
const sizeClass = fullColumnWidth ? 'is-full' : 'is-half';
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"box",
|
||||
"box-link-shadow",
|
||||
"column",
|
||||
"is-clipped",
|
||||
sizeClass
|
||||
'box',
|
||||
'box-link-shadow',
|
||||
'column',
|
||||
'is-clipped',
|
||||
sizeClass,
|
||||
)}
|
||||
key={index}
|
||||
>
|
||||
@@ -76,14 +75,14 @@ export default class CardColumnGroup extends React.Component<Props, State> {
|
||||
<Container>
|
||||
<h2>
|
||||
<span
|
||||
className={classNames("is-size-4", "has-cursor-pointer")}
|
||||
className={classNames('is-size-4', 'has-cursor-pointer')}
|
||||
onClick={this.toggleCollapse}
|
||||
>
|
||||
<i className={classNames("fa", icon)} /> {name}
|
||||
<i className={classNames('fa', icon)} /> {name}
|
||||
</span>
|
||||
</h2>
|
||||
<hr />
|
||||
<Wrapper className={classNames("columns", "is-multiline")}>
|
||||
<Wrapper className={classNames('columns', 'is-multiline')}>
|
||||
{content}
|
||||
</Wrapper>
|
||||
<div className="is-clearfix" />
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from "react";
|
||||
import DateFromNow from "./DateFromNow";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from 'react';
|
||||
import DateFromNow from './DateFromNow';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
const baseProps = {
|
||||
timeZone: "Europe/Berlin",
|
||||
baseDate: "2019-10-12T13:56:42+02:00"
|
||||
timeZone: 'Europe/Berlin',
|
||||
baseDate: '2019-10-12T13:56:42+02:00',
|
||||
};
|
||||
|
||||
storiesOf("DateFromNow", module).add("Default", () => (
|
||||
storiesOf('DateFromNow', module).add('Default', () => (
|
||||
<div>
|
||||
<p>
|
||||
<DateFromNow date="2009-06-30T18:30:00+02:00" {...baseProps} />
|
||||
@@ -1,19 +1,18 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { formatDistance, format, parseISO } from "date-fns";
|
||||
import { enUS, de, es } from "date-fns/locale";
|
||||
import styled from "styled-components";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { formatDistance, format, parseISO } from 'date-fns';
|
||||
import { enUS, de, es } from 'date-fns/locale';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const supportedLocales = {
|
||||
enUS,
|
||||
de,
|
||||
es
|
||||
es,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
date?: string,
|
||||
timeZone?: string,
|
||||
date?: string;
|
||||
timeZone?: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* ci server.
|
||||
*/
|
||||
baseDate?: string,
|
||||
baseDate?: string;
|
||||
|
||||
// context props
|
||||
i18n: any
|
||||
i18n: any;
|
||||
};
|
||||
|
||||
const DateElement = styled.time`
|
||||
@@ -44,7 +43,7 @@ class DateFromNow extends React.Component<Props> {
|
||||
|
||||
createOptions = () => {
|
||||
const { timeZone } = this.props;
|
||||
const options: Object = {
|
||||
const options: object = {
|
||||
addSuffix: true,
|
||||
locate: this.getLocale(),
|
||||
};
|
||||
@@ -68,7 +67,7 @@ class DateFromNow extends React.Component<Props> {
|
||||
const isoDate = parseISO(date);
|
||||
const options = this.createOptions();
|
||||
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 null;
|
||||
@@ -1,19 +1,18 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import ErrorNotification from "./ErrorNotification";
|
||||
import * as React from 'react';
|
||||
import ErrorNotification from './ErrorNotification';
|
||||
|
||||
type Props = {
|
||||
fallback?: React.ComponentType<any>,
|
||||
children: React.Node
|
||||
fallback?: React.ComponentType<any>;
|
||||
children: React.Node;
|
||||
};
|
||||
|
||||
type ErrorInfo = {
|
||||
componentStack: string
|
||||
componentStack: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
error?: Error,
|
||||
errorInfo?: ErrorInfo
|
||||
error?: Error;
|
||||
errorInfo?: ErrorInfo;
|
||||
};
|
||||
|
||||
class ErrorBoundary extends React.Component<Props, State> {
|
||||
@@ -26,7 +25,7 @@ class ErrorBoundary extends React.Component<Props,State> {
|
||||
// Catch errors in any components below and re-render with error message
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
errorInfo,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import {BackendError, ForbiddenError, UnauthorizedError} from "./errors";
|
||||
import Notification from "./Notification";
|
||||
import BackendErrorNotification from "./BackendErrorNotification";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { BackendError, ForbiddenError, UnauthorizedError } from './errors';
|
||||
import Notification from './Notification';
|
||||
import BackendErrorNotification from './BackendErrorNotification';
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
error?: Error
|
||||
t: (p: string) => string;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
class ErrorNotification extends React.Component<Props> {
|
||||
@@ -19,24 +18,24 @@ class ErrorNotification extends React.Component<Props> {
|
||||
} else if (error instanceof UnauthorizedError) {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("errorNotification.prefix")}:</strong>{" "}
|
||||
{t("errorNotification.timeout")}{" "}
|
||||
<strong>{t('errorNotification.prefix')}:</strong>{' '}
|
||||
{t('errorNotification.timeout')}{' '}
|
||||
<a href="javascript:window.location.reload(true)">
|
||||
{t("errorNotification.loginLink")}
|
||||
{t('errorNotification.loginLink')}
|
||||
</a>
|
||||
</Notification>
|
||||
);
|
||||
} else if (error instanceof ForbiddenError) {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("errorNotification.prefix")}:</strong>{" "}
|
||||
{t("errorNotification.forbidden")}
|
||||
<strong>{t('errorNotification.prefix')}:</strong>{' '}
|
||||
{t('errorNotification.forbidden')}
|
||||
</Notification>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Notification type="danger">
|
||||
<strong>{t("errorNotification.prefix")}:</strong> {error.message}
|
||||
<strong>{t('errorNotification.prefix')}:</strong> {error.message}
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
@@ -45,4 +44,4 @@ class ErrorNotification extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(ErrorNotification);
|
||||
export default translate('commons')(ErrorNotification);
|
||||
@@ -1,12 +1,11 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import ErrorNotification from "./ErrorNotification";
|
||||
import { BackendError, ForbiddenError } from "./errors";
|
||||
import React from 'react';
|
||||
import ErrorNotification from './ErrorNotification';
|
||||
import { BackendError, ForbiddenError } from './errors';
|
||||
|
||||
type Props = {
|
||||
error: Error,
|
||||
title: string,
|
||||
subtitle: string
|
||||
error: Error;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
|
||||
class ErrorPage extends React.Component<Props> {
|
||||
@@ -29,8 +28,8 @@ class ErrorPage extends React.Component<Props> {
|
||||
if (error instanceof BackendError || error instanceof ForbiddenError) {
|
||||
return null;
|
||||
}
|
||||
return <p className="subtitle">{subtitle}</p>
|
||||
}
|
||||
return <p className="subtitle">{subtitle}</p>;
|
||||
};
|
||||
}
|
||||
|
||||
export default ErrorPage;
|
||||
@@ -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");
|
||||
});
|
||||
10
scm-ui/ui-components/src/FileSize.test.ts
Normal file
10
scm-ui/ui-components/src/FileSize.test.ts
Normal 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');
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
bytes: number
|
||||
bytes: number;
|
||||
};
|
||||
|
||||
class FileSize extends React.Component<Props> {
|
||||
static format(bytes: number) {
|
||||
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 size = i === 0 ? bytes : (bytes / 1000 ** i).toFixed(2);
|
||||
@@ -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);
|
||||
26
scm-ui/ui-components/src/GroupAutocomplete.tsx
Normal file
26
scm-ui/ui-components/src/GroupAutocomplete.tsx
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
scm-ui/ui-components/src/Help.tsx
Normal file
29
scm-ui/ui-components/src/Help.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import Icon from "./Icon";
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default class HelpIcon extends React.Component<Props> {
|
||||
@@ -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)} />;
|
||||
}
|
||||
}
|
||||
43
scm-ui/ui-components/src/Icon.tsx
Normal file
43
scm-ui/ui-components/src/Icon.tsx
Normal 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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {withContextPath} from "./urls";
|
||||
import React from 'react';
|
||||
import { withContextPath } from './urls';
|
||||
|
||||
type Props = {
|
||||
src: string,
|
||||
alt: string,
|
||||
className?: any
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: any;
|
||||
};
|
||||
|
||||
class Image extends React.Component<Props> {
|
||||
|
||||
createImageSrc = () => {
|
||||
const { src } = this.props;
|
||||
if (src.startsWith("http")) {
|
||||
if (src.startsWith('http')) {
|
||||
return src;
|
||||
}
|
||||
return withContextPath(src);
|
||||
@@ -1,16 +1,15 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { PagedCollection } from "@scm-manager/ui-types";
|
||||
import { Button } from "./buttons";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { PagedCollection } from '@scm-manager/ui-types';
|
||||
import { Button } from './buttons';
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
page: number,
|
||||
filter?: string,
|
||||
collection: PagedCollection;
|
||||
page: number;
|
||||
filter?: string;
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
t: (p: string) => string;
|
||||
};
|
||||
|
||||
class LinkPaginator extends React.Component<Props> {
|
||||
@@ -26,9 +25,9 @@ class LinkPaginator extends React.Component<Props> {
|
||||
return (
|
||||
<Button
|
||||
className="pagination-link"
|
||||
label={"1"}
|
||||
label={'1'}
|
||||
disabled={false}
|
||||
link={this.addFilterToLink("1")}
|
||||
link={this.addFilterToLink('1')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +40,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
<Button
|
||||
className={className}
|
||||
label={label ? label : previousPage.toString()}
|
||||
disabled={!this.hasLink("prev")}
|
||||
disabled={!this.hasLink('prev')}
|
||||
link={this.addFilterToLink(`${previousPage}`)}
|
||||
/>
|
||||
);
|
||||
@@ -59,7 +58,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
<Button
|
||||
className={className}
|
||||
label={label ? label : nextPage.toString()}
|
||||
disabled={!this.hasLink("next")}
|
||||
disabled={!this.hasLink('next')}
|
||||
link={this.addFilterToLink(`${nextPage}`)}
|
||||
/>
|
||||
);
|
||||
@@ -104,45 +103,40 @@ class LinkPaginator extends React.Component<Props> {
|
||||
links.push(this.separator());
|
||||
}
|
||||
if (page > 2) {
|
||||
links.push(this.renderPreviousButton("pagination-link"));
|
||||
links.push(this.renderPreviousButton('pagination-link'));
|
||||
}
|
||||
|
||||
links.push(this.currentPage(page));
|
||||
|
||||
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
|
||||
links.push(this.separator());
|
||||
if (page < pageTotal) {
|
||||
links.push(this.renderLastButton());
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { collection, t } = this.props;
|
||||
|
||||
if (collection) {
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton(
|
||||
"pagination-previous",
|
||||
t("paginator.previous")
|
||||
'pagination-previous',
|
||||
t('paginator.previous'),
|
||||
)}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton("pagination-next", t("paginator.next"))}
|
||||
{this.renderNextButton('pagination-next', t('paginator.next'))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(LinkPaginator);
|
||||
export default translate('commons')(LinkPaginator);
|
||||
@@ -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>
|
||||
));
|
||||
9
scm-ui/ui-components/src/Loading.stories.tsx
Normal file
9
scm-ui/ui-components/src/Loading.stories.tsx
Normal 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>
|
||||
));
|
||||
@@ -1,12 +1,11 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Image from "./Image";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import Image from './Image';
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
message?: string
|
||||
t: (p: string) => string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -27,11 +26,11 @@ class Loading extends React.Component<Props> {
|
||||
const { message, t } = this.props;
|
||||
return (
|
||||
<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>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(Loading);
|
||||
export default translate('commons')(Loading);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
));
|
||||
16
scm-ui/ui-components/src/Logo.stories.tsx
Normal file
16
scm-ui/ui-components/src/Logo.stories.tsx
Normal 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>
|
||||
));
|
||||
16
scm-ui/ui-components/src/Logo.tsx
Normal file
16
scm-ui/ui-components/src/Logo.tsx
Normal 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);
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
address?: string
|
||||
address?: string;
|
||||
};
|
||||
|
||||
class MailLink extends React.Component<Props> {
|
||||
@@ -11,7 +10,7 @@ class MailLink extends React.Component<Props> {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
return <a href={"mailto:" + address}>{address}</a>;
|
||||
return <a href={'mailto:' + address}>{address}</a>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
15
scm-ui/ui-components/src/MarkdownHeadingRenderer.test.ts
Normal file
15
scm-ui/ui-components/src/MarkdownHeadingRenderer.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { withContextPath } from "./urls";
|
||||
import * as React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withContextPath } from './urls';
|
||||
|
||||
/**
|
||||
* Adds anchor links to markdown headings.
|
||||
@@ -10,13 +9,13 @@ import { withContextPath } from "./urls";
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
level: number,
|
||||
location: any
|
||||
children: React.Node;
|
||||
level: number;
|
||||
location: any;
|
||||
};
|
||||
|
||||
function flatten(text: string, child: any) {
|
||||
return typeof child === "string"
|
||||
return typeof child === 'string'
|
||||
? text + child
|
||||
: React.Children.toArray(child.props.children).reduce(flatten, text);
|
||||
}
|
||||
@@ -27,15 +26,19 @@ function flatten(text: string, child: any) {
|
||||
* @VisibleForTesting
|
||||
*/
|
||||
export function headingToAnchorId(heading: string) {
|
||||
return heading.toLowerCase().replace(/\W/g, "-");
|
||||
return heading.toLowerCase().replace(/\W/g, '-');
|
||||
}
|
||||
|
||||
function MarkdownHeadingRenderer(props: Props) {
|
||||
const children = React.Children.toArray(props.children);
|
||||
const heading = children.reduce(flatten, "");
|
||||
const heading = children.reduce(flatten, '');
|
||||
const anchorId = headingToAnchorId(heading);
|
||||
const headingElement = React.createElement("h" + props.level, {}, props.children);
|
||||
const href = withContextPath(props.location.pathname + "#" + anchorId);
|
||||
const headingElement = React.createElement(
|
||||
'h' + props.level,
|
||||
{},
|
||||
props.children,
|
||||
);
|
||||
const href = withContextPath(props.location.pathname + '#' + anchorId);
|
||||
|
||||
return (
|
||||
<a id={`${anchorId}`} className="anchor" href={href}>
|
||||
@@ -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>
|
||||
));
|
||||
21
scm-ui/ui-components/src/MarkdownView.stories.tsx
Normal file
21
scm-ui/ui-components/src/MarkdownView.stories.tsx
Normal 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>
|
||||
));
|
||||
@@ -1,26 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import Markdown from "react-markdown/with-html";
|
||||
import styled from "styled-components";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Markdown from 'react-markdown/with-html';
|
||||
import styled from 'styled-components';
|
||||
import { binder } from '@scm-manager/ui-extensions';
|
||||
import SyntaxHighlighter from './SyntaxHighlighter';
|
||||
import MarkdownHeadingRenderer from './MarkdownHeadingRenderer';
|
||||
|
||||
type Props = {
|
||||
content: string,
|
||||
renderContext?: Object,
|
||||
renderers?: Object,
|
||||
skipHtml?: boolean,
|
||||
enableAnchorHeadings: boolean,
|
||||
content: string;
|
||||
renderContext?: object;
|
||||
renderers?: object;
|
||||
skipHtml?: boolean;
|
||||
enableAnchorHeadings: boolean;
|
||||
|
||||
// context props
|
||||
location: any
|
||||
location: any;
|
||||
};
|
||||
|
||||
const MarkdownWrapper = styled.div`
|
||||
> .content {
|
||||
> h1, h2, h3, h4, h5, h6 {
|
||||
> h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -30,7 +34,10 @@ const MarkdownWrapper = styled.div`
|
||||
> h2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
> h3, h4, h5, h6 {
|
||||
> h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
& strong {
|
||||
@@ -42,10 +49,10 @@ const MarkdownWrapper = styled.div`
|
||||
class MarkdownView extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
enableAnchorHeadings: false,
|
||||
skipHtml: false
|
||||
skipHtml: false,
|
||||
};
|
||||
|
||||
contentRef: ?HTMLDivElement;
|
||||
contentRef: HTMLDivElement | null | undefined;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -72,10 +79,10 @@ class MarkdownView extends React.Component<Props> {
|
||||
renderers,
|
||||
renderContext,
|
||||
enableAnchorHeadings,
|
||||
skipHtml
|
||||
skipHtml,
|
||||
} = this.props;
|
||||
|
||||
const rendererFactory = binder.getExtension("markdown-renderer-factory");
|
||||
const rendererFactory = binder.getExtension('markdown-renderer-factory');
|
||||
let rendererList = renderers;
|
||||
|
||||
if (rendererFactory) {
|
||||
@@ -1,25 +1,24 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type NotificationType =
|
||||
| "primary"
|
||||
| "info"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "inherit";
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'inherit';
|
||||
|
||||
type Props = {
|
||||
type: NotificationType,
|
||||
onClose?: () => void,
|
||||
className?: string,
|
||||
children?: React.Node
|
||||
type: NotificationType;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
children?: React.Node;
|
||||
};
|
||||
|
||||
class Notification extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
type: "info"
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
renderCloseButton() {
|
||||
@@ -27,16 +26,16 @@ class Notification extends React.Component<Props> {
|
||||
if (onClose) {
|
||||
return <button className="delete" onClick={onClose} />;
|
||||
}
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, className, children } = this.props;
|
||||
|
||||
const color = type !== "inherit" ? "is-" + type : "";
|
||||
const color = type !== 'inherit' ? 'is-' + type : '';
|
||||
|
||||
return (
|
||||
<div className={classNames("notification", color, className)}>
|
||||
<div className={classNames('notification', color, className)}>
|
||||
{this.renderCloseButton()}
|
||||
{children}
|
||||
</div>
|
||||
@@ -1,19 +1,18 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { History } from "history";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { Button, urls } from "./index";
|
||||
import { FilterInput } from "./forms";
|
||||
import React from 'react';
|
||||
import { History } from 'history';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { Button, urls } from './index';
|
||||
import { FilterInput } from './forms';
|
||||
|
||||
type Props = {
|
||||
showCreateButton: boolean,
|
||||
link: string,
|
||||
label?: string,
|
||||
showCreateButton: boolean;
|
||||
link: string;
|
||||
label?: string;
|
||||
|
||||
// context props
|
||||
history: History,
|
||||
location: any
|
||||
history: History;
|
||||
location: any;
|
||||
};
|
||||
|
||||
class OverviewPageActions extends React.Component<Props> {
|
||||
@@ -36,7 +35,7 @@ class OverviewPageActions extends React.Component<Props> {
|
||||
const { showCreateButton, link, label } = this.props;
|
||||
if (showCreateButton) {
|
||||
return (
|
||||
<div className={classNames("input-button", "control")}>
|
||||
<div className={classNames('input-button', 'control')}>
|
||||
<Button label={label} link={`/${link}/create`} color="primary" />
|
||||
</div>
|
||||
);
|
||||
@@ -1,49 +1,43 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
|
||||
import "@scm-manager/ui-tests/i18n";
|
||||
import Paginator from "./Paginator";
|
||||
|
||||
xdescribe("paginator rendering tests", () => {
|
||||
import React from 'react';
|
||||
import { mount, shallow } from '@scm-manager/ui-tests/enzyme-router';
|
||||
import '@scm-manager/ui-tests/i18n';
|
||||
import Paginator from './Paginator';
|
||||
|
||||
xdescribe('paginator rendering tests', () => {
|
||||
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 = {
|
||||
page: 10,
|
||||
pageTotal: 20,
|
||||
_links: {},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(7);
|
||||
for (let button of buttons) {
|
||||
expect(button.props.disabled).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("should render buttons for first page", () => {
|
||||
it('should render buttons for first page', () => {
|
||||
const collection = {
|
||||
page: 0,
|
||||
pageTotal: 148,
|
||||
_links: {
|
||||
first: dummyLink,
|
||||
next: dummyLink,
|
||||
last: dummyLink
|
||||
last: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
// previous button
|
||||
@@ -58,15 +52,15 @@ xdescribe("paginator rendering tests", () => {
|
||||
// next button
|
||||
const nextButton = buttons.get(3).props;
|
||||
expect(nextButton.disabled).toBeFalsy();
|
||||
expect(nextButton.label).toBe("2");
|
||||
expect(nextButton.label).toBe('2');
|
||||
|
||||
// last button
|
||||
const lastButton = buttons.get(4).props;
|
||||
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 = {
|
||||
page: 1,
|
||||
pageTotal: 148,
|
||||
@@ -74,15 +68,13 @@ xdescribe("paginator rendering tests", () => {
|
||||
first: dummyLink,
|
||||
prev: dummyLink,
|
||||
next: dummyLink,
|
||||
last: dummyLink
|
||||
last: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
// previous button
|
||||
@@ -92,7 +84,7 @@ xdescribe("paginator rendering tests", () => {
|
||||
// first button
|
||||
const firstButton = buttons.get(2).props;
|
||||
expect(firstButton.disabled).toBeFalsy();
|
||||
expect(firstButton.label).toBe("1");
|
||||
expect(firstButton.label).toBe('1');
|
||||
|
||||
// current button
|
||||
const currentButton = buttons.get(3).props;
|
||||
@@ -102,29 +94,27 @@ xdescribe("paginator rendering tests", () => {
|
||||
// next button
|
||||
const nextButton = buttons.get(4).props;
|
||||
expect(nextButton.disabled).toBeFalsy();
|
||||
expect(nextButton.label).toBe("3");
|
||||
expect(nextButton.label).toBe('3');
|
||||
|
||||
// last button
|
||||
const lastButton = buttons.get(5).props;
|
||||
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 = {
|
||||
page: 147,
|
||||
pageTotal: 148,
|
||||
_links: {
|
||||
first: dummyLink,
|
||||
prev: dummyLink
|
||||
prev: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
// previous button
|
||||
@@ -134,12 +124,12 @@ xdescribe("paginator rendering tests", () => {
|
||||
// first button
|
||||
const firstButton = buttons.get(2).props;
|
||||
expect(firstButton.disabled).toBeFalsy();
|
||||
expect(firstButton.label).toBe("1");
|
||||
expect(firstButton.label).toBe('1');
|
||||
|
||||
// next button
|
||||
const nextButton = buttons.get(3).props;
|
||||
expect(nextButton.disabled).toBeFalsy();
|
||||
expect(nextButton.label).toBe("147");
|
||||
expect(nextButton.label).toBe('147');
|
||||
|
||||
// last button
|
||||
const lastButton = buttons.get(4).props;
|
||||
@@ -147,7 +137,7 @@ xdescribe("paginator rendering tests", () => {
|
||||
expect(lastButton.label).toBe(148);
|
||||
});
|
||||
|
||||
it("should render buttons for penultimate page", () => {
|
||||
it('should render buttons for penultimate page', () => {
|
||||
const collection = {
|
||||
page: 146,
|
||||
pageTotal: 148,
|
||||
@@ -155,15 +145,13 @@ xdescribe("paginator rendering tests", () => {
|
||||
first: dummyLink,
|
||||
prev: dummyLink,
|
||||
next: dummyLink,
|
||||
last: dummyLink
|
||||
last: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
// previous button
|
||||
@@ -174,11 +162,11 @@ xdescribe("paginator rendering tests", () => {
|
||||
// first button
|
||||
const firstButton = buttons.get(2).props;
|
||||
expect(firstButton.disabled).toBeFalsy();
|
||||
expect(firstButton.label).toBe("1");
|
||||
expect(firstButton.label).toBe('1');
|
||||
|
||||
const currentButton = buttons.get(3).props;
|
||||
expect(currentButton.disabled).toBeFalsy();
|
||||
expect(currentButton.label).toBe("146");
|
||||
expect(currentButton.label).toBe('146');
|
||||
|
||||
// current button
|
||||
const nextButton = buttons.get(4).props;
|
||||
@@ -188,10 +176,10 @@ xdescribe("paginator rendering tests", () => {
|
||||
// last button
|
||||
const lastButton = buttons.get(5).props;
|
||||
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 = {
|
||||
page: 41,
|
||||
pageTotal: 148,
|
||||
@@ -199,15 +187,13 @@ xdescribe("paginator rendering tests", () => {
|
||||
first: dummyLink,
|
||||
prev: dummyLink,
|
||||
next: dummyLink,
|
||||
last: dummyLink
|
||||
last: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const buttons = paginator.find('Button');
|
||||
expect(buttons.length).toBe(7);
|
||||
|
||||
// previous button
|
||||
@@ -218,12 +204,12 @@ xdescribe("paginator rendering tests", () => {
|
||||
// first button
|
||||
const firstButton = buttons.get(2).props;
|
||||
expect(firstButton.disabled).toBeFalsy();
|
||||
expect(firstButton.label).toBe("1");
|
||||
expect(firstButton.label).toBe('1');
|
||||
|
||||
// previous Button
|
||||
const previousButton = buttons.get(3).props;
|
||||
expect(previousButton.disabled).toBeFalsy();
|
||||
expect(previousButton.label).toBe("41");
|
||||
expect(previousButton.label).toBe('41');
|
||||
|
||||
// current button
|
||||
const currentButton = buttons.get(4).props;
|
||||
@@ -233,27 +219,27 @@ xdescribe("paginator rendering tests", () => {
|
||||
// next button
|
||||
const nextButton = buttons.get(5).props;
|
||||
expect(nextButton.disabled).toBeFalsy();
|
||||
expect(nextButton.label).toBe("43");
|
||||
expect(nextButton.label).toBe('43');
|
||||
|
||||
// last button
|
||||
const lastButton = buttons.get(6).props;
|
||||
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 = {
|
||||
page: 41,
|
||||
pageTotal: 148,
|
||||
_links: {
|
||||
first: dummyLink,
|
||||
prev: {
|
||||
href: "https://www.scm-manager.org"
|
||||
href: 'https://www.scm-manager.org',
|
||||
},
|
||||
next: dummyLink,
|
||||
last: dummyLink
|
||||
last: dummyLink,
|
||||
},
|
||||
_embedded: {}
|
||||
_embedded: {},
|
||||
};
|
||||
|
||||
let urlToOpen;
|
||||
@@ -262,10 +248,10 @@ xdescribe("paginator rendering tests", () => {
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { PagedCollection } from "@scm-manager/ui-types";
|
||||
import { Button } from "./buttons";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { PagedCollection } from '@scm-manager/ui-types';
|
||||
import { Button } from './buttons';
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
onPageChange?: string => void,
|
||||
t: string => string
|
||||
collection: PagedCollection;
|
||||
onPageChange?: (p: string) => void;
|
||||
t: (p: string) => string;
|
||||
};
|
||||
|
||||
class Paginator extends React.Component<Props> {
|
||||
@@ -26,30 +25,30 @@ class Paginator extends React.Component<Props> {
|
||||
};
|
||||
|
||||
renderFirstButton() {
|
||||
return this.renderPageButton(1, "first");
|
||||
return this.renderPageButton(1, 'first');
|
||||
}
|
||||
|
||||
renderPreviousButton() {
|
||||
const { t } = this.props;
|
||||
return this.renderButton(
|
||||
"pagination-previous",
|
||||
t("paginator.previous"),
|
||||
"prev"
|
||||
'pagination-previous',
|
||||
t('paginator.previous'),
|
||||
'prev',
|
||||
);
|
||||
}
|
||||
|
||||
renderNextButton() {
|
||||
const { t } = this.props;
|
||||
return this.renderButton("pagination-next", t("paginator.next"), "next");
|
||||
return this.renderButton('pagination-next', t('paginator.next'), 'next');
|
||||
}
|
||||
|
||||
renderLastButton() {
|
||||
const { collection } = this.props;
|
||||
return this.renderPageButton(collection.pageTotal, "last");
|
||||
return this.renderPageButton(collection.pageTotal, 'last');
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -90,24 +89,21 @@ class Paginator extends React.Component<Props> {
|
||||
links.push(this.seperator());
|
||||
}
|
||||
if (page > 2) {
|
||||
links.push(this.renderPageButton(page - 1, "prev"));
|
||||
links.push(this.renderPageButton(page - 1, 'prev'));
|
||||
}
|
||||
|
||||
links.push(this.currentPage(page));
|
||||
|
||||
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
|
||||
links.push(this.seperator());
|
||||
if (page < pageTotal) {
|
||||
links.push(this.renderLastButton());
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<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);
|
||||
@@ -1,10 +1,9 @@
|
||||
//@flow
|
||||
import React, { Component } from "react";
|
||||
import { Route, Redirect, withRouter } from "react-router-dom";
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Redirect, withRouter } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean,
|
||||
component: Component<any, any>
|
||||
authenticated?: boolean;
|
||||
component: Component<any, any>;
|
||||
};
|
||||
|
||||
class ProtectedRoute extends React.Component<Props> {
|
||||
@@ -16,8 +15,10 @@ class ProtectedRoute extends React.Component<Props> {
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: { from: routeProps.location }
|
||||
pathname: '/login',
|
||||
state: {
|
||||
from: routeProps.location,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,16 +1,15 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { PagedCollection } from "@scm-manager/ui-types";
|
||||
import { Button } from "./index";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { PagedCollection } from '@scm-manager/ui-types';
|
||||
import { Button } from './index';
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
page: number,
|
||||
updatePage: number => void,
|
||||
collection: PagedCollection;
|
||||
page: number;
|
||||
updatePage: (p: number) => void;
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
t: (p: string) => string;
|
||||
};
|
||||
|
||||
class StatePaginator extends React.Component<Props> {
|
||||
@@ -18,7 +17,7 @@ class StatePaginator extends React.Component<Props> {
|
||||
return (
|
||||
<Button
|
||||
className="pagination-link"
|
||||
label={"1"}
|
||||
label={'1'}
|
||||
disabled={false}
|
||||
action={() => this.updateCurrentPage(1)}
|
||||
/>
|
||||
@@ -37,7 +36,7 @@ class StatePaginator extends React.Component<Props> {
|
||||
<Button
|
||||
className="pagination-previous"
|
||||
label={label ? label : previousPage.toString()}
|
||||
disabled={!this.hasLink("prev")}
|
||||
disabled={!this.hasLink('prev')}
|
||||
action={() => this.updateCurrentPage(previousPage)}
|
||||
/>
|
||||
);
|
||||
@@ -55,7 +54,7 @@ class StatePaginator extends React.Component<Props> {
|
||||
<Button
|
||||
className="pagination-next"
|
||||
label={label ? label : nextPage.toString()}
|
||||
disabled={!this.hasLink("next")}
|
||||
disabled={!this.hasLink('next')}
|
||||
action={() => this.updateCurrentPage(nextPage)}
|
||||
/>
|
||||
);
|
||||
@@ -109,30 +108,26 @@ class StatePaginator extends React.Component<Props> {
|
||||
if (page + 1 < pageTotal) {
|
||||
links.push(this.renderNextButton());
|
||||
}
|
||||
if (page + 2 < pageTotal)
|
||||
if (page + 2 < pageTotal) links.push(this.separator());
|
||||
//if there exists pages between next and last
|
||||
links.push(this.separator());
|
||||
if (page < pageTotal) {
|
||||
links.push(this.renderLastButton());
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton(t("paginator.previous"))}
|
||||
{this.renderPreviousButton(t('paginator.previous'))}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton(t("paginator.next"))}
|
||||
{this.renderNextButton(t('paginator.next'))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(StatePaginator);
|
||||
export default translate('commons')(StatePaginator);
|
||||
@@ -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>
|
||||
));
|
||||
35
scm-ui/ui-components/src/SyntaxHighlighter.stories.tsx
Normal file
35
scm-ui/ui-components/src/SyntaxHighlighter.stories.tsx
Normal 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>
|
||||
));
|
||||
@@ -1,16 +1,14 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import {LightAsync as ReactSyntaxHighlighter} from "react-syntax-highlighter";
|
||||
import {arduinoLight} from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||
import { LightAsync as ReactSyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { arduinoLight } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
|
||||
|
||||
type Props = {
|
||||
language: string,
|
||||
value: string
|
||||
language: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
class SyntaxHighlighter extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactSyntaxHighlighter
|
||||
@@ -1,20 +1,19 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
color: string,
|
||||
icon?: string,
|
||||
label: string,
|
||||
title?: string,
|
||||
onClick?: () => void,
|
||||
onRemove?: () => void
|
||||
className?: string;
|
||||
color: string;
|
||||
icon?: string;
|
||||
label: string;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
class Tag extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
color: "light"
|
||||
color: 'light',
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -25,13 +24,13 @@ class Tag extends React.Component<Props> {
|
||||
label,
|
||||
title,
|
||||
onClick,
|
||||
onRemove
|
||||
onRemove,
|
||||
} = this.props;
|
||||
let showIcon = null;
|
||||
if (icon) {
|
||||
showIcon = (
|
||||
<>
|
||||
<i className={classNames("fas", `fa-${icon}`)} />
|
||||
<i className={classNames('fas', `fa-${icon}`)} />
|
||||
|
||||
</>
|
||||
);
|
||||
@@ -44,7 +43,7 @@ class Tag extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={classNames("tag", `is-${color}`, className)}
|
||||
className={classNames('tag', `is-${color}`, className)}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -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;
|
||||
35
scm-ui/ui-components/src/Tooltip.tsx
Normal file
35
scm-ui/ui-components/src/Tooltip.tsx
Normal 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;
|
||||
@@ -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);
|
||||
26
scm-ui/ui-components/src/UserAutocomplete.tsx
Normal file
26
scm-ui/ui-components/src/UserAutocomplete.tsx
Normal 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);
|
||||
@@ -1,25 +1,24 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type {SelectValue} from "@scm-manager/ui-types";
|
||||
import Autocomplete from "./Autocomplete";
|
||||
import React from 'react';
|
||||
import { SelectValue } from '@scm-manager/ui-types';
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
export type AutocompleteProps = {
|
||||
autocompleteLink: string,
|
||||
valueSelected: SelectValue => void,
|
||||
value?: SelectValue
|
||||
autocompleteLink: string;
|
||||
valueSelected: (p: SelectValue) => void;
|
||||
value?: SelectValue;
|
||||
};
|
||||
|
||||
type Props = AutocompleteProps & {
|
||||
label: string,
|
||||
noOptionsMessage: string,
|
||||
loadingMessage: string,
|
||||
placeholder: string
|
||||
label: string;
|
||||
noOptionsMessage: string;
|
||||
loadingMessage: string;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export default class UserGroupAutocomplete extends React.Component<Props> {
|
||||
loadSuggestions = (inputValue: string) => {
|
||||
const url = this.props.autocompleteLink;
|
||||
const link = url + "?q=";
|
||||
const link = url + '?q=';
|
||||
return fetch(link + inputValue)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
@@ -29,7 +28,7 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
|
||||
: element.id;
|
||||
return {
|
||||
value: element,
|
||||
label
|
||||
label,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
`;
|
||||
@@ -343,7 +343,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
||||
className="sc-EHOje lbpDzp"
|
||||
>
|
||||
<div
|
||||
className="sc-ifAKCX bndlBs"
|
||||
className="sc-ifAKCX frmxmY"
|
||||
>
|
||||
<div
|
||||
className="content"
|
||||
@@ -2207,190 +2207,6 @@ exports[`Storyshots SyntaxHighlighter Java 1`] = `
|
||||
</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`] = `
|
||||
<div
|
||||
className="sc-bZQynM hAJIOS"
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
74
scm-ui/ui-components/src/apiclient.test.ts
Normal file
74
scm-ui/ui-components/src/apiclient.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,18 @@
|
||||
// @flow
|
||||
import { contextPath } from "./urls";
|
||||
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors";
|
||||
import type { BackendErrorContent } from "./errors";
|
||||
import { contextPath } from './urls';
|
||||
import {
|
||||
createBackendError,
|
||||
ForbiddenError,
|
||||
isBackendError,
|
||||
UnauthorizedError,
|
||||
} from './errors';
|
||||
import { BackendErrorContent } from './errors';
|
||||
|
||||
const applyFetchOptions: (RequestOptions) => RequestOptions = o => {
|
||||
o.credentials = "same-origin";
|
||||
const applyFetchOptions: (p: RequestOptions) => RequestOptions = o => {
|
||||
o.credentials = 'same-origin';
|
||||
o.headers = {
|
||||
Cache: "no-cache",
|
||||
Cache: 'no-cache',
|
||||
// identify the request as ajax request
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
return o;
|
||||
};
|
||||
@@ -16,30 +20,29 @@ const applyFetchOptions: (RequestOptions) => RequestOptions = o => {
|
||||
function handleFailure(response: Response) {
|
||||
if (!response.ok) {
|
||||
if (isBackendError(response)) {
|
||||
return response.json()
|
||||
.then((content: BackendErrorContent) => {
|
||||
return response.json().then((content: BackendErrorContent) => {
|
||||
throw createBackendError(content, response.status);
|
||||
});
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
throw new UnauthorizedError("Unauthorized", 401);
|
||||
throw new UnauthorizedError('Unauthorized', 401);
|
||||
} 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;
|
||||
}
|
||||
|
||||
export function createUrl(url: string) {
|
||||
if (url.includes("://")) {
|
||||
if (url.includes('://')) {
|
||||
return url;
|
||||
}
|
||||
let urlWithStartingSlash = url;
|
||||
if (url.indexOf("/") !== 0) {
|
||||
urlWithStartingSlash = "/" + urlWithStartingSlash;
|
||||
if (url.indexOf('/') !== 0) {
|
||||
urlWithStartingSlash = '/' + urlWithStartingSlash;
|
||||
}
|
||||
return `${contextPath}/api/v2${urlWithStartingSlash}`;
|
||||
}
|
||||
@@ -49,28 +52,28 @@ class ApiClient {
|
||||
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
||||
}
|
||||
|
||||
post(url: string, payload: any, contentType: string = "application/json") {
|
||||
return this.httpRequestWithJSONBody("POST", url, contentType, payload);
|
||||
post(url: string, payload: any, contentType: string = 'application/json') {
|
||||
return this.httpRequestWithJSONBody('POST', url, contentType, payload);
|
||||
}
|
||||
|
||||
postBinary(url: string, fileAppender: FormData => void) {
|
||||
postBinary(url: string, fileAppender: (p: FormData) => void) {
|
||||
let formData = new FormData();
|
||||
fileAppender(formData);
|
||||
|
||||
let options: RequestOptions = {
|
||||
method: "POST",
|
||||
body: formData
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
};
|
||||
return this.httpRequestWithBinaryBody(options, url);
|
||||
}
|
||||
|
||||
put(url: string, payload: any, contentType: string = "application/json") {
|
||||
return this.httpRequestWithJSONBody("PUT", url, contentType, payload);
|
||||
put(url: string, payload: any, contentType: string = 'application/json') {
|
||||
return this.httpRequestWithJSONBody('PUT', url, contentType, payload);
|
||||
}
|
||||
|
||||
head(url: string) {
|
||||
let options: RequestOptions = {
|
||||
method: "HEAD"
|
||||
method: 'HEAD',
|
||||
};
|
||||
options = applyFetchOptions(options);
|
||||
return fetch(createUrl(url), options).then(handleFailure);
|
||||
@@ -78,7 +81,7 @@ class ApiClient {
|
||||
|
||||
delete(url: string): Promise<Response> {
|
||||
let options: RequestOptions = {
|
||||
method: "DELETE"
|
||||
method: 'DELETE',
|
||||
};
|
||||
options = applyFetchOptions(options);
|
||||
return fetch(createUrl(url), options).then(handleFailure);
|
||||
@@ -88,20 +91,24 @@ class ApiClient {
|
||||
method: string,
|
||||
url: string,
|
||||
contentType: string,
|
||||
payload: any
|
||||
payload: any,
|
||||
): Promise<Response> {
|
||||
let options: RequestOptions = {
|
||||
method: method,
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
};
|
||||
return this.httpRequestWithBinaryBody(options, url, contentType);
|
||||
}
|
||||
|
||||
httpRequestWithBinaryBody(options: RequestOptions, url: string, contentType?: string) {
|
||||
httpRequestWithBinaryBody(
|
||||
options: RequestOptions,
|
||||
url: string,
|
||||
contentType?: string,
|
||||
) {
|
||||
options = applyFetchOptions(options);
|
||||
if (contentType) {
|
||||
// $FlowFixMe
|
||||
options.headers["Content-Type"] = contentType;
|
||||
options.headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
return fetch(createUrl(url), options).then(handleFailure);
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export type Person = {
|
||||
name: string,
|
||||
mail?: string
|
||||
};
|
||||
|
||||
export const EXTENSION_POINT = "avatar.factory";
|
||||
6
scm-ui/ui-components/src/avatar/Avatar.ts
Normal file
6
scm-ui/ui-components/src/avatar/Avatar.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Person = {
|
||||
name: string;
|
||||
mail?: string;
|
||||
};
|
||||
|
||||
export const EXTENSION_POINT = 'avatar.factory';
|
||||
@@ -1,13 +1,11 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {binder} from "@scm-manager/ui-extensions";
|
||||
import {Image} from "..";
|
||||
import type { Person } from "./Avatar";
|
||||
import { EXTENSION_POINT } from "./Avatar";
|
||||
|
||||
import React from 'react';
|
||||
import { binder } from '@scm-manager/ui-extensions';
|
||||
import { Image } from '..';
|
||||
import { Person } from './Avatar';
|
||||
import { EXTENSION_POINT } from './Avatar';
|
||||
|
||||
type Props = {
|
||||
person: Person
|
||||
person: Person;
|
||||
};
|
||||
|
||||
class AvatarImage extends React.Component<Props> {
|
||||
@@ -19,11 +17,7 @@ class AvatarImage extends React.Component<Props> {
|
||||
const avatar = avatarFactory(person);
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="has-rounded-border"
|
||||
src={avatar}
|
||||
alt={person.name}
|
||||
/>
|
||||
<Image className="has-rounded-border" src={avatar} alt={person.name} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import {binder} from "@scm-manager/ui-extensions";
|
||||
import { EXTENSION_POINT } from "./Avatar";
|
||||
import * as React from 'react';
|
||||
import { binder } from '@scm-manager/ui-extensions';
|
||||
import { EXTENSION_POINT } from './Avatar';
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
children: React.Node;
|
||||
};
|
||||
|
||||
class AvatarWrapper extends React.Component<Props> {
|
||||
@@ -1,4 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export { default as AvatarWrapper } from "./AvatarWrapper";
|
||||
export { default as AvatarImage } from "./AvatarImage";
|
||||
2
scm-ui/ui-components/src/avatar/index.ts
Normal file
2
scm-ui/ui-components/src/avatar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AvatarWrapper } from './AvatarWrapper';
|
||||
export { default as AvatarImage } from './AvatarImage';
|
||||
@@ -1,6 +1,5 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import Button, { type ButtonProps } from "./Button";
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
class AddButton extends React.Component<ButtonProps> {
|
||||
render() {
|
||||
@@ -1,34 +1,33 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import Icon from "../Icon";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export type ButtonProps = {
|
||||
label?: string,
|
||||
loading?: boolean,
|
||||
disabled?: boolean,
|
||||
action?: (event: Event) => void,
|
||||
link?: string,
|
||||
className?: string,
|
||||
icon?: string,
|
||||
fullWidth?: boolean,
|
||||
reducedMobile?: boolean,
|
||||
children?: React.Node
|
||||
label?: string;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
action?: (event: Event) => void;
|
||||
link?: string;
|
||||
className?: string;
|
||||
icon?: string;
|
||||
fullWidth?: boolean;
|
||||
reducedMobile?: boolean;
|
||||
children?: React.Node;
|
||||
};
|
||||
|
||||
type Props = ButtonProps & {
|
||||
type: string,
|
||||
color: string,
|
||||
type: string;
|
||||
color: string;
|
||||
|
||||
// context prop
|
||||
history: any
|
||||
history: any;
|
||||
};
|
||||
|
||||
class Button extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
type: "button",
|
||||
color: "default"
|
||||
type: 'button',
|
||||
color: 'default',
|
||||
};
|
||||
|
||||
onClick = (event: Event) => {
|
||||
@@ -51,11 +50,11 @@ class Button extends React.Component<Props> {
|
||||
icon,
|
||||
fullWidth,
|
||||
reducedMobile,
|
||||
children
|
||||
children,
|
||||
} = this.props;
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
|
||||
const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : "";
|
||||
const loadingClass = loading ? 'is-loading' : '';
|
||||
const fullWidthClass = fullWidth ? 'is-fullwidth' : '';
|
||||
const reducedMobileClass = reducedMobile ? 'is-reduced-mobile' : '';
|
||||
if (icon) {
|
||||
return (
|
||||
<button
|
||||
@@ -63,12 +62,12 @@ class Button extends React.Component<Props> {
|
||||
disabled={disabled}
|
||||
onClick={this.onClick}
|
||||
className={classNames(
|
||||
"button",
|
||||
"is-" + color,
|
||||
'button',
|
||||
'is-' + color,
|
||||
loadingClass,
|
||||
fullWidthClass,
|
||||
reducedMobileClass,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="icon is-medium">
|
||||
@@ -87,11 +86,11 @@ class Button extends React.Component<Props> {
|
||||
disabled={disabled}
|
||||
onClick={this.onClick}
|
||||
className={classNames(
|
||||
"button",
|
||||
"is-" + color,
|
||||
'button',
|
||||
'is-' + color,
|
||||
loadingClass,
|
||||
fullWidthClass,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label} {children}
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Flex = styled.div`
|
||||
&.field:not(:last-child) {
|
||||
@@ -10,8 +9,8 @@ const Flex = styled.div`
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
children: React.Node
|
||||
className?: string;
|
||||
children: React.Node;
|
||||
};
|
||||
|
||||
class ButtonAddons extends React.Component<Props> {
|
||||
@@ -24,13 +23,13 @@ class ButtonAddons extends React.Component<Props> {
|
||||
childWrapper.push(
|
||||
<p className="control" key={childWrapper.length}>
|
||||
{child}
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex className={classNames("field", "has-addons", className)}>
|
||||
<Flex className={classNames('field', 'has-addons', className)}>
|
||||
{childWrapper}
|
||||
</Flex>
|
||||
);
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
children: React.Node
|
||||
className?: string;
|
||||
children: React.Node;
|
||||
};
|
||||
|
||||
class ButtonGroup extends React.Component<Props> {
|
||||
@@ -14,12 +13,16 @@ class ButtonGroup extends React.Component<Props> {
|
||||
const childWrapper = [];
|
||||
React.Children.forEach(children, child => {
|
||||
if (child) {
|
||||
childWrapper.push(<div className="control" key={childWrapper.length}>{child}</div>);
|
||||
childWrapper.push(
|
||||
<div className="control" key={childWrapper.length}>
|
||||
{child}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classNames("field", "is-grouped", className)}>
|
||||
<div className={classNames('field', 'is-grouped', className)}>
|
||||
{childWrapper}
|
||||
</div>
|
||||
);
|
||||
@@ -1,7 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Button, { type ButtonProps } from "./Button";
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-top: 2em;
|
||||
@@ -1,6 +1,5 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import Button, { type ButtonProps } from "./Button";
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
class DeleteButton extends React.Component<ButtonProps> {
|
||||
render() {
|
||||
@@ -1,11 +1,10 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
displayName: string,
|
||||
url: string,
|
||||
disabled: boolean,
|
||||
onClick?: () => void
|
||||
displayName: string;
|
||||
url: string;
|
||||
disabled: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
class DownloadButton extends React.Component<Props> {
|
||||
@@ -13,7 +12,12 @@ class DownloadButton extends React.Component<Props> {
|
||||
const { displayName, url, disabled, onClick } = this.props;
|
||||
const onClickOrDefault = !!onClick ? onClick : () => {};
|
||||
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">
|
||||
<i className="fas fa-arrow-circle-down" />
|
||||
</span>
|
||||
@@ -1,6 +1,5 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import Button, { type ButtonProps } from "./Button";
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
class EditButton extends React.Component<ButtonProps> {
|
||||
render() {
|
||||
@@ -1,13 +1,12 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { DeleteButton } from ".";
|
||||
import classNames from "classnames";
|
||||
import React from 'react';
|
||||
import { DeleteButton } from '.';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
entryname: string,
|
||||
removeEntry: string => void,
|
||||
disabled: boolean,
|
||||
label: string
|
||||
entryname: string;
|
||||
removeEntry: (p: string) => void;
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type State = {};
|
||||
@@ -16,7 +15,7 @@ class RemoveEntryOfTableButton extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { label, entryname, removeEntry, disabled } = this.props;
|
||||
return (
|
||||
<div className={classNames("is-pulled-right")}>
|
||||
<div className={classNames('is-pulled-right')}>
|
||||
<DeleteButton
|
||||
label={label}
|
||||
action={(event: Event) => {
|
||||
@@ -1,14 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import Button, { type ButtonProps } from "./Button";
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
type SubmitButtonProps = ButtonProps & {
|
||||
scrollToTop: boolean
|
||||
}
|
||||
scrollToTop: boolean;
|
||||
};
|
||||
|
||||
class SubmitButton extends React.Component<SubmitButtonProps> {
|
||||
static defaultProps = {
|
||||
scrollToTop: true
|
||||
scrollToTop: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -18,7 +17,7 @@ class SubmitButton extends React.Component<SubmitButtonProps> {
|
||||
type="submit"
|
||||
color="primary"
|
||||
{...this.props}
|
||||
action={(event) => {
|
||||
action={event => {
|
||||
if (action) {
|
||||
action(event);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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>);
|
||||
74
scm-ui/ui-components/src/buttons/index.stories.tsx
Normal file
74
scm-ui/ui-components/src/buttons/index.stories.tsx
Normal 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>);
|
||||
14
scm-ui/ui-components/src/buttons/index.ts
Normal file
14
scm-ui/ui-components/src/buttons/index.ts
Normal 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';
|
||||
@@ -1,37 +1,36 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Links } from "@scm-manager/ui-types";
|
||||
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../";
|
||||
import React from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Links } from '@scm-manager/ui-types';
|
||||
import { apiClient, SubmitButton, Loading, ErrorNotification } from '../';
|
||||
|
||||
type RenderProps = {
|
||||
readOnly: boolean,
|
||||
initialConfiguration: ConfigurationType,
|
||||
onConfigurationChange: (ConfigurationType, boolean) => void
|
||||
readOnly: boolean;
|
||||
initialConfiguration: ConfigurationType;
|
||||
onConfigurationChange: (p1: ConfigurationType, p2: boolean) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
link: string,
|
||||
render: (props: RenderProps) => any, // ???
|
||||
link: string;
|
||||
render: (props: RenderProps) => any; // ???
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
t: (p: string) => string;
|
||||
};
|
||||
|
||||
type ConfigurationType = {
|
||||
_links: Links
|
||||
} & Object;
|
||||
_links: Links;
|
||||
} & object;
|
||||
|
||||
type State = {
|
||||
error?: Error,
|
||||
fetching: boolean,
|
||||
modifying: boolean,
|
||||
contentType?: string,
|
||||
configChanged: boolean,
|
||||
error?: Error;
|
||||
fetching: boolean;
|
||||
modifying: boolean;
|
||||
contentType?: string;
|
||||
configChanged: boolean;
|
||||
|
||||
configuration?: ConfigurationType,
|
||||
modifiedConfiguration?: ConfigurationType,
|
||||
valid: boolean
|
||||
configuration?: ConfigurationType;
|
||||
modifiedConfiguration?: ConfigurationType;
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -45,7 +44,7 @@ class Configuration extends React.Component<Props, State> {
|
||||
fetching: true,
|
||||
modifying: false,
|
||||
configChanged: false,
|
||||
valid: false
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,23 +60,23 @@ class Configuration extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
captureContentType = (response: Response) => {
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
this.setState({
|
||||
contentType
|
||||
contentType,
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
getContentType = (): string => {
|
||||
const { contentType } = this.state;
|
||||
return contentType ? contentType : "application/json";
|
||||
return contentType ? contentType : 'application/json';
|
||||
};
|
||||
|
||||
handleError = (error: Error) => {
|
||||
this.setState({
|
||||
error,
|
||||
fetching: false,
|
||||
modifying: false
|
||||
modifying: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -85,11 +84,11 @@ class Configuration extends React.Component<Props, State> {
|
||||
this.setState({
|
||||
configuration,
|
||||
fetching: false,
|
||||
error: undefined
|
||||
error: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
getModificationUrl = (): ?string => {
|
||||
getModificationUrl = (): string | null | undefined => {
|
||||
const { configuration } = this.state;
|
||||
if (configuration) {
|
||||
const links = configuration._links;
|
||||
@@ -107,14 +106,16 @@ class Configuration extends React.Component<Props, State> {
|
||||
configurationChanged = (configuration: ConfigurationType, valid: boolean) => {
|
||||
this.setState({
|
||||
modifiedConfiguration: configuration,
|
||||
valid
|
||||
valid,
|
||||
});
|
||||
};
|
||||
|
||||
modifyConfiguration = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({ modifying: true });
|
||||
this.setState({
|
||||
modifying: true,
|
||||
});
|
||||
|
||||
const { modifiedConfiguration } = this.state;
|
||||
|
||||
@@ -122,9 +123,15 @@ class Configuration extends React.Component<Props, State> {
|
||||
.put(
|
||||
this.getModificationUrl(),
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -134,9 +141,13 @@ class Configuration extends React.Component<Props, State> {
|
||||
<div className="notification is-primary">
|
||||
<button
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -157,7 +168,7 @@ class Configuration extends React.Component<Props, State> {
|
||||
const renderProps: RenderProps = {
|
||||
readOnly,
|
||||
initialConfiguration: configuration,
|
||||
onConfigurationChange: this.configurationChanged
|
||||
onConfigurationChange: this.configurationChanged,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -167,7 +178,7 @@ class Configuration extends React.Component<Props, State> {
|
||||
{this.props.render(renderProps)}
|
||||
<hr />
|
||||
<SubmitButton
|
||||
label={t("config.form.submit")}
|
||||
label={t('config.form.submit')}
|
||||
disabled={!valid || readOnly}
|
||||
loading={modifying}
|
||||
/>
|
||||
@@ -178,4 +189,4 @@ class Configuration extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("config")(Configuration);
|
||||
export default translate('config')(Configuration);
|
||||
@@ -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();
|
||||
134
scm-ui/ui-components/src/config/ConfigurationBinder.tsx
Normal file
134
scm-ui/ui-components/src/config/ConfigurationBinder.tsx
Normal 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();
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
export { default as ConfigurationBinder } from "./ConfigurationBinder";
|
||||
export { default as Configuration } from "./Configuration";
|
||||
2
scm-ui/ui-components/src/config/index.ts
Normal file
2
scm-ui/ui-components/src/config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ConfigurationBinder } from './ConfigurationBinder';
|
||||
export { default as Configuration } from './Configuration';
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
40
scm-ui/ui-components/src/errors.test.ts
Normal file
40
scm-ui/ui-components/src/errors.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,25 @@
|
||||
// @flow
|
||||
type Context = { type: string, id: string }[];
|
||||
type Violation = { path: string, message: string };
|
||||
type Context = {
|
||||
type: string;
|
||||
id: string;
|
||||
}[];
|
||||
type Violation = {
|
||||
path: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type BackendErrorContent = {
|
||||
transactionId: string,
|
||||
errorCode: string,
|
||||
message: string,
|
||||
url?: string,
|
||||
context: Context,
|
||||
violations: Violation[]
|
||||
transactionId: string;
|
||||
errorCode: string;
|
||||
message: string;
|
||||
url?: string;
|
||||
context: Context;
|
||||
violations: Violation[];
|
||||
};
|
||||
|
||||
export class BackendError extends Error {
|
||||
transactionId: string;
|
||||
errorCode: string;
|
||||
url: ?string;
|
||||
url: string | null | undefined;
|
||||
context: Context = [];
|
||||
statusCode: number;
|
||||
violations: Violation[];
|
||||
@@ -49,19 +54,19 @@ export class ForbiddenError extends Error {
|
||||
|
||||
export class NotFoundError extends BackendError {
|
||||
constructor(content: BackendErrorContent, statusCode: number) {
|
||||
super(content, "NotFoundError", statusCode);
|
||||
super(content, 'NotFoundError', statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends BackendError {
|
||||
constructor(content: BackendErrorContent, statusCode: number) {
|
||||
super(content, "ConflictError", statusCode);
|
||||
super(content, 'ConflictError', statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export function createBackendError(
|
||||
content: BackendErrorContent,
|
||||
statusCode: number
|
||||
statusCode: number,
|
||||
) {
|
||||
switch (statusCode) {
|
||||
case 404:
|
||||
@@ -69,13 +74,13 @@ export function createBackendError(
|
||||
case 409:
|
||||
return new ConflictError(content, statusCode);
|
||||
default:
|
||||
return new BackendError(content, "BackendError", statusCode);
|
||||
return new BackendError(content, 'BackendError', statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export function isBackendError(response: Response) {
|
||||
return (
|
||||
response.headers.get("Content-Type") ===
|
||||
"application/vnd.scmm-error+json;v=2"
|
||||
response.headers.get('Content-Type') ===
|
||||
'application/vnd.scmm-error+json;v=2'
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,37 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import { AddButton } from "../buttons";
|
||||
import InputField from "./InputField";
|
||||
import { AddButton } from '../buttons';
|
||||
import InputField from './InputField';
|
||||
|
||||
type Props = {
|
||||
addEntry: string => void,
|
||||
disabled: boolean,
|
||||
buttonLabel: string,
|
||||
fieldLabel: string,
|
||||
errorMessage: string,
|
||||
helpText?: string,
|
||||
validateEntry?: string => boolean
|
||||
addEntry: (p: string) => void;
|
||||
disabled: boolean;
|
||||
buttonLabel: string;
|
||||
fieldLabel: string;
|
||||
errorMessage: string;
|
||||
helpText?: string;
|
||||
validateEntry?: (p: string) => boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
entryToAdd: string
|
||||
entryToAdd: string;
|
||||
};
|
||||
|
||||
class AddEntryToTableField extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
entryToAdd: ""
|
||||
entryToAdd: '',
|
||||
};
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
const { validateEntry } = this.props;
|
||||
if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) {
|
||||
if (
|
||||
!this.state.entryToAdd ||
|
||||
this.state.entryToAdd === '' ||
|
||||
!validateEntry
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return validateEntry(this.state.entryToAdd);
|
||||
@@ -41,7 +44,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
buttonLabel,
|
||||
fieldLabel,
|
||||
errorMessage,
|
||||
helpText
|
||||
helpText,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
@@ -58,7 +61,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
<AddButton
|
||||
label={buttonLabel}
|
||||
action={this.addButtonClicked}
|
||||
disabled={disabled || this.state.entryToAdd ==="" || !this.isValid()}
|
||||
disabled={disabled || this.state.entryToAdd === '' || !this.isValid()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -72,13 +75,16 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
appendEntry = () => {
|
||||
const { entryToAdd } = this.state;
|
||||
this.props.addEntry(entryToAdd);
|
||||
this.setState({ ...this.state, entryToAdd: "" });
|
||||
this.setState({
|
||||
...this.state,
|
||||
entryToAdd: '',
|
||||
});
|
||||
};
|
||||
|
||||
handleAddEntryChange = (entryname: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
entryToAdd: entryname
|
||||
entryToAdd: entryname,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,30 +1,31 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
|
||||
import Autocomplete from "../Autocomplete";
|
||||
import AddButton from "../buttons/AddButton";
|
||||
import { AutocompleteObject, SelectValue } from '@scm-manager/ui-types';
|
||||
import Autocomplete from '../Autocomplete';
|
||||
import AddButton from '../buttons/AddButton';
|
||||
|
||||
type Props = {
|
||||
addEntry: SelectValue => void,
|
||||
disabled: boolean,
|
||||
buttonLabel: string,
|
||||
fieldLabel: string,
|
||||
helpText?: string,
|
||||
loadSuggestions: string => Promise<AutocompleteObject>,
|
||||
placeholder?: string,
|
||||
loadingMessage?: string,
|
||||
noOptionsMessage?: string
|
||||
addEntry: (p: SelectValue) => void;
|
||||
disabled: boolean;
|
||||
buttonLabel: string;
|
||||
fieldLabel: string;
|
||||
helpText?: string;
|
||||
loadSuggestions: (p: string) => Promise<AutocompleteObject>;
|
||||
placeholder?: string;
|
||||
loadingMessage?: string;
|
||||
noOptionsMessage?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedValue?: SelectValue
|
||||
selectedValue?: SelectValue;
|
||||
};
|
||||
|
||||
class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { selectedValue: undefined };
|
||||
this.state = {
|
||||
selectedValue: undefined,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
@@ -35,7 +36,7 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
loadSuggestions,
|
||||
placeholder,
|
||||
loadingMessage,
|
||||
noOptionsMessage
|
||||
noOptionsMessage,
|
||||
} = this.props;
|
||||
|
||||
const { selectedValue } = this.state;
|
||||
@@ -73,15 +74,19 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
// $FlowFixMe null is needed to clear the selection; undefined does not work
|
||||
this.setState({ ...this.state, selectedValue: null }, () =>
|
||||
this.props.addEntry(selectedValue)
|
||||
this.setState(
|
||||
{
|
||||
...this.state,
|
||||
selectedValue: null,
|
||||
},
|
||||
() => this.props.addEntry(selectedValue),
|
||||
);
|
||||
};
|
||||
|
||||
handleAddEntryChange = (selection: SelectValue) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedValue: selection
|
||||
selectedValue: selection,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Help } from "../index";
|
||||
import React from 'react';
|
||||
import { Help } from '../index';
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
name?: string,
|
||||
checked: boolean,
|
||||
onChange?: (value: boolean, name?: string) => void,
|
||||
disabled?: boolean,
|
||||
helpText?: string
|
||||
label?: string;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?: (value: boolean, name?: string) => void;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
};
|
||||
|
||||
class Checkbox extends React.Component<Props> {
|
||||
|
||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(event.target.checked, this.props.name);
|
||||
@@ -36,8 +34,7 @@ class Checkbox extends React.Component<Props> {
|
||||
checked={this.props.checked}
|
||||
onChange={this.onCheckboxChange}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
{" "}
|
||||
/>{' '}
|
||||
{this.props.label}
|
||||
{this.renderHelp()}
|
||||
</label>
|
||||
@@ -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;
|
||||
56
scm-ui/ui-components/src/forms/DropDown.tsx
Normal file
56
scm-ui/ui-components/src/forms/DropDown.tsx
Normal 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
Reference in New Issue
Block a user