re integrate @scm-manager/ui-types into @scm-manager/ui-components to reduce maven modules

This commit is contained in:
Sebastian Sdorra
2018-09-03 16:17:36 +02:00
parent 0a0fd7a261
commit 6a4f3a8cf6
79 changed files with 12913 additions and 480 deletions

View File

@@ -69,6 +69,7 @@
<module>scm-annotation-processor</module> <module>scm-annotation-processor</module>
<module>scm-core</module> <module>scm-core</module>
<module>scm-test</module> <module>scm-test</module>
<module>scm-ui-components</module>
<module>scm-plugins</module> <module>scm-plugins</module>
<module>scm-dao-xml</module> <module>scm-dao-xml</module>
<module>scm-ui</module> <module>scm-ui</module>

View File

@@ -2,7 +2,6 @@
.*/node_modules/module-deps/.* .*/node_modules/module-deps/.*
[include] [include]
../ui-types/.*
[libs] [libs]

View File

@@ -8,6 +8,6 @@
"@scm-manager/ui-extensions": "^0.0.7" "@scm-manager/ui-extensions": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.7" "@scm-manager/ui-bundler": "^0.0.11"
} }
} }

View File

@@ -61,6 +61,33 @@
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.1.0</version>
<configuration>
<node>
<version>8.11.4</version>
</node>
<pkgManager>
<type>YARN</type>
<version>1.9.4</version>
</pkgManager>
</configuration>
<executions>
<execution>
<id>link-components</id>
<phase>process-resources</phase>
<goals>
<goal>install-link</goal>
</goals>
<configuration>
<pkg>@scm-manager/ui-components</pkg>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>

View File

@@ -1,5 +1,6 @@
//@flow //@flow
import React from 'react'; import React from "react";
import { Image } from "@scm-manager/ui-components";
type Props = { type Props = {
}; };
@@ -7,8 +8,7 @@ type Props = {
class GitAvatar extends React.Component<Props> { class GitAvatar extends React.Component<Props> {
render() { render() {
// TODO we have to use Image from ui-components return <Image src="/images/git-logo.png" alt="Git Logo" />;
return <img src="/scm/images/git-logo.png" alt="Git Logo" />;
} }
} }

View File

@@ -1,9 +1,9 @@
//@flow //@flow
import React from 'react'; import React from "react";
import { Repository } from "@scm-manager/ui-types";
// TODO flow types ???
type Props = { type Props = {
repository: Object repository: Repository
} }
class ProtocolInformation extends React.Component<Props> { class ProtocolInformation extends React.Component<Props> {

View File

@@ -1,6 +1,7 @@
//@flow
import { binder } from "@scm-manager/ui-extensions"; import { binder } from "@scm-manager/ui-extensions";
import ProtocolInformation from './ProtocolInformation'; import ProtocolInformation from "./ProtocolInformation";
import GitAvatar from './GitAvatar'; import GitAvatar from "./GitAvatar";
const gitPredicate = (props: Object) => { const gitPredicate = (props: Object) => {
return props.repository && props.repository.type === "git"; return props.repository && props.repository.type === "git";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"],
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "@scm-manager/eslint-config"
}

View File

@@ -0,0 +1,8 @@
[ignore]
.*/node_modules/module-deps/.*
[include]
[libs]
[options]

View File

@@ -0,0 +1,23 @@
// flow-typed signature: cf86673cc32d185bdab1d2ea90578d37
// flow-typed version: 614bf49aa8/classnames_v2.x.x/flow_>=v0.25.x
type $npm$classnames$Classes =
| string
| { [className: string]: * }
| false
| void
| null;
declare module "classnames" {
declare module.exports: (
...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]>
) => string;
}
declare module "classnames/bind" {
declare module.exports: $Exports<"classnames">;
}
declare module "classnames/dedupe" {
declare module.exports: $Exports<"classnames">;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,331 @@
// flow-typed signature: 23b805356f90ad9384dd88489654e380
// flow-typed version: e9374c5fe9/moment_v2.3.x/flow_>=v0.25.x
type moment$MomentOptions = {
y?: number | string,
year?: number | string,
years?: number | string,
M?: number | string,
month?: number | string,
months?: number | string,
d?: number | string,
day?: number | string,
days?: number | string,
date?: number | string,
h?: number | string,
hour?: number | string,
hours?: number | string,
m?: number | string,
minute?: number | string,
minutes?: number | string,
s?: number | string,
second?: number | string,
seconds?: number | string,
ms?: number | string,
millisecond?: number | string,
milliseconds?: number | string
};
type moment$MomentObject = {
years: number,
months: number,
date: number,
hours: number,
minutes: number,
seconds: number,
milliseconds: number
};
type moment$MomentCreationData = {
input: string,
format: string,
locale: Object,
isUTC: boolean,
strict: boolean
};
type moment$CalendarFormat = string | ((moment: moment$Moment) => string);
type moment$CalendarFormats = {
sameDay?: moment$CalendarFormat,
nextDay?: moment$CalendarFormat,
nextWeek?: moment$CalendarFormat,
lastDay?: moment$CalendarFormat,
lastWeek?: moment$CalendarFormat,
sameElse?: moment$CalendarFormat
};
declare class moment$LocaleData {
months(moment: moment$Moment): string,
monthsShort(moment: moment$Moment): string,
monthsParse(month: string): number,
weekdays(moment: moment$Moment): string,
weekdaysShort(moment: moment$Moment): string,
weekdaysMin(moment: moment$Moment): string,
weekdaysParse(weekDay: string): number,
longDateFormat(dateFormat: string): string,
isPM(date: string): boolean,
meridiem(hours: number, minutes: number, isLower: boolean): string,
calendar(
key:
| "sameDay"
| "nextDay"
| "lastDay"
| "nextWeek"
| "prevWeek"
| "sameElse",
moment: moment$Moment
): string,
relativeTime(
number: number,
withoutSuffix: boolean,
key: "s" | "m" | "mm" | "h" | "hh" | "d" | "dd" | "M" | "MM" | "y" | "yy",
isFuture: boolean
): string,
pastFuture(diff: any, relTime: string): string,
ordinal(number: number): string,
preparse(str: string): any,
postformat(str: string): any,
week(moment: moment$Moment): string,
invalidDate(): string,
firstDayOfWeek(): number,
firstDayOfYear(): number
}
declare class moment$MomentDuration {
humanize(suffix?: boolean): string,
milliseconds(): number,
asMilliseconds(): number,
seconds(): number,
asSeconds(): number,
minutes(): number,
asMinutes(): number,
hours(): number,
asHours(): number,
days(): number,
asDays(): number,
months(): number,
asWeeks(): number,
weeks(): number,
asMonths(): number,
years(): number,
asYears(): number,
add(value: number | moment$MomentDuration | Object, unit?: string): this,
subtract(value: number | moment$MomentDuration | Object, unit?: string): this,
as(unit: string): number,
get(unit: string): number,
toJSON(): string,
toISOString(): string,
isValid(): boolean
}
declare class moment$Moment {
static ISO_8601: string,
static (
string?: string,
format?: string | Array<string>,
strict?: boolean
): moment$Moment,
static (
string?: string,
format?: string | Array<string>,
locale?: string,
strict?: boolean
): moment$Moment,
static (
initDate: ?Object | number | Date | Array<number> | moment$Moment | string
): moment$Moment,
static unix(seconds: number): moment$Moment,
static utc(): moment$Moment,
static utc(number: number | Array<number>): moment$Moment,
static utc(
str: string,
str2?: string | Array<string>,
str3?: string
): moment$Moment,
static utc(moment: moment$Moment): moment$Moment,
static utc(date: Date): moment$Moment,
static parseZone(): moment$Moment,
static parseZone(rawDate: string): moment$Moment,
static parseZone(
rawDate: string,
format: string | Array<string>
): moment$Moment,
static parseZone(
rawDate: string,
format: string,
strict: boolean
): moment$Moment,
static parseZone(
rawDate: string,
format: string,
locale: string,
strict: boolean
): moment$Moment,
isValid(): boolean,
invalidAt(): 0 | 1 | 2 | 3 | 4 | 5 | 6,
creationData(): moment$MomentCreationData,
millisecond(number: number): this,
milliseconds(number: number): this,
millisecond(): number,
milliseconds(): number,
second(number: number): this,
seconds(number: number): this,
second(): number,
seconds(): number,
minute(number: number): this,
minutes(number: number): this,
minute(): number,
minutes(): number,
hour(number: number): this,
hours(number: number): this,
hour(): number,
hours(): number,
date(number: number): this,
dates(number: number): this,
date(): number,
dates(): number,
day(day: number | string): this,
days(day: number | string): this,
day(): number,
days(): number,
weekday(number: number): this,
weekday(): number,
isoWeekday(number: number): this,
isoWeekday(): number,
dayOfYear(number: number): this,
dayOfYear(): number,
week(number: number): this,
weeks(number: number): this,
week(): number,
weeks(): number,
isoWeek(number: number): this,
isoWeeks(number: number): this,
isoWeek(): number,
isoWeeks(): number,
month(number: number): this,
months(number: number): this,
month(): number,
months(): number,
quarter(number: number): this,
quarter(): number,
year(number: number): this,
years(number: number): this,
year(): number,
years(): number,
weekYear(number: number): this,
weekYear(): number,
isoWeekYear(number: number): this,
isoWeekYear(): number,
weeksInYear(): number,
isoWeeksInYear(): number,
get(string: string): number,
set(unit: string, value: number): this,
set(options: { [unit: string]: number }): this,
static max(...dates: Array<moment$Moment>): moment$Moment,
static max(dates: Array<moment$Moment>): moment$Moment,
static min(...dates: Array<moment$Moment>): moment$Moment,
static min(dates: Array<moment$Moment>): moment$Moment,
add(
value: number | moment$MomentDuration | moment$Moment | Object,
unit?: string
): this,
subtract(
value: number | moment$MomentDuration | moment$Moment | string | Object,
unit?: string
): this,
startOf(unit: string): this,
endOf(unit: string): this,
local(): this,
utc(): this,
utcOffset(
offset: number | string,
keepLocalTime?: boolean,
keepMinutes?: boolean
): this,
utcOffset(): number,
format(format?: string): string,
fromNow(removeSuffix?: boolean): string,
from(
value: moment$Moment | string | number | Date | Array<number>,
removePrefix?: boolean
): string,
toNow(removePrefix?: boolean): string,
to(
value: moment$Moment | string | number | Date | Array<number>,
removePrefix?: boolean
): string,
calendar(refTime?: any, formats?: moment$CalendarFormats): string,
diff(
date: moment$Moment | string | number | Date | Array<number>,
format?: string,
floating?: boolean
): number,
valueOf(): number,
unix(): number,
daysInMonth(): number,
toDate(): Date,
toArray(): Array<number>,
toJSON(): string,
toISOString(
keepOffset?: boolean
): string,
toObject(): moment$MomentObject,
isBefore(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSame(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isAfter(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSameOrBefore(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSameOrAfter(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isBetween(
fromDate: moment$Moment | string | number | Date | Array<number>,
toDate?: ?moment$Moment | string | number | Date | Array<number>,
granularity?: ?string,
inclusion?: ?string
): boolean,
isDST(): boolean,
isDSTShifted(): boolean,
isLeapYear(): boolean,
clone(): moment$Moment,
static isMoment(obj: any): boolean,
static isDate(obj: any): boolean,
static locale(locale: string, localeData?: Object): string,
static updateLocale(locale: string, localeData?: ?Object): void,
static locale(locales: Array<string>): string,
locale(locale: string, customization?: Object | null): moment$Moment,
locale(): string,
static months(): Array<string>,
static monthsShort(): Array<string>,
static weekdays(): Array<string>,
static weekdaysShort(): Array<string>,
static weekdaysMin(): Array<string>,
static months(): string,
static monthsShort(): string,
static weekdays(): string,
static weekdaysShort(): string,
static weekdaysMin(): string,
static localeData(key?: string): moment$LocaleData,
static duration(
value: number | Object | string,
unit?: string
): moment$MomentDuration,
static isDuration(obj: any): boolean,
static normalizeUnits(unit: string): string,
static invalid(object: any): moment$Moment
}
declare module "moment" {
declare module.exports: Class<moment$Moment>;
}

View File

@@ -0,0 +1,137 @@
// flow-typed signature: ba35d02d668b0d0a3e04a63a6847974e
// flow-typed version: <<STUB>>/react-jss_v8.6.1/flow_v0.79.1
/**
* This is an autogenerated libdef stub for:
*
* 'react-jss'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-jss' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-jss/dist/react-jss' {
declare module.exports: any;
}
declare module 'react-jss/dist/react-jss.min' {
declare module.exports: any;
}
declare module 'react-jss/lib/compose' {
declare module.exports: any;
}
declare module 'react-jss/lib/compose.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/contextTypes' {
declare module.exports: any;
}
declare module 'react-jss/lib/createHoc' {
declare module.exports: any;
}
declare module 'react-jss/lib/getDisplayName' {
declare module.exports: any;
}
declare module 'react-jss/lib/index' {
declare module.exports: any;
}
declare module 'react-jss/lib/index.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/injectSheet' {
declare module.exports: any;
}
declare module 'react-jss/lib/injectSheet.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/jss' {
declare module.exports: any;
}
declare module 'react-jss/lib/JssProvider' {
declare module.exports: any;
}
declare module 'react-jss/lib/JssProvider.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/ns' {
declare module.exports: any;
}
declare module 'react-jss/lib/propTypes' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-jss/dist/react-jss.js' {
declare module.exports: $Exports<'react-jss/dist/react-jss'>;
}
declare module 'react-jss/dist/react-jss.min.js' {
declare module.exports: $Exports<'react-jss/dist/react-jss.min'>;
}
declare module 'react-jss/lib/compose.js' {
declare module.exports: $Exports<'react-jss/lib/compose'>;
}
declare module 'react-jss/lib/compose.test.js' {
declare module.exports: $Exports<'react-jss/lib/compose.test'>;
}
declare module 'react-jss/lib/contextTypes.js' {
declare module.exports: $Exports<'react-jss/lib/contextTypes'>;
}
declare module 'react-jss/lib/createHoc.js' {
declare module.exports: $Exports<'react-jss/lib/createHoc'>;
}
declare module 'react-jss/lib/getDisplayName.js' {
declare module.exports: $Exports<'react-jss/lib/getDisplayName'>;
}
declare module 'react-jss/lib/index.js' {
declare module.exports: $Exports<'react-jss/lib/index'>;
}
declare module 'react-jss/lib/index.test.js' {
declare module.exports: $Exports<'react-jss/lib/index.test'>;
}
declare module 'react-jss/lib/injectSheet.js' {
declare module.exports: $Exports<'react-jss/lib/injectSheet'>;
}
declare module 'react-jss/lib/injectSheet.test.js' {
declare module.exports: $Exports<'react-jss/lib/injectSheet.test'>;
}
declare module 'react-jss/lib/jss.js' {
declare module.exports: $Exports<'react-jss/lib/jss'>;
}
declare module 'react-jss/lib/JssProvider.js' {
declare module.exports: $Exports<'react-jss/lib/JssProvider'>;
}
declare module 'react-jss/lib/JssProvider.test.js' {
declare module.exports: $Exports<'react-jss/lib/JssProvider.test'>;
}
declare module 'react-jss/lib/ns.js' {
declare module.exports: $Exports<'react-jss/lib/ns'>;
}
declare module 'react-jss/lib/propTypes.js' {
declare module.exports: $Exports<'react-jss/lib/propTypes'>;
}

View File

@@ -0,0 +1,43 @@
{
"name": "@scm-manager/ui-components",
"version": "0.0.1",
"description": "UI Components for SCM-Manager and its plugins",
"main": "src/index.js",
"repository": "https://bitbucket.org/sdorra/scm-manager",
"author": "Sebastian Sdorra <s.sdorra@gmail.com>",
"license": "MIT",
"scripts": {
"update-index": "create-index -r src",
"eslint-fix": "eslint src --fix"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.12",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",
"flow-bin": "^0.79.1",
"flow-typed": "^2.5.1",
"jest": "^23.5.0",
"raf": "^3.4.0"
},
"dependencies": {
"classnames": "^2.2.6",
"moment": "^2.22.2",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-i18next": "^7.11.0",
"react-jss": "^8.6.1",
"react-router-dom": "^4.3.1"
},
"browserify": {
"transform": [
[
"babelify",
{
"plugins": ["@babel/plugin-proposal-class-properties"],
"presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"]
}
]
]
}
}

47
scm-ui-components/pom.xml Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>sonia.scm</groupId>
<artifactId>scm</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-ui-components</artifactId>
<packaging>pom</packaging>
<version>2.0.0-SNAPSHOT</version>
<name>scm-ui-components</name>
<build>
<plugins>
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.1.0</version>
<configuration>
<node>
<version>8.11.4</version>
</node>
<pkgManager>
<type>YARN</type>
<version>1.9.4</version>
</pkgManager>
</configuration>
<executions>
<execution>
<id>link</id>
<phase>process-resources</phase>
<goals>
<goal>link</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
//@flow
import React from "react";
export type SelectItem = {
value: string,
label: string
};
type Props = {
label?: string,
options: SelectItem[],
value?: SelectItem,
onChange: string => void
};
class Select extends React.Component<Props> {
field: ?HTMLSelectElement;
componentDidMount() {
// trigger change after render, if value is null to set it to the first value
// of the given options.
if (!this.props.value && this.field && this.field.value) {
this.props.onChange(this.field.value);
}
}
handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { options, value } = this.props;
return (
<div className="field">
{this.renderLabel()}
<div className="control select">
<select
ref={input => {
this.field = input;
}}
value={value}
onChange={this.handleInput}
>
{options.map(opt => {
return (
<option value={opt.value} key={opt.value}>
{opt.label}
</option>
);
})}
</select>
</div>
</div>
);
}
}
export default Select;

View File

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

View File

@@ -0,0 +1,8 @@
// @create-index
export { default as AddEntryToTableField } from "./AddEntryToTableField.js";
export { default as Checkbox } from "./Checkbox.js";
export { default as InputField } from "./InputField.js";
export { default as Select } from "./Select.js";
export { default as Textarea } from "./Textarea.js";

View File

@@ -0,0 +1,15 @@
// @create-index
export { default as DateFromNow } from "./DateFromNow.js";
export { default as ErrorNotification } from "./ErrorNotification.js";
export { default as ErrorPage } from "./ErrorPage.js";
export { default as Image } from "./Image.js";
export { default as Loading } from "./Loading.js";
export { default as Logo } from "./Logo.js";
export { default as MailLink } from "./MailLink.js";
export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as urls } from "./urls.js";
export { default as validation } from "./validation.js";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
// @create-index
export { default as Footer } from "./Footer.js";
export { default as Header } from "./Header.js";
export { default as Page } from "./Page.js";
export { default as Subtitle } from "./Subtitle.js";
export { default as Title } from "./Title.js";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
// @create-index
export { default as NavAction } from "./NavAction.js";
export { default as NavLink } from "./NavLink.js";
export { default as Navigation } from "./Navigation.js";
export { default as PrimaryNavigation } from "./PrimaryNavigation.js";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js";
export { default as Section } from "./Section.js";

View File

@@ -0,0 +1,12 @@
import "raf/polyfill";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
// Temporary hack to suppress error
// https://github.com/facebook/create-react-app/issues/3199#issuecomment-345024029
window.requestAnimationFrame = function(callback) {
setTimeout(callback, 0);
return 0;
};
configure({ adapter: new Adapter() });

View File

@@ -0,0 +1,7 @@
jest.mock("react-i18next", () => ({
// this mock makes sure any components using the translate HoC receive the t function as a prop
translate: () => Component => {
Component.defaultProps = { ...Component.defaultProps, t: key => key };
return Component;
}
}));

View File

@@ -0,0 +1,7 @@
// @flow
export type Action = {
type: string,
payload?: any,
itemId?: string | number,
resetPending?: boolean
};

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
// @flow
export type Me = {
name: string,
displayName: string
};

View File

@@ -0,0 +1,24 @@
//@flow
import type { PagedCollection, Links } from "./hal";
export type Repository = {
namespace: string,
name: string,
type: string,
contact?: string,
description?: string,
creationDate?: string,
lastModified?: string,
_links: Links
};
export type RepositoryCollection = PagedCollection & {
_embedded: {
repositories: Repository[] | string[]
}
};
export type RepositoryGroup = {
name: string,
repositories: Repository[]
};

View File

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

View File

@@ -0,0 +1,15 @@
//@flow
import type { Links } from "./hal";
export type User = {
displayName: string,
name: string,
mail: string,
password: string,
admin: boolean,
active: boolean,
type?: string,
creationDate?: string,
lastModified?: string,
_links: Links
};

View File

@@ -0,0 +1,16 @@
// @flow
export type Link = {
href: string
};
export type Links = { [string]: Link };
export type Collection = {
_embedded: Object,
_links: Links
};
export type PagedCollection = Collection & {
page: number,
pageTotal: number
};

View File

@@ -0,0 +1,12 @@
// @flow
export type { Action } from "./Action";
export type { Link, Links, Collection, PagedCollection } from "./hal";
export type { Me } from "./Me";
export type { User } from "./User";
export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export type { Config } from "./Config";

View File

@@ -0,0 +1,6 @@
// @flow
export const contextPath = window.ctxPath || "";
export function withContextPath(path: string) {
return contextPath + path;
}

View File

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

View File

@@ -0,0 +1,102 @@
// @flow
import * as validator from "./validation";
describe("test name validation", () => {
it("should return false", () => {
// invalid names taken from ValidationUtilTest.java
const invalidNames = [
" test 123",
" test 123 ",
"test 123 ",
"test/123",
"test%123",
"test:123",
"t ",
" t",
" t ",
"",
" invalid_name",
"another%one",
"!!!",
"!_!"
];
for (let name of invalidNames) {
expect(validator.isNameValid(name)).toBe(false);
}
});
it("should return true", () => {
// valid names taken from ValidationUtilTest.java
const validNames = [
"test",
"test.git",
"Test123.git",
"Test123-git",
"Test_user-123.git",
"test@scm-manager.de",
"test 123",
"tt",
"t",
"valid_name",
"another1",
"stillValid",
"this.one_as-well",
"and@this"
];
for (let name of validNames) {
expect(validator.isNameValid(name)).toBe(true);
}
});
});
describe("test mail validation", () => {
it("should return false", () => {
// invalid taken from ValidationUtilTest.java
const invalid = [
"ostfalia.de",
"@ostfalia.de",
"s.sdorra@",
"s.sdorra@ostfalia",
"s.sdorra@@ostfalia.de",
"s.sdorra@ ostfalia.de",
"s.sdorra @ostfalia.de"
];
for (let mail of invalid) {
expect(validator.isMailValid(mail)).toBe(false);
}
});
it("should return true", () => {
// valid taken from ValidationUtilTest.java
const valid = [
"s.sdorra@ostfalia.de",
"sdorra@ostfalia.de",
"s.sdorra@hbk-bs.de",
"s.sdorra@gmail.com",
"s.sdorra@t.co",
"s.sdorra@ucla.college",
"s.sdorra@example.xn--p1ai",
"s.sdorra@scm.solutions"
];
for (let mail of valid) {
expect(validator.isMailValid(mail)).toBe(true);
}
});
});
describe("test number validation", () => {
it("should return false", () => {
const invalid = ["1a", "35gu", "dj6", "45,5", "test"];
for (let number of invalid) {
expect(validator.isNumberValid(number)).toBe(false);
}
});
it("should return true", () => {
const valid = ["1", "35", "2", "235", "34.4"];
for (let number of valid) {
expect(validator.isNumberValid(number)).toBe(true);
}
});
});

8163
scm-ui-components/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -31,8 +31,8 @@
"webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/styles/webfonts", "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/styles/webfonts",
"build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles", "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles",
"watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles --watch --recursive", "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles --watch --recursive",
"start-js": "ui-bundler serve", "start-js": "ui-bundler serve --vendor vendor.bundle.js",
"start": "npm-run-all -p webfonts watch-css build-vendor start-js", "start": "npm-run-all -p webfonts watch-css start-js",
"build-js": "ui-bundler bundle target/scm-ui.bundle.js", "build-js": "ui-bundler bundle target/scm-ui.bundle.js",
"build-vendor": "ui-bundler vendor target/vendor.bundle.js", "build-vendor": "ui-bundler vendor target/vendor.bundle.js",
"build": "npm-run-all -s webfonts build-css build-vendor build-js", "build": "npm-run-all -s webfonts build-css build-vendor build-js",
@@ -42,7 +42,7 @@
"pre-commit": "jest && flow && eslint src" "pre-commit": "jest && flow && eslint src"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.9", "@scm-manager/ui-bundler": "^0.0.12",
"copyfiles": "^2.0.0", "copyfiles": "^2.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",

View File

@@ -31,18 +31,28 @@
<plugin> <plugin>
<groupId>com.github.sdorra</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId> <artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.0.1</version> <version>2.1.0</version>
<configuration> <configuration>
<node> <node>
<version>8.11.3</version> <version>8.11.4</version>
</node> </node>
<pkgManager> <pkgManager>
<type>YARN</type> <type>YARN</type>
<version>1.7.0</version> <version>1.9.4</version>
</pkgManager> </pkgManager>
<script>run</script> <script>run</script>
</configuration> </configuration>
<executions> <executions>
<execution>
<id>link-components</id>
<phase>process-resources</phase>
<goals>
<goal>install-link</goal>
</goals>
<configuration>
<pkg>@scm-manager/ui-components</pkg>
</configuration>
</execution>
<execution> <execution>
<id>install</id> <id>install</id>
<phase>process-resources</phase> <phase>process-resources</phase>

View File

@@ -732,9 +732,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.9": "@scm-manager/ui-bundler@^0.0.10":
version "0.0.9" version "0.0.10"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.9.tgz#b61bdccaf6cf0ff3f4856098f95b462223c626c8" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.10.tgz#94c90ed1c1830352e274e048158843436ce31352"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"