This commit is contained in:
Florian Scholdei
2019-02-07 17:30:54 +01:00
258 changed files with 2966 additions and 2468 deletions

View File

@@ -13,10 +13,11 @@ const styles = {
minWidthOfLabel: {
minWidth: "4.5rem"
},
wrapper: {
padding: "1rem 1.5rem",
border: "1px solid #eee",
borderRadius: "5px 5px 0 0"
labelSizing: {
fontSize: "1rem !important"
},
noBottomMargin: {
marginBottom: "0 !important"
}
};
@@ -52,9 +53,9 @@ class BranchSelector extends React.Component<Props, State> {
return (
<div
className={classNames(
"has-background-light field",
"field",
"is-horizontal",
classes.wrapper
classes.noBottomMargin
)}
>
<div
@@ -65,10 +66,14 @@ class BranchSelector extends React.Component<Props, State> {
classes.minWidthOfLabel
)}
>
<label className="label">{label}</label>
<label className={classNames("label", classes.labelSizing)}>
{label}
</label>
</div>
<div className="field-body">
<div className="field is-narrow">
<div
className={classNames("field is-narrow", classes.noBottomMargin)}
>
<div className="control">
<DropDown
className="is-fullwidth"

View File

@@ -2,31 +2,40 @@
import React from "react";
import moment from "moment";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
const styles = {
date: {
borderBottom: "1px dotted rgba(219, 219, 219)",
cursor: "help"
}
};
type Props = {
date?: string,
// context props
classes: any,
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 { i18n, date, classes } = this.props;
const fromNow = DateFromNow.format(i18n.language, date);
return <span>{fromNow}</span>;
if (date) {
const dateWithLocale = moment(date).locale(i18n.language);
return (
<time title={dateWithLocale.format()} className={classes.date}>
{dateWithLocale.fromNow()}
</time>
);
}
return null;
}
}
export default translate()(DateFromNow);
export default injectSheet(styles)(translate()(DateFromNow));

View File

@@ -4,21 +4,27 @@ import classNames from "classnames";
type Props = {
message: string,
className: string,
className?: string,
location: string,
children: React.Node
};
class Tooltip extends React.Component<Props> {
static defaultProps = {
location: "right"
};
render() {
const { className, message, children } = this.props;
const { className, message, location, children } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip", "is-tooltip-right", multiline, className)}
<span
className={classNames("tooltip", "is-tooltip-" + location, multiline, className)}
data-tooltip={message}
>
{children}
</div>
</span>
);
}
}

View File

@@ -1,16 +1,17 @@
//@flow
import React from "react";
import * as React from "react";
import classNames from "classnames";
import { withRouter } from "react-router-dom";
export type ButtonProps = {
label: string,
label?: string,
loading?: boolean,
disabled?: boolean,
action?: (event: Event) => void,
link?: string,
fullWidth?: boolean,
className?: string,
children?: React.Node,
classes: any
};
@@ -45,6 +46,7 @@ class Button extends React.Component<Props> {
type,
color,
fullWidth,
children,
className
} = this.props;
const loadingClass = loading ? "is-loading" : "";
@@ -62,7 +64,7 @@ class Button extends React.Component<Props> {
className
)}
>
{label}
{label} {children}
</button>
);
};

View File

@@ -1,41 +1,30 @@
// @flow
import React from "react";
import Button from "./Button";
import * as React from "react";
type Props = {
firstlabel: string,
secondlabel: string,
firstAction?: (event: Event) => void,
secondAction?: (event: Event) => void,
firstIsSelected: boolean
addons?: boolean,
className?: string,
children: React.Node
};
class ButtonGroup extends React.Component<Props> {
static defaultProps = {
addons: true
};
render() {
const { firstlabel, secondlabel, firstAction, secondAction, firstIsSelected } = this.props;
let showFirstColor = "";
let showSecondColor = "";
if (firstIsSelected) {
showFirstColor += "link is-selected";
} else {
showSecondColor += "link is-selected";
const { addons, className, children } = this.props;
let styleClasses = "buttons";
if (addons) {
styleClasses += " has-addons";
}
if (className) {
styleClasses += " " + className;
}
return (
<div className="buttons has-addons">
<Button
label={firstlabel}
color={showFirstColor}
action={firstAction}
/>
<Button
label={secondlabel}
color={showSecondColor}
action={secondAction}
/>
<div className={styleClasses}>
{ children }
</div>
);
}

View File

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

View File

@@ -72,6 +72,33 @@ class ConfigurationBinder {
binder.bind("repository.route", RepoRoute, repoPredicate);
}
bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + "/settings" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.subnavigation", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + "/settings" + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -10,7 +10,8 @@ type Props = {
buttonLabel: string,
fieldLabel: string,
errorMessage: string,
helpText?: string
helpText?: string,
validateEntry?: string => boolean
};
type State = {
@@ -25,6 +26,15 @@ class AddEntryToTableField extends React.Component<Props, State> {
};
}
isValid = () => {
const {validateEntry} = this.props;
if (!this.state.entryToAdd || this.state.entryToAdd === "" || !validateEntry) {
return true;
} else {
return validateEntry(this.state.entryToAdd);
}
};
render() {
const {
disabled,
@@ -39,7 +49,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
label={fieldLabel}
errorMessage={errorMessage}
onChange={this.handleAddEntryChange}
validationError={false}
validationError={!this.isValid()}
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
@@ -48,7 +58,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
<AddButton
label={buttonLabel}
action={this.addButtonClicked}
disabled={disabled || this.state.entryToAdd ===""}
disabled={disabled || this.state.entryToAdd ==="" || !this.isValid()}
/>
</div>
);

View File

@@ -1,102 +0,0 @@
/*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

@@ -1,9 +1,7 @@
// @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";
import ReactDOM from "react-dom";
import Modal from "./Modal";
type Button = {
label: string,
@@ -25,59 +23,47 @@ class ConfirmAlert extends React.Component<Props> {
};
close = () => {
removeElementReconfirm();
ReactDOM.unmountComponentAtNode(document.getElementById("modalRoot"));
};
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)}
href="javascript:void(0);"
>
{button.label}
</button>
))}
</div>
</div>
}
</div>
const body = <>{message}</>;
const footer = (
<div className="field is-grouped">
{buttons.map((button, i) => (
<p className="control">
<a
className="button is-info"
key={i}
onClick={() => this.handleClickButton(button)}
>
{button.label}
</a>
</p>
))}
</div>
);
return (
<Modal
title={title}
closeFunction={() => this.close()}
body={body}
active={true}
footer={footer}
/>
);
}
}
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);
const root = document.getElementById("modalRoot");
if (root) {
ReactDOM.render(<ConfirmAlert {...properties} />, root);
}
}
export default ConfirmAlert;

View File

@@ -0,0 +1,54 @@
// @flow
import * as React from "react";
import classNames from "classnames";
import injectSheet from "react-jss";
type Props = {
title: string,
closeFunction: () => void,
body: any,
footer?: any,
active: boolean,
classes: any
};
const styles = {
resize: {
maxWidth: "100%",
width: "auto !important",
display: "inline-block"
}
};
class Modal extends React.Component<Props> {
render() {
const { title, closeFunction, body, footer, active, classes } = this.props;
const isActive = active ? "is-active" : null;
let showFooter = null;
if (footer) {
showFooter = <footer className="modal-card-foot">{footer}</footer>;
}
return (
<div className={classNames("modal", isActive)}>
<div className="modal-background" />
<div className={classNames("modal-card", classes.resize)}>
<header className="modal-card-head">
<p className="modal-card-title">{title}</p>
<button
className="delete"
aria-label="close"
onClick={closeFunction}
/>
</header>
<section className="modal-card-body">{body}</section>
{showFooter}
</div>
</div>
);
}
}
export default injectSheet(styles)(Modal);

View File

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

View File

@@ -28,7 +28,7 @@ class NavLink extends React.Component<Props> {
let showIcon = null;
if (icon) {
showIcon = (<><i className={icon}></i>{" "}</>);
showIcon = (<><i className={icon} />{" "}</>);
}
return (

View File

@@ -50,8 +50,19 @@ class PrimaryNavigation extends React.Component<Props> {
createNavigationItems = () => {
const navigationItems = [];
const { t, links } = this.props;
const props = {
links,
label: t("primary-navigation.first-menu")
};
const append = this.createNavigationAppender(navigationItems);
if (binder.hasExtension("primary-navigation.first-menu", props)) {
navigationItems.push(
<ExtensionPoint name="primary-navigation.first-menu" props={props} />
);
}
append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories");
append("/users", "/(user|users)", "primary-navigation.users", "users");
append("/groups", "/(group|groups)", "primary-navigation.groups", "groups");

View File

@@ -0,0 +1,65 @@
//@flow
import * as React from "react";
import { Link, Route } from "react-router-dom";
type Props = {
to: string,
icon?: string,
label: string,
activeOnlyWhenExact?: boolean,
activeWhenMatch?: (route: any) => boolean,
children?: React.Node
};
class SubNavigation extends React.Component<Props> {
static defaultProps = {
activeOnlyWhenExact: false
};
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => {
const { to, icon, label } = this.props;
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
}
let children = null;
if (this.isActive(route)) {
children = <ul className="sub-menu">{this.props.children}</ul>;
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={defaultIcon} /> {label}
</Link>
{children}
</li>
);
};
render() {
const { to, activeOnlyWhenExact } = this.props;
// removes last part of url
let parents = to.split("/");
parents.splice(-1, 1);
let parent = parents.join("/");
return (
<Route
path={parent}
exact={activeOnlyWhenExact}
children={this.renderLink}
/>
);
}
}
export default SubNavigation;

View File

@@ -3,6 +3,7 @@
export { default as NavAction } from "./NavAction.js";
export { default as NavLink } from "./NavLink.js";
export { default as Navigation } from "./Navigation.js";
export { default as SubNavigation } from "./SubNavigation.js";
export { default as PrimaryNavigation } from "./PrimaryNavigation.js";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js";
export { default as Section } from "./Section.js";

View File

@@ -1,9 +1,14 @@
//@flow
import React from "react";
import type {Changeset} from "@scm-manager/ui-types";
import type { Changeset } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import {translate} from "react-i18next";
type Props = {
changeset: Changeset
changeset: Changeset,
// context props
t: (string) => string
};
class ChangesetAuthor extends React.Component<Props> {
@@ -13,26 +18,35 @@ class ChangesetAuthor extends React.Component<Props> {
return null;
}
const { name } = changeset.author;
const { name, mail } = changeset.author;
if (mail) {
return this.withExtensionPoint(this.renderWithMail(name, mail));
}
return this.withExtensionPoint(<>{name}</>);
}
renderWithMail(name: string, mail: string) {
const { t } = this.props;
return (
<>
{name} {this.renderMail()}
</>
<a href={"mailto: " + mail} title={t("changeset.author.mailto") + " " + mail}>
{name}
</a>
);
}
renderMail() {
const { mail } = this.props.changeset.author;
if (mail) {
return (
<a className="is-hidden-mobile" href={"mailto:" + mail}>
&lt;
{mail}
&gt;
</a>
);
}
withExtensionPoint(child: any) {
const { t } = this.props;
return (
<>
{t("changeset.author.prefix")} {child}
<ExtensionPoint
name="changesets.author.suffix"
props={{ changeset: this.props.changeset }}
renderAll={true}
/>
</>
);
}
}
export default ChangesetAuthor;
export default translate("repos")(ChangesetAuthor);

View File

@@ -0,0 +1,49 @@
//@flow
import React from "react";
import type { Changeset, Repository } from "@scm-manager/ui-types";
import ButtonGroup from "../../buttons/ButtonGroup";
import Button from "../../buttons/Button";
import { createChangesetLink, createSourcesLink } from "./changesets";
import { translate } from "react-i18next";
type Props = {
repository: Repository,
changeset: Changeset,
// context props
t: (string) => string
}
class ChangesetButtonGroup extends React.Component<Props> {
render() {
const { repository, changeset, t } = this.props;
const changesetLink = createChangesetLink(repository, changeset);
const sourcesLink = createSourcesLink(repository, changeset);
return (
<ButtonGroup className="is-pulled-right">
<Button link={changesetLink}>
<span className="icon">
<i className="fas fa-code-branch"></i>
</span>
<span className="is-hidden-mobile is-hidden-tablet-only">
{t("changeset.buttons.details")}
</span>
</Button>
<Button link={sourcesLink}>
<span className="icon">
<i className="fas fa-code"></i>
</span>
<span className="is-hidden-mobile is-hidden-tablet-only">
{t("changeset.buttons.sources")}
</span>
</Button>
</ButtonGroup>
);
}
}
export default translate("repos")(ChangesetButtonGroup);

View File

@@ -25,7 +25,7 @@ class ChangesetDiff extends React.Component<Props> {
render() {
const { changeset, t } = this.props;
if (!this.isDiffSupported(changeset)) {
return <Notification type="danger">{t("changesets.diff.not-supported")}</Notification>;
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
} else {
const url = this.createUrl(changeset);
return <LoadingDiff url={url} />;

View File

@@ -3,6 +3,7 @@
import {Link} from "react-router-dom";
import React from "react";
import type {Changeset, Repository} from "@scm-manager/ui-types";
import { createChangesetLink } from "./changesets";
type Props = {
repository: Repository,
@@ -20,13 +21,11 @@ export default class ChangesetId extends React.Component<Props> {
};
renderLink = () => {
const { changeset, repository } = this.props;
const { repository, changeset } = this.props;
const link = createChangesetLink(repository, changeset);
return (
<Link
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
changeset.id
}`}
>
<Link to={link}>
{this.shortId(changeset)}
</Link>
);

View File

@@ -21,7 +21,7 @@ class ChangesetList extends React.Component<Props> {
/>
);
});
return <div className="box">{content}</div>;
return <>{content}</>;
}
}

View File

@@ -8,21 +8,39 @@ import ChangesetId from "./ChangesetId";
import injectSheet from "react-jss";
import { DateFromNow } from "../..";
import ChangesetAuthor from "./ChangesetAuthor";
import ChangesetTag from "./ChangesetTag";
import { parseDescription } from "./changesets";
import { AvatarWrapper, AvatarImage } from "../../avatar";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import ChangesetTags from "./ChangesetTags";
import ChangesetButtonGroup from "./ChangesetButtonGroup";
const styles = {
pointer: {
cursor: "pointer"
changeset: {
// & references parent rule
// have a look at https://cssinjs.org/jss-plugin-nested?v=v10.0.0-alpha.9
"& + &": {
borderTop: "1px solid rgba(219, 219, 219, 0.5)",
marginTop: "1rem",
paddingTop: "1rem"
}
},
changesetGroup: {
marginBottom: "1em"
avatarFigure: {
marginTop: ".25rem",
marginRight: ".5rem",
},
withOverflow: {
overflow: "auto"
avatarImage: {
height: "35px",
width: "35px"
},
isVcentered: {
marginTop: "auto",
marginBottom: "auto"
},
metadata: {
marginLeft: 0
},
tag: {
marginTop: ".5rem"
}
};
@@ -34,74 +52,70 @@ type Props = {
};
class ChangesetRow extends React.Component<Props> {
createLink = (changeset: Changeset) => {
createChangesetId = (changeset: Changeset) => {
const { repository } = this.props;
return <ChangesetId changeset={changeset} repository={repository} />;
};
getTags = () => {
const { changeset } = this.props;
return changeset._embedded.tags || [];
};
render() {
const { changeset, classes } = this.props;
const changesetLink = this.createLink(changeset);
const dateFromNow = <DateFromNow date={changeset.date} />;
const authorLine = <ChangesetAuthor changeset={changeset} />;
const { repository, changeset, classes } = this.props;
const description = parseDescription(changeset.description);
const changesetId = this.createChangesetId(changeset);
const dateFromNow = <DateFromNow date={changeset.date} />;
return (
<article className={classNames("media", classes.inner)}>
<AvatarWrapper>
<div>
<figure className="media-left">
<p className="image is-64x64">
<AvatarImage person={changeset.author} />
</p>
</figure>
<div className={classes.changeset}>
<div className="columns">
<div className="column is-three-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 className={classNames(classes.avatarFigure, "media-left")}>
<div className={classNames("image", classes.avatarImage)}>
<AvatarImage person={changeset.author} />
</div>
</figure>
</AvatarWrapper>
<div className={classNames(classes.metadata, "media-right")}>
<p className="is-hidden-mobile is-hidden-tablet-only">
<Interpolate
i18nKey="changeset.summary"
id={changesetId}
time={dateFromNow}
/>
</p>
<p className="is-hidden-desktop">
<Interpolate
i18nKey="changeset.shortSummary"
id={changesetId}
time={dateFromNow}
/>
</p>
<p className="is-size-7">
<ChangesetAuthor changeset={changeset} />
</p>
</div>
</div>
</div>
</AvatarWrapper>
<div className={classNames("media-content", classes.withOverflow)}>
<div className="content">
<p className="is-ellipsis-overflow">
<strong>
<ExtensionPoint
name="changesets.changeset.description"
props={{ changeset, value: description.title }}
renderAll={false}
>
{description.title}
</ExtensionPoint>
</strong>
<br />
<Interpolate
i18nKey="changesets.changeset.summary"
id={changesetLink}
time={dateFromNow}
/>
</p>{" "}
<div className="is-size-7">{authorLine}</div>
<div className={classNames("column", classes.isVcentered)}>
<ChangesetTags changeset={changeset} />
<ChangesetButtonGroup repository={repository} changeset={changeset} />
</div>
</div>
{this.renderTags()}
</article>
</div>
);
}
renderTags = () => {
const tags = this.getTags();
if (tags.length > 0) {
return (
<div className="media-right">
{tags.map((tag: Tag) => {
return <ChangesetTag key={tag.name} tag={tag} />;
})}
</div>
);
}
return null;
};
}
export default injectSheet(styles)(translate("repos")(ChangesetRow));

View File

@@ -1,32 +1,17 @@
//@flow
import React from "react";
import type { Tag } from "@scm-manager/ui-types";
import injectSheet from "react-jss";
import classNames from "classnames";
const styles = {
spacing: {
marginRight: "4px"
}
};
import ChangesetTagBase from "./ChangesetTagBase";
type Props = {
tag: Tag,
// context props
classes: Object
tag: Tag
};
class ChangesetTag extends React.Component<Props> {
render() {
const { tag, classes } = this.props;
return (
<span className="tag is-info">
<span className={classNames("fa", "fa-tag", classes.spacing)} />{" "}
{tag.name}
</span>
);
const { tag } = this.props;
return <ChangesetTagBase icon={"fa-tag"} label={tag.name} />;
}
}
export default injectSheet(styles)(ChangesetTag);
export default ChangesetTag;

View File

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

View File

@@ -0,0 +1,31 @@
//@flow
import React from "react";
import type { Changeset} from "@scm-manager/ui-types";
import ChangesetTag from "./ChangesetTag";
import ChangesetTagsCollapsed from "./ChangesetTagsCollapsed";
type Props = {
changeset: Changeset
};
class ChangesetTags extends React.Component<Props> {
getTags = () => {
const { changeset } = this.props;
return changeset._embedded.tags || [];
};
render() {
const tags = this.getTags();
if (tags.length === 1) {
return <ChangesetTag tag={tags[0]} />;
} else if (tags.length > 1) {
return <ChangesetTagsCollapsed tags={tags} />;
} else {
return null;
}
}
}
export default ChangesetTags;

View File

@@ -0,0 +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 Tooltip from "../../Tooltip";
type Props = {
tags: Tag[],
// context props
t: (string) => string
};
class ChangesetTagsCollapsed extends React.Component<Props> {
render() {
const { tags, t } = this.props;
const message = tags.map((tag) => tag.name).join(", ");
return (
<Tooltip location="top" message={message}>
<ChangesetTagBase icon={"fa-tags"} label={ tags.length + " " + t("changeset.tags") } />
</Tooltip>
);
}
}
export default translate("repos")(ChangesetTagsCollapsed);

View File

@@ -1,9 +1,19 @@
// @flow
import type { Changeset, Repository } from "@scm-manager/ui-types";
export type Description = {
title: string,
message: string
};
export function createChangesetLink(repository: Repository, changeset: Changeset) {
return `/repo/${repository.namespace}/${repository.name}/changeset/${changeset.id}`;
}
export function createSourcesLink(repository: Repository, changeset: Changeset) {
return `/repo/${repository.namespace}/${repository.name}/sources/${changeset.id}`;
}
export function parseDescription(description?: string): Description {
const desc = description ? description : "";
const lineBreak = desc.indexOf("\n");

View File

@@ -3,8 +3,11 @@ import * as changesets from "./changesets";
export { changesets };
export { default as ChangesetAuthor } from "./ChangesetAuthor";
export { default as ChangesetButtonGroup } from "./ChangesetButtonGroup";
export { default as ChangesetDiff } from "./ChangesetDiff";
export { default as ChangesetId } from "./ChangesetId";
export { default as ChangesetList } from "./ChangesetList";
export { default as ChangesetRow } from "./ChangesetRow";
export { default as ChangesetTag } from "./ChangesetTag";
export { default as ChangesetDiff } from "./ChangesetDiff";
export { default as ChangesetTags } from "./ChangesetTags";
export { default as ChangesetTagsCollapsed } from "./ChangesetTagsCollapsed";