This commit is contained in:
Eduard Heimbuch
2019-09-25 15:34:48 +02:00
41 changed files with 829 additions and 383 deletions

View File

@@ -1,8 +1,9 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import Tooltip from './Tooltip';
import HelpIcon from './HelpIcon';
import classNames from "classnames";
import Tooltip from "./Tooltip";
import HelpIcon from "./HelpIcon";
const styles = {
tooltip: {
@@ -14,21 +15,22 @@ const styles = {
type Props = {
message: string,
className?: string,
classes: any
}
};
class Help extends React.Component<Props> {
render() {
const { message, classes } = this.props;
const { message, className, classes } = this.props;
return (
<Tooltip className={classes.tooltip} message={message}>
<Tooltip
className={classNames(classes.tooltip, className)}
message={message}
>
<HelpIcon />
</Tooltip>
);
}
}
export default injectSheet(styles)(Help);

View File

@@ -1,22 +1,24 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import Icon from "./Icon";
type Props = {
classes: any
};
const styles = {
textinfo: {
color: "#98d8f3 !important"
}
textinfo: {
color: "#98d8f3 !important"
}
};
class HelpIcon extends React.Component<Props> {
render() {
const { classes } = this.props;
return <i className={classNames("fa fa-question-circle has-text-info", classes.textinfo)}></i>;
return (
<Icon className={classes.textinfo} name="question-circle" />
);
}
}

View File

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

View File

@@ -1,11 +1,11 @@
//@flow
import React from "react";
import SyntaxHighlighter from "./SyntaxHighlighter";
import Markdown from "react-markdown/with-html";
import {binder} from "@scm-manager/ui-extensions";
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
import { withRouter } from "react-router-dom";
import injectSheet from "react-jss";
import Markdown from "react-markdown/with-html";
import { binder } from "@scm-manager/ui-extensions";
import SyntaxHighlighter from "./SyntaxHighlighter";
import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer";
type Props = {
content: string,
@@ -14,11 +14,34 @@ type Props = {
enableAnchorHeadings: boolean,
// context props
classes: any,
location: any
};
class MarkdownView extends React.Component<Props> {
const styles = {
markdown: {
"& > .content": {
"& > h1, h2, h3, h4, h5, h6": {
margin: "0.5rem 0",
fontSize: "0.9rem"
},
"& > h1": {
fontWeight: "700"
},
"& > h2": {
fontWeight: "600"
},
"& > h3, h4, h5, h6": {
fontWeight: "500"
},
"& strong": {
fontWeight: "500"
}
}
}
};
class MarkdownView extends React.Component<Props> {
static defaultProps = {
enableAnchorHeadings: false
};
@@ -45,16 +68,22 @@ class MarkdownView extends React.Component<Props> {
}
render() {
const {content, renderers, renderContext, enableAnchorHeadings} = this.props;
const {
content,
renderers,
renderContext,
enableAnchorHeadings,
classes
} = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
if (rendererFactory){
if (rendererFactory) {
rendererList = rendererFactory(renderContext);
}
if (!rendererList){
if (!rendererList) {
rendererList = {};
}
@@ -62,12 +91,12 @@ class MarkdownView extends React.Component<Props> {
rendererList.heading = MarkdownHeadingRenderer;
}
if (!rendererList.code){
if (!rendererList.code) {
rendererList.code = SyntaxHighlighter;
}
return (
<div ref={el => (this.contentRef = el)}>
<div className={classes.markdown} ref={el => (this.contentRef = el)}>
<Markdown
className="content"
skipHtml={true}
@@ -80,4 +109,4 @@ class MarkdownView extends React.Component<Props> {
}
}
export default withRouter(MarkdownView);
export default injectSheet(styles)(withRouter(MarkdownView));

View File

@@ -0,0 +1,60 @@
//@flow
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
};
class Tag extends React.Component<Props> {
static defaultProps = {
color: "light"
};
render() {
const {
className,
color,
icon,
label,
title,
onClick,
onRemove
} = this.props;
let showIcon = null;
if (icon) {
showIcon = (
<>
<i className={classNames("fas", `fa-${icon}`)} />
&nbsp;
</>
);
}
let showDelete = null;
if (onRemove) {
showDelete = <a className="tag is-delete" onClick={onRemove} />;
}
return (
<>
<span
className={classNames("tag", `is-${color}`, className)}
title={title}
onClick={onClick}
>
{showIcon}
{label}
</span>
{showDelete}
</>
);
}
}
export default Tag;

View File

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

View File

@@ -2,6 +2,7 @@
import * as React from "react";
import classNames from "classnames";
import { withRouter } from "react-router-dom";
import Icon from "../Icon";
export type ButtonProps = {
label?: string,
@@ -9,10 +10,12 @@ export type ButtonProps = {
disabled?: boolean,
action?: (event: Event) => void,
link?: string,
fullWidth?: boolean,
className?: string,
icon?: string,
fullWidth?: boolean,
reducedMobile?: boolean,
children?: React.Node,
// context props
classes: any
};
@@ -47,12 +50,40 @@ class Button extends React.Component<Props> {
disabled,
type,
color,
className,
icon,
fullWidth,
children,
className
reducedMobile,
children
} = this.props;
const loadingClass = loading ? "is-loading" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : "";
if (icon) {
return (
<button
type={type}
disabled={disabled}
onClick={this.onClick}
className={classNames(
"button",
"is-" + color,
loadingClass,
fullWidthClass,
reducedMobileClass,
className
)}
>
<span className="icon is-medium">
<Icon name={icon} color="inherit" />
</span>
<span>
{label} {children}
</span>
</button>
);
}
return (
<button
type={type}
@@ -69,8 +100,7 @@ class Button extends React.Component<Props> {
{label} {children}
</button>
);
};
}
}
export default withRouter(Button);

View File

@@ -14,7 +14,7 @@ class ButtonAddons extends React.Component<Props> {
const childWrapper = [];
React.Children.forEach(children, child => {
if (child) {
childWrapper.push(<p className="control">{child}</p>);
childWrapper.push(<p className="control" key={childWrapper.length}>{child}</p>);
}
});

View File

@@ -4,7 +4,7 @@ import Button, { type ButtonProps } from "./Button";
class DeleteButton extends React.Component<ButtonProps> {
render() {
return <Button color="warning" {...this.props} />;
return <Button color="warning" icon="times" {...this.props} />;
}
}

View File

@@ -13,7 +13,7 @@ class DownloadButton extends React.Component<Props> {
const { displayName, url, disabled, onClick } = this.props;
const onClickOrDefault = !!onClick ? onClick : () => {};
return (
<a className="button is-large 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>

View File

@@ -1,48 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import RemoveEntryOfTableButton from "../buttons/RemoveEntryOfTableButton";
type Props = {
members: string[],
t: string => string,
memberListChanged: (string[]) => void
};
type State = {};
class MemberNameTable extends React.Component<Props, State> {
render() {
const { t } = this.props;
return (
<div>
<table className="table is-hoverable is-fullwidth">
<tbody>
{this.props.members.map(member => {
return (
<tr key={member}>
<td key={member}>{member}</td>
<td>
<RemoveEntryOfTableButton
entryname={member}
removeEntry={this.removeEntry}
disabled={false}
label={t("remove-member-button.label")}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
removeEntry = (membername: string) => {
const newMembers = this.props.members.filter(name => name !== membername);
this.props.memberListChanged(newMembers);
};
}
export default translate("groups")(MemberNameTable);

View File

@@ -0,0 +1,37 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { DisplayedUser } from "@scm-manager/ui-types";
import TagGroup from "./TagGroup";
type Props = {
members: string[],
memberListChanged: (string[]) => void,
t: string => string
};
class MemberNameTagGroup extends React.Component<Props> {
render() {
const { members, t } = this.props;
const membersExtended = members.map(id => {
return { id, displayName: id, mail: "" };
});
return (
<TagGroup
items={membersExtended}
label={t("group.members")}
helpText={t("groupForm.help.memberHelpText")}
onRemove={this.removeEntry}
/>
);
}
removeEntry = (membersExtended: DisplayedUser[]) => {
const members = membersExtended.map(function(item) {
return item["id"];
});
this.props.memberListChanged(members);
};
}
export default translate("groups")(MemberNameTagGroup);

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import { translate } from "react-i18next";
import InputField from "./InputField";
type State = {
@@ -40,26 +40,28 @@ class PasswordConfirmation extends React.Component<Props, State> {
render() {
const { t } = this.props;
return (
<>
<InputField
label={t("password.newPassword")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={!this.state.passwordValid}
errorMessage={t("password.passwordInvalid")}
helpText={t("password.passwordHelpText")}
/>
<InputField
label={t("password.confirmPassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.confirmedPassword : ""}
validationError={this.state.passwordConfirmationFailed}
errorMessage={t("password.passwordConfirmFailed")}
helpText={t("password.passwordConfirmHelpText")}
/>
</>
<div className="columns is-multiline">
<div className="column is-half">
<InputField
label={t("password.newPassword")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={!this.state.passwordValid}
errorMessage={t("password.passwordInvalid")}
/>
</div>
<div className="column is-half">
<InputField
label={t("password.confirmPassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.confirmedPassword : ""}
validationError={this.state.passwordConfirmationFailed}
errorMessage={t("password.passwordConfirmFailed")}
/>
</div>
</div>
);
}
@@ -99,7 +101,7 @@ class PasswordConfirmation extends React.Component<Props, State> {
};
isValid = () => {
return this.state.passwordValid && !this.state.passwordConfirmationFailed
return this.state.passwordValid && !this.state.passwordConfirmationFailed;
};
propagateChange = () => {

View File

@@ -0,0 +1,66 @@
//@flow
import * as React from "react";
import injectSheet from "react-jss";
import type { DisplayedUser } from "@scm-manager/ui-types";
import { Help, Tag } from "../index";
type Props = {
items: DisplayedUser[],
label: string,
helpText?: string,
onRemove: (DisplayedUser[]) => void,
// context props
classes: Object
};
const styles = {
help: {
position: "relative"
}
};
class TagGroup extends React.Component<Props> {
render() {
const { items, label, helpText, classes } = this.props;
let help = null;
if (helpText) {
help = <Help className={classes.help} message={helpText} />;
}
return (
<div className="field is-grouped is-grouped-multiline">
{label && items ? (
<div className="control">
<strong>
{label}
{help}
{items.length > 0 ? ":" : ""}
</strong>
</div>
) : (
""
)}
{items.map((item, key) => {
return (
<div className="control" key={key}>
<div className="tags has-addons">
<Tag
color="info is-outlined"
label={item.displayName}
onRemove={() => this.removeEntry(item)}
/>
</div>
</div>
);
})}
</div>
);
}
removeEntry = item => {
const newItems = this.props.items.filter(name => name !== item);
this.props.onRemove(newItems);
};
}
export default injectSheet(styles)(TagGroup);

View File

@@ -2,7 +2,8 @@
export { default as AddEntryToTableField } from "./AddEntryToTableField.js";
export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEntryToTableField.js";
export { default as MemberNameTable } from "./MemberNameTable.js";
export { default as TagGroup } from "./TagGroup.js";
export { default as MemberNameTagGroup } from "./MemberNameTagGroup.js";
export { default as Checkbox } from "./Checkbox.js";
export { default as Radio } from "./Radio.js";
export { default as FilterInput } from "./FilterInput.js";

View File

@@ -23,6 +23,7 @@ export { default as FileSize } from "./FileSize.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help";
export { default as HelpIcon } from "./HelpIcon";
export { default as Tag } from "./Tag";
export { default as Tooltip } from "./Tooltip";
// TODO do we need this? getPageFromMatch is already exported by urls
export { getPageFromMatch } from "./urls";

View File

@@ -18,7 +18,15 @@ class Modal extends React.Component<Props> {
};
render() {
const { title, closeFunction, body, footer, active, className, headColor } = this.props;
const {
title,
closeFunction,
body,
footer,
active,
className,
headColor
} = this.props;
const isActive = active ? "is-active" : null;
@@ -31,7 +39,12 @@ class Modal extends React.Component<Props> {
<div className={classNames("modal", className, isActive)}>
<div className="modal-background" />
<div className="modal-card">
<header className={classNames("modal-card-head", `has-background-${headColor}`)}>
<header
className={classNames(
"modal-card-head",
`has-background-${headColor}`
)}
>
<p className="modal-card-title is-marginless">{title}</p>
<button
className="delete"

View File

@@ -1,10 +1,18 @@
//@flow
import React from "react";
import {Change, Diff as DiffComponent, DiffObjectProps, File, getChangeKey, Hunk} from "react-diff-view";
import {
Change,
Diff as DiffComponent,
DiffObjectProps,
File,
getChangeKey,
Hunk
} from "react-diff-view";
import injectSheets from "react-jss";
import classNames from "classnames";
import {translate} from "react-i18next";
import {Button, ButtonGroup} from "../buttons";
import { translate } from "react-i18next";
import { Button, ButtonGroup } from "../buttons";
import Tag from "../Tag";
const styles = {
panel: {
@@ -34,12 +42,35 @@ const styles = {
},
changeType: {
marginLeft: ".75rem"
},
diff: {
/* column sizing */
"& > colgroup .diff-gutter-col": {
width: "3.25rem"
},
/* prevent following content from moving down */
"& > .diff-gutter:empty:hover::after": {
fontSize: "0.7rem"
},
/* smaller font size for code */
"& .diff-line": {
fontSize: "0.75rem"
},
/* comment padding for sideBySide view */
"&.split .diff-widget-content .is-indented-line": {
paddingLeft: "3.25rem"
},
/* comment padding for combined view */
"&.unified .diff-widget-content .is-indented-line": {
paddingLeft: "6.5rem"
}
}
};
type Props = DiffObjectProps & {
file: File,
collapsible: true,
// context props
classes: any,
t: string => string
@@ -179,23 +210,21 @@ class DiffFile extends React.Component<Props, State> {
}
const color =
value === "added"
? "is-success"
? "success is-outlined"
: value === "deleted"
? "is-danger"
: "is-info";
? "danger is-outlined"
: "info is-outlined";
return (
<span
<Tag
className={classNames(
"tag",
"is-rounded",
"has-text-weight-normal",
color,
classes.changeType
)}
>
{value}
</span>
color={color}
label={value}
/>
);
};
@@ -219,15 +248,18 @@ class DiffFile extends React.Component<Props, State> {
: null;
icon = "fa fa-angle-down";
body = (
<div className="panel-block is-paddingless is-size-7">
<div className="panel-block is-paddingless">
{fileAnnotations}
<DiffComponent viewType={viewType}>
<DiffComponent
className={classNames(viewType, classes.diff)}
viewType={viewType}
>
{file.hunks.map(this.renderHunk)}
</DiffComponent>
</div>
);
}
const collapseIcon = collapsible? <i className={icon} />: null;
const collapseIcon = collapsible ? <i className={icon} /> : null;
const fileControls = fileControlFactory
? fileControlFactory(file, this.setCollapse)

View File

@@ -25,7 +25,7 @@ const styles = {
}
},
avatarFigure: {
marginTop: ".25rem",
marginTop: ".5rem",
marginRight: ".5rem"
},
avatarImage: {
@@ -35,6 +35,9 @@ const styles = {
metadata: {
marginLeft: 0
},
authorMargin: {
marginTop: "0.5rem"
},
isVcentered: {
alignSelf: "center"
},
@@ -70,15 +73,6 @@ class ChangesetRow extends React.Component<Props> {
<div className="column is-three-fifths">
<div className="columns is-gapless">
<div className="column is-four-fifths">
<h4 className="has-text-weight-bold is-ellipsis-overflow">
<ExtensionPoint
name="changeset.description"
props={{ changeset, value: description.title }}
renderAll={false}
>
{description.title}
</ExtensionPoint>
</h4>
<div className="media">
<AvatarWrapper>
<figure
@@ -90,6 +84,15 @@ class ChangesetRow extends React.Component<Props> {
</figure>
</AvatarWrapper>
<div className={classNames(classes.metadata, "media-right")}>
<h4 className="has-text-weight-bold is-ellipsis-overflow">
<ExtensionPoint
name="changeset.description"
props={{ changeset, value: description.title }}
renderAll={false}
>
{description.title}
</ExtensionPoint>
</h4>
<p className="is-hidden-touch">
<Interpolate
i18nKey="changeset.summary"
@@ -104,7 +107,7 @@ class ChangesetRow extends React.Component<Props> {
time={dateFromNow}
/>
</p>
<p className="is-size-7">
<p className={classNames("is-size-7", classes.authorMargin)}>
<ChangesetAuthor changeset={changeset} />
</p>
</div>

View File

@@ -10,7 +10,7 @@ type Props = {
class ChangesetTag extends React.Component<Props> {
render() {
const { tag } = this.props;
return <ChangesetTagBase icon={"fa-tag"} label={tag.name} />;
return <ChangesetTagBase icon="tag" label={tag.name} />;
}
}

View File

@@ -1,31 +1,19 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
const styles = {
spacing: {
marginRight: ".25rem"
}
};
import Tag from "../../Tag";
type Props = {
icon: string,
label: string,
// context props
classes: Object
label: string
};
class ChangesetTagBase extends React.Component<Props> {
render() {
const { icon, label, classes } = this.props;
const { icon, label } = this.props;
return (
<span className={classNames("tag", "is-info")}>
<span className={classNames("fa", icon, classes.spacing)} /> {label}
</span>
<Tag color="info" icon={icon} label={label} />
);
}
}
export default injectSheet(styles)(ChangesetTagBase);
export default ChangesetTagBase;

View File

@@ -1,24 +1,27 @@
//@flow
import React from "react";
import type { Tag } from "@scm-manager/ui-types";
import ChangesetTagBase from "./ChangesetTagBase";
import { translate } from "react-i18next";
import type { Tag } from "@scm-manager/ui-types";
import Tooltip from "../../Tooltip";
import ChangesetTagBase from "./ChangesetTagBase";
type Props = {
tags: Tag[],
// context props
t: (string) => string
t: string => string
};
class ChangesetTagsCollapsed extends React.Component<Props> {
render() {
const { tags, t } = this.props;
const message = tags.map((tag) => tag.name).join(", ");
const message = tags.map(tag => tag.name).join(", ");
return (
<Tooltip location="top" message={message}>
<ChangesetTagBase icon={"fa-tags"} label={ tags.length + " " + t("changeset.tags") } />
<ChangesetTagBase
icon="tags"
label={tags.length + " " + t("changeset.tags")}
/>
</Tooltip>
);
}