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

@@ -79,9 +79,11 @@ final class GitHunkParser {
++oldLineCounter;
break;
default:
if (!line.equals("\\ No newline at end of file")) {
throw new IllegalStateException("cannot handle diff line: " + line);
}
}
}
private static class AddedGitDiffLine implements DiffLine {
private final int newLineNumber;

View File

@@ -68,6 +68,17 @@ class GitHunkParserTest {
" a\n" +
"~illegal line\n";
private static final String NO_NEWLINE_DIFF = "diff --git a/.editorconfig b/.editorconfig\n" +
"index ea2a3ba..2f02f32 100644\n" +
"--- a/.editorconfig\n" +
"+++ b/.editorconfig\n" +
"@@ -10,3 +10,4 @@\n" +
" indent_style = space\n" +
" indent_size = 2\n" +
" charset = utf-8\n" +
"+added line\n" +
"\\ No newline at end of file\n";
@Test
void shouldParseHunks() {
List<Hunk> hunks = new GitHunkParser().parse(DIFF_001);
@@ -127,6 +138,27 @@ class GitHunkParserTest {
assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF));
}
@Test
void shouldIgnoreNoNewlineLine() {
List<Hunk> hunks = new GitHunkParser().parse(NO_NEWLINE_DIFF);
Hunk hunk = hunks.get(0);
Iterator<DiffLine> lines = hunk.iterator();
DiffLine line1 = lines.next();
assertThat(line1.getOldLineNumber()).hasValue(10);
assertThat(line1.getNewLineNumber()).hasValue(10);
assertThat(line1.getContent()).isEqualTo("indent_style = space");
lines.next();
lines.next();
DiffLine lastLine = lines.next();
assertThat(lastLine.getOldLineNumber()).isEmpty();
assertThat(lastLine.getNewLineNumber()).hasValue(13);
assertThat(lastLine.getContent()).isEqualTo("added line");
}
private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) {
assertThat(hunk.getOldStart()).isEqualTo(oldStart);
assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount);

View File

@@ -5,15 +5,15 @@ import { translate } from "react-i18next";
import { InputField, Checkbox } from "@scm-manager/ui-components";
type Configuration = {
"hgBinary": string,
"pythonBinary": string,
"pythonPath"?: string,
"encoding": string,
"useOptimizedBytecode": boolean,
"showRevisionInId": boolean,
"disableHookSSLValidation": boolean,
"enableHttpPostArgs": boolean,
"_links": Links
hgBinary: string,
pythonBinary: string,
pythonPath?: string,
encoding: string,
useOptimizedBytecode: boolean,
showRevisionInId: boolean,
disableHookSSLValidation: boolean,
enableHttpPostArgs: boolean,
_links: Links
};
type Props = {
@@ -23,24 +23,21 @@ type Props = {
onConfigurationChange: (Configuration, boolean) => void,
// context props
t: (string) => string
}
t: string => string
};
type State = Configuration & {
validationErrors: string[]
};
class HgConfigurationForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { ...props.initialConfiguration, validationErrors: [] };
}
updateValidationStatus = () => {
const requiredFields = [
"hgBinary", "pythonBinary", "encoding"
];
const requiredFields = ["hgBinary", "pythonBinary", "encoding"];
const validationErrors = [];
for (let field of requiredFields) {
@@ -56,20 +53,28 @@ class HgConfigurationForm extends React.Component<Props, State> {
return validationErrors.length === 0;
};
hasValidationError = (name: string) => {
return this.state.validationErrors.indexOf(name) >= 0;
};
handleChange = (value: any, name: string) => {
this.setState({
this.setState(
{
[name]: value
}, () => this.props.onConfigurationChange(this.state, this.updateValidationStatus()));
},
() =>
this.props.onConfigurationChange(
this.state,
this.updateValidationStatus()
)
);
};
inputField = (name: string) => {
const { readOnly, t } = this.props;
return <InputField
return (
<div className="column is-half">
<InputField
name={name}
label={t("scm-hg-plugin.config." + name)}
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
@@ -78,36 +83,43 @@ class HgConfigurationForm extends React.Component<Props, State> {
validationError={this.hasValidationError(name)}
errorMessage={t("scm-hg-plugin.config.required")}
disabled={readOnly}
/>;
/>
</div>
);
};
checkbox = (name: string) => {
const { readOnly, t } = this.props;
return <Checkbox
return (
<Checkbox
name={name}
label={t("scm-hg-plugin.config." + name)}
helpText={t("scm-hg-plugin.config." + name + "HelpText")}
checked={this.state[name]}
onChange={this.handleChange}
disabled={readOnly}
/>;
/>
);
};
render() {
return (
<>
<div className="columns is-multiline">
{this.inputField("hgBinary")}
{this.inputField("pythonBinary")}
{this.inputField("pythonPath")}
{this.inputField("encoding")}
<div className="column is-half">
{this.checkbox("useOptimizedBytecode")}
{this.checkbox("showRevisionInId")}
</div>
<div className="column is-half">
{this.checkbox("disableHookSSLValidation")}
{this.checkbox("enableHttpPostArgs")}
</>
</div>
</div>
);
}
}
export default translate("plugins")(HgConfigurationForm);

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,7 +1,7 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import Icon from "./Icon";
type Props = {
classes: any
@@ -16,7 +16,9 @@ const styles = {
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;
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 { 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";
import { withRouter } from "react-router-dom";
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,7 +68,13 @@ 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;
@@ -67,7 +96,7 @@ class MarkdownView extends React.Component<Props> {
}
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

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

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,8 +10,10 @@ 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
@@ -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

@@ -40,7 +40,8 @@ class PasswordConfirmation extends React.Component<Props, State> {
render() {
const { t } = this.props;
return (
<>
<div className="columns is-multiline">
<div className="column is-half">
<InputField
label={t("password.newPassword")}
type="password"
@@ -48,8 +49,9 @@ class PasswordConfirmation extends React.Component<Props, State> {
value={this.state.password ? this.state.password : ""}
validationError={!this.state.passwordValid}
errorMessage={t("password.passwordInvalid")}
helpText={t("password.passwordHelpText")}
/>
</div>
<div className="column is-half">
<InputField
label={t("password.confirmPassword")}
type="password"
@@ -57,9 +59,9 @@ class PasswordConfirmation extends React.Component<Props, State> {
value={this.state ? this.state.confirmedPassword : ""}
validationError={this.state.passwordConfirmationFailed}
errorMessage={t("password.passwordConfirmFailed")}
helpText={t("password.passwordConfirmHelpText")}
/>
</>
</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 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,9 +248,12 @@ 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>

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>
);
}

View File

@@ -1,6 +1,12 @@
//@flow
import type { Links } from "./hal";
export type DisplayedUser = {
id: string,
displayName: string,
mail: string
};
export type User = {
displayName: string,
name: string,

View File

@@ -3,7 +3,7 @@ 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 { DisplayedUser, User } from "./User";
export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";

View File

@@ -79,8 +79,6 @@
"password": {
"label": "Passwort",
"newPassword": "Neues Passwort",
"passwordHelpText": "Klartext Passwort des Benutzers.",
"passwordConfirmHelpText": "Passwort zur Bestätigen wiederholen.",
"currentPassword": "Aktuelles Passwort",
"currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.",
"confirmPassword": "Passwort wiederholen",

View File

@@ -38,15 +38,11 @@
"add-member-button": {
"label": "Mitglied hinzufügen"
},
"remove-member-button": {
"label": "Mitglied entfernen"
},
"add-member-textfield": {
"label": "Mitglied hinzufügen",
"error": "Ungültiger Name für Mitglied"
},
"add-member-autocomplete": {
"placeholder": "Benutzername eingeben",
"placeholder": "Mitglied hinzufügen",
"loading": "Suche...",
"no-options": "Kein Vorschlag für Benutzername verfügbar"
},

View File

@@ -80,8 +80,6 @@
"password": {
"label": "Password",
"newPassword": "New password",
"passwordHelpText": "Plain text password of the user",
"passwordConfirmHelpText": "Repeat the password for confirmation",
"currentPassword": "Current password",
"currentPasswordHelpText": "The password currently in use",
"confirmPassword": "Confirm password",

View File

@@ -38,15 +38,11 @@
"add-member-button": {
"label": "Add Member"
},
"remove-member-button": {
"label": "Remove Member"
},
"add-member-textfield": {
"label": "Add Member",
"error": "Invalid member name"
},
"add-member-autocomplete": {
"placeholder": "Enter Member",
"placeholder": "Add Member",
"loading": "Loading...",
"no-options": "No suggestion available"
},

View File

@@ -38,15 +38,11 @@
"add-member-button": {
"label": "Añadir miembro"
},
"remove-member-button": {
"label": "Eliminar miembro"
},
"add-member-textfield": {
"label": "Añadir miembro",
"error": "El nombre del miembro es incorrecto"
},
"add-member-autocomplete": {
"placeholder": "Introducir el nombre del miembro",
"placeholder": "Añadir miembro",
"loading": "Cargando...",
"no-options": "No hay sugerencias disponibles"
},

View File

@@ -1,8 +1,8 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import { translate } from "react-i18next";
import { Tag } from "@scm-manager/ui-components";
type Props = {
system?: boolean,
@@ -25,9 +25,11 @@ class SystemRoleTag extends React.Component<Props> {
if (system) {
return (
<span className={classNames("tag is-dark", classes.tag)}>
{t("repositoryRole.system")}
</span>
<Tag
className={classes.tag}
color="dark"
label={t("repositoryRole.system")}
/>
);
}
return null;

View File

@@ -21,7 +21,8 @@ const styles = {
marginTop: "0.5em"
},
content: {
marginLeft: "1.5em"
marginLeft: "1.5em",
minHeight: "10.5rem"
},
link: {
display: "block",

View File

@@ -110,6 +110,8 @@ class ChangeUserPassword extends React.Component<Props, State> {
return (
<form onSubmit={this.submit}>
{message}
<div className="columns">
<div className="column">
<InputField
label={t("password.currentPassword")}
type="password"
@@ -119,21 +121,31 @@ class ChangeUserPassword extends React.Component<Props, State> {
value={this.state.oldPassword ? this.state.oldPassword : ""}
helpText={t("password.currentPasswordHelpText")}
/>
</div>
</div>
<PasswordConfirmation
passwordChanged={this.passwordChanged}
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<div className="columns">
<div className="column">
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("password.submit")}
/>
</div>
</div>
</form>
);
}
passwordChanged = (password: string, passwordValid: boolean) => {
this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) });
this.setState({
...this.state,
password,
passwordValid: !!password && passwordValid
});
};
onClose = () => {

View File

@@ -4,8 +4,7 @@ import { translate } from "react-i18next";
import {
Subtitle,
AutocompleteAddEntryToTableField,
LabelWithHelpIcon,
MemberNameTable,
MemberNameTagGroup,
InputField,
SubmitButton,
Textarea,
@@ -55,10 +54,7 @@ class GroupForm extends React.Component<Props, State> {
}
isFalsy(value) {
if (!value) {
return true;
}
return false;
return !value;
}
isValid = () => {
@@ -85,11 +81,7 @@ class GroupForm extends React.Component<Props, State> {
const { loadUserSuggestions, t } = this.props;
return (
<>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("groupForm.help.memberHelpText")}
/>
<MemberNameTable
<MemberNameTagGroup
members={group.members}
memberListChanged={this.memberListChanged}
/>
@@ -97,7 +89,6 @@ class GroupForm extends React.Component<Props, State> {
addEntry={this.addMember}
disabled={false}
buttonLabel={t("add-member-button.label")}
fieldLabel={t("add-member-textfield.label")}
errorMessage={t("add-member-textfield.error")}
loadSuggestions={loadUserSuggestions}
placeholder={t("add-member-autocomplete.placeholder")}

View File

@@ -1,24 +1,29 @@
// @flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import classNames from "classnames";
import type { Link } from "@scm-manager/ui-types";
import {
Notification,
ErrorNotification,
SubmitButton
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import { getLink } from "../../modules/indexResource";
import {
loadPermissionsForEntity,
setPermissions
} from "./handlePermissions";
import PermissionCheckbox from "./PermissionCheckbox";
import { connect } from "react-redux";
import { getLink } from "../../modules/indexResource";
type Props = {
t: string => string,
availablePermissionLink: string,
selectedPermissionsLink: Link
selectedPermissionsLink: Link,
// context props
classes: any,
t: string => string
};
type State = {
@@ -30,6 +35,17 @@ type State = {
overwritePermissionsLink?: Link
};
const styles = {
permissionsWrapper: {
paddingBottom: "0",
"& .field .control": {
width: "100%",
wordWrap: "break-word"
}
}
};
class SetPermissions extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -135,17 +151,35 @@ class SetPermissions extends React.Component<Props, State> {
}
renderPermissions = () => {
const { classes } = this.props;
const { overwritePermissionsLink, permissions } = this.state;
return Object.keys(permissions).map(p => (
<div key={p}>
const permissionArray = Object.keys(permissions);
return (
<div className="columns">
<div className={classNames("column", "is-half", classes.permissionsWrapper)}>
{permissionArray.slice(0, (permissionArray.length/2)+1).map(p => (
<PermissionCheckbox
key={p}
permission={p}
checked={permissions[p]}
onChange={this.valueChanged}
disabled={!overwritePermissionsLink}
/>
))}
</div>
));
<div className={classNames("column", "is-half", classes.permissionsWrapper)}>
{permissionArray.slice((permissionArray.length/2)+1, permissionArray.length).map(p => (
<PermissionCheckbox
key={p}
permission={p}
checked={permissions[p]}
onChange={this.valueChanged}
disabled={!overwritePermissionsLink}
/>
))}
</div>
</div>
);
};
valueChanged = (value: boolean, name: string) => {
@@ -174,5 +208,4 @@ const mapStateToProps = state => {
};
export default connect(mapStateToProps)(
translate("permissions")(SetPermissions)
);
injectSheet(styles)(translate("permissions")(SetPermissions)));

View File

@@ -1,11 +1,13 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import { translate } from "react-i18next";
import { Tag } from "@scm-manager/ui-components";
type Props = {
defaultBranch?: boolean,
// context props
classes: any,
t: string => string
};
@@ -23,9 +25,11 @@ class DefaultBranchTag extends React.Component<Props> {
if (defaultBranch) {
return (
<span className={classNames("tag is-dark", classes.tag)}>
{t("branch.defaultTag")}
</span>
<Tag
className={classes.tag}
color="dark"
label={t("branch.defaultTag")}
/>
);
}
return null;

View File

@@ -105,17 +105,25 @@ class SetUserPassword extends React.Component<Props, State> {
passwordChanged={this.passwordChanged}
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<div className="columns">
<div className="column">
<SubmitButton
disabled={!this.state.passwordValid}
loading={loading}
label={t("singleUserPassword.button")}
/>
</div>
</div>
</form>
);
}
passwordChanged = (password: string, passwordValid: boolean) => {
this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) });
this.setState({
...this.state,
password,
passwordValid: !!password && passwordValid
});
};
onClose = () => {

View File

@@ -162,9 +162,9 @@ class UserForm extends React.Component<Props, State> {
/>
</div>
</div>
{passwordChangeField}
<div className="columns">
<div className="column">
{passwordChangeField}
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}

View File

@@ -1,10 +1,10 @@
@import "bulma/sass/utilities/initial-variables";
@import "bulma/sass/utilities/functions";
$turquoise: #00d1df;
$blue: #33b2e8;
$cyan: $blue;
$green: #00c79b;
$mint: #11dfd0;
.is-ellipsis-overflow {
overflow: hidden;
@@ -36,7 +36,7 @@ $mint: #11dfd0;
}
.main {
min-height: calc(100vh - 260px);
min-height: calc(100vh - 300px);
}
// shown in top section when pageactions set
@@ -96,7 +96,14 @@ $danger-75: scale-color($danger, $lightness: 25%);
$danger-50: scale-color($danger, $lightness: 50%);
$danger-25: scale-color($danger, $lightness: 75%);
/*
// not supported by ie
// css vars for external reuse
:root {
// asc sorted initial variables
--black: #{$black};
--white: #{$white};
// asc sorted derived-variables
--primary: #{$primary};
--primary-75: #{$primary-75};
@@ -131,7 +138,14 @@ $danger-25: scale-color($danger, $lightness: 75%);
--link-50: #{$link-50};
--link-25: #{$link-25};
}
*/
// readability issues with original color
.has-text-warning {
color: #ffb600 !important;
}
// border and background colors
.has-background-dark-75 {
background-color: $dark-75;
}
@@ -196,11 +210,183 @@ $danger-25: scale-color($danger, $lightness: 75%);
background-color: $danger-25;
}
.has-background-brown {
background-color: #000000b3;
.has-background-warning-invert {
background-color: $warning-invert;
}
// tags
.tag:not(body) {
border: 1px solid transparent;
background-color: $white;
&.is-delete {
background-color: $light;
}
&.is-outlined {
background-color: $white;
}
&.is-black.is-outlined {
color: $black;
border-color: $black;
}
&.is-dark.is-outlined {
color: $dark;
border-color: $dark;
}
&.is-light.is-outlined {
color: $light;
border-color: $light;
}
&.is-white.is-outlined {
color: $white;
border-color: $white;
}
&.is-primary.is-outlined {
color: $primary;
border-color: $primary;
}
&.is-link.is-outlined {
color: $link;
border-color: $link;
}
&.is-info.is-outlined {
color: $info;
border-color: $info;
}
&.is-success.is-outlined {
color: $success;
border-color: $success;
}
&.is-warning.is-outlined {
color: $warning;
border-color: $warning;
}
&.is-danger.is-outlined {
color: $danger;
border-color: $danger;
}
}
// buttons
.button {
padding-left: 1.5em;
padding-right: 1.5em;
height: 2.5rem;
font-weight: 600;
&[disabled] {
opacity: 1;
}
&.is-primary:hover,
&.is-primary.is-hovered {
background-color: scale-color($primary, $lightness: -10%);
}
&.is-primary:active,
&.is-primary.is-active {
background-color: scale-color($primary, $lightness: -20%);
}
&.is-primary[disabled] {
background-color: scale-color($primary, $lightness: 75%);
}
&.is-info:hover,
&.is-info.is-hovered {
background-color: scale-color($info, $lightness: -10%);
}
&.is-info:active,
&.is-info.is-active {
background-color: scale-color($info, $lightness: -20%);
}
&.is-info[disabled] {
background-color: scale-color($info, $lightness: 75%);
}
&.is-link:hover,
&.is-link.is-hovered {
background-color: scale-color($link, $lightness: -10%);
}
&.is-link:active,
&.is-link.is-active {
background-color: scale-color($link, $lightness: -20%);
}
&.is-link[disabled] {
background-color: scale-color($link, $lightness: 75%);
}
&.is-success:hover,
&.is-success.is-hovered {
background-color: scale-color($success, $lightness: -10%);
}
&.is-success:active,
&.is-success.is-active {
background-color: scale-color($success, $lightness: -20%);
}
&.is-success[disabled] {
background-color: scale-color($success, $lightness: 75%);
}
&.is-warning:hover,
&.is-warning.is-hovered {
background-color: scale-color($warning, $lightness: -10%);
}
&.is-warning:active,
&.is-warning.is-active {
background-color: scale-color($warning, $lightness: -20%);
}
&.is-warning[disabled] {
background-color: scale-color($warning, $lightness: 75%);
}
&.is-danger:hover,
&.is-danger.is-hovered {
background-color: scale-color($danger, $lightness: -10%);
}
&.is-danger:active,
&.is-danger.is-active {
background-color: scale-color($danger, $lightness: -20%);
}
&.is-danger[disabled] {
background-color: scale-color($danger, $lightness: 75%);
}
&.is-reduced-mobile,
&.reduced-mobile {
font-size: 0.9rem;
@media screen and (max-width: 1087px) {
> span:nth-child(2) {
display: none;
}
}
@media screen and (max-width: 1087px) and (min-width: 769px) {
// simultaneously with left margin of Bulma
> .icon:first-child:not(:last-child) {
margin: 0;
}
}
@media screen and (max-width: 768px) {
// simultaneously with left margin of Bulma
.icon:first-child:not(:last-child) {
margin-right: calc(-0.375em - 1px);
}
}
}
}
// import at the end, because we need a lot of stuff from bulma/bulma
.box-link-shadow {
&:hover,
&:focus {
@@ -235,53 +421,7 @@ ul.is-separated {
}
}
.user {
display: inline-block;
font-weight: bold;
}
// buttons
.button {
padding-left: 1.5em;
padding-right: 1.5em;
height: 2.5rem;
&.is-primary {
background-color: #00d1df;
}
&.is-primary:hover,
&.is-primary.is-hovered {
background-color: #00b9c6;
}
&.is-primary:active,
&.is-primary.is-active {
background-color: #00a1ac;
}
&.is-primary[disabled] {
background-color: #40dde7;
}
&.reduced-mobile {
@media screen and (max-width: 1087px) {
> span:nth-child(2) {
display: none;
}
}
@media screen and (max-width: 1087px) and (min-width: 769px) {
// simultaneously with left margin of Bulma
> .icon:first-child:not(:last-child) {
margin: 0;
}
}
@media screen and (max-width: 768px) {
// simultaneously with left margin of Bulma
.icon:first-child:not(:last-child) {
margin-right: calc(-0.375em - 1px);
}
}
}
}
// multiline Columns
// columns
.columns.is-multiline {
.column {
height: 120px;
@@ -416,7 +556,7 @@ ul.is-separated {
.panel-heading {
border: none;
border-bottom: 1px solid #dbdbdb;
border-bottom: 1px solid $border;
border-radius: 0.25rem 0.25rem 0 0;
> .field {
@@ -427,14 +567,6 @@ ul.is-separated {
.panel-block {
display: block;
border: none;
& .comment-wrapper:first-child div:first-child {
border-top: none;
}
& .diff-widget-content div {
border-bottom: none;
}
}
.panel-footer {
@@ -445,7 +577,7 @@ ul.is-separated {
line-height: 1.25;
padding: 0.5em 0.75em;
border: none;
border-top: 1px solid #dbdbdb;
border-top: 1px solid $border;
border-radius: 0 0 0.25rem 0.25rem;
}
}
@@ -470,10 +602,6 @@ form .field:not(.is-grouped) {
}
}
.is-icon {
color: $grey-light;
}
// label with help-icon compensation
.label-icon-spacing {
margin-top: 30px;
@@ -499,14 +627,19 @@ form .field:not(.is-grouped) {
.pagination-ellipsis {
padding-left: 1.5em;
padding-right: 1.5em;
height: 2.5rem;
height: 2.8rem;
min-width: 5rem;
}
.pagination-previous,
.pagination-next {
height: 2.8rem;
min-width: 6.75em;
}
.pagination-link.is-current {
opacity: 1;
}
// dark hero colors
// hero
.hero.is-dark {
background-color: #002e4b;
background-image: url(../images/scmManagerHero.jpg);
@@ -528,7 +661,7 @@ form .field:not(.is-grouped) {
background-color: whitesmoke;
}
// sidebar menu
// aside
.aside-background {
bottom: 0;
left: 50%;
@@ -537,6 +670,8 @@ form .field:not(.is-grouped) {
top: 0;
background-color: whitesmoke;
}
// menu
.menu {
div {
height: 100%;
@@ -544,7 +679,6 @@ form .field:not(.is-grouped) {
margin-bottom: 1rem;
}
}
.menu-label {
color: #fff;
font-size: 1em;
@@ -614,17 +748,6 @@ form .field:not(.is-grouped) {
}
}
// modal
.modal {
.modal-card-foot {
justify-content: flex-end; // pulled-right
}
}
.modal-card-body div div:last-child {
border-bottom: none;
}
.sub-menu li {
line-height: 1;
@@ -650,6 +773,17 @@ form .field:not(.is-grouped) {
}
}
// modal
.modal {
.modal-card-foot {
justify-content: flex-end; // pulled-right
}
}
.modal-card-body div div:last-child {
border-bottom: none;
}
// cursor
.has-cursor-pointer {
cursor: pointer;