mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 14:05:44 +01:00
improve changeset styling
This commit is contained in:
@@ -9,9 +9,18 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Image extends React.Component<Props> {
|
class Image extends React.Component<Props> {
|
||||||
|
|
||||||
|
createImageSrc = () => {
|
||||||
|
const { src } = this.props;
|
||||||
|
if (src.startsWith("http")) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
return withContextPath(src);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { src, alt, className } = this.props;
|
const { alt, className } = this.props;
|
||||||
return <img className={className} src={withContextPath(src)} alt={alt} />;
|
return <img className={className} src={this.createImageSrc()} alt={alt} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"summary": "Changeset {{id}} committed {{time}}"
|
"summary": "Changeset {{id}} was committed {{time}}"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Author",
|
"name": "Author",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import React from "react";
|
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
|
||||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
|
||||||
import { Image } from "@scm-manager/ui-components";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
changeset: Changeset,
|
|
||||||
repository: Repository
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChangesetAvatar extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { changeset, repository } = this.props;
|
|
||||||
return (
|
|
||||||
<p className="image is-64x64">
|
|
||||||
<ExtensionPoint
|
|
||||||
name="repos.changeset.gravatar"
|
|
||||||
props={{ changeset, repository }}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChangesetAvatar;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import React from "react";
|
|
||||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import { MailLink, DateFromNow } from "@scm-manager/ui-components";
|
|
||||||
import ChangesetAvatar from "./ChangesetAvatar";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import injectSheet from "react-jss";
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
floatLeft: {
|
|
||||||
float: "left"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
changeset: Changeset,
|
|
||||||
repository: Repository,
|
|
||||||
t: string => string,
|
|
||||||
classes: any
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChangesetDetails extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { changeset, repository, t, classes } = this.props;
|
|
||||||
|
|
||||||
const mailadress = changeset.author.mail ? (
|
|
||||||
<tr>
|
|
||||||
<td>{t("author.mail")}</td>
|
|
||||||
<td>
|
|
||||||
<MailLink address={changeset.author.mail} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<table className={classNames("table", classes.floatLeft)}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>{t("changeset.id")}</td>
|
|
||||||
<td>{changeset.id}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("author.name")}</td>
|
|
||||||
<td>{changeset.author.name}</td>
|
|
||||||
</tr>
|
|
||||||
{mailadress}
|
|
||||||
<tr>
|
|
||||||
<td>{t("changeset.description")}</td>
|
|
||||||
<td>{changeset.description}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("changeset.date")}</td>
|
|
||||||
<td>
|
|
||||||
<DateFromNow date={changeset.date} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<figure className={classNames(classes.floatLeft)}>
|
|
||||||
<ChangesetAvatar changeset={changeset} repository={repository} />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectSheet(styles)(translate("changesets")(ChangesetDetails));
|
|
||||||
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { binder } from "@scm-manager/ui-extensions";
|
||||||
|
import type { Changeset } from "@scm-manager/ui-types";
|
||||||
|
import { Image } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
changeset: Changeset
|
||||||
|
};
|
||||||
|
|
||||||
|
class AvatarImage extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { changeset } = this.props;
|
||||||
|
|
||||||
|
const avatarFactory = binder.getExtension("changeset.avatar-factory");
|
||||||
|
if (avatarFactory) {
|
||||||
|
const avatar = avatarFactory(changeset);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
className="has-rounded-border"
|
||||||
|
src={avatar}
|
||||||
|
alt={changeset.author.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarImage;
|
||||||
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { binder } from "@scm-manager/ui-extensions";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.Node
|
||||||
|
};
|
||||||
|
|
||||||
|
class AvatarWrapper extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
if (binder.hasExtension("changeset.avatar-factory")) {
|
||||||
|
return <>{this.props.children}</>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarWrapper;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import React from "react";
|
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
|
||||||
import type { Changeset } from "@scm-manager/ui-types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
changeset: Changeset
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChangesetAvatar extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { changeset } = this.props;
|
|
||||||
return (
|
|
||||||
<ExtensionPoint
|
|
||||||
name="repos.changeset-table.information"
|
|
||||||
renderAll={true}
|
|
||||||
props={{ changeset }}
|
|
||||||
>
|
|
||||||
{/* extension should render something like this: */}
|
|
||||||
{/* <div className="image is-64x64"> */}
|
|
||||||
{/* <figure className="media-left"> */}
|
|
||||||
{/* <Image src="/some/image.jpg" alt="Logo" /> */}
|
|
||||||
{/* </figure> */}
|
|
||||||
{/* </div> */}
|
|
||||||
</ExtensionPoint>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChangesetAvatar;
|
|
||||||
96
scm-ui/src/repos/components/changesets/ChangesetDetails.js
Normal file
96
scm-ui/src/repos/components/changesets/ChangesetDetails.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Changeset, Repository } from "../../../../../scm-ui-components/packages/ui-types/src/index";
|
||||||
|
import { Interpolate, translate } from "react-i18next";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import ChangesetTag from "./ChangesetTag";
|
||||||
|
import ChangesetAuthor from "./ChangesetAuthor";
|
||||||
|
import { parseDescription } from "./changesets";
|
||||||
|
import { DateFromNow } from "../../../../../scm-ui-components/packages/ui-components/src/index";
|
||||||
|
import AvatarWrapper from "./AvatarWrapper";
|
||||||
|
import AvatarImage from "./AvatarImage";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import ChangesetId from "./ChangesetId";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
spacing: {
|
||||||
|
marginRight: "1em"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
changeset: Changeset,
|
||||||
|
repository: Repository,
|
||||||
|
t: string => string,
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChangesetDetails extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { changeset, repository, t, classes } = this.props;
|
||||||
|
|
||||||
|
const description = parseDescription(changeset.description);
|
||||||
|
|
||||||
|
const id = (
|
||||||
|
<ChangesetId repository={repository} changeset={changeset} link={false} />
|
||||||
|
);
|
||||||
|
const date = <DateFromNow date={changeset.date} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
<h4>{description.title}</h4>
|
||||||
|
<article className="media">
|
||||||
|
<AvatarWrapper>
|
||||||
|
<p className={classNames("image", "is-64x64", classes.spacing)}>
|
||||||
|
<AvatarImage changeset={changeset} />
|
||||||
|
</p>
|
||||||
|
</AvatarWrapper>
|
||||||
|
<div className="media-content">
|
||||||
|
<p>
|
||||||
|
<ChangesetAuthor changeset={changeset} />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Interpolate
|
||||||
|
i18nKey="changesets.changeset.summary"
|
||||||
|
id={id}
|
||||||
|
time={date}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="media-right">{this.renderTags()}</div>
|
||||||
|
</article>
|
||||||
|
<p>
|
||||||
|
{description.message.split("\n").map((item, key) => {
|
||||||
|
return (
|
||||||
|
<span key={key}>
|
||||||
|
{item}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTags = () => {
|
||||||
|
const { changeset } = this.props;
|
||||||
|
return changeset._embedded.tags || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTags = () => {
|
||||||
|
const tags = this.getTags();
|
||||||
|
if (tags.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="level-item">
|
||||||
|
{tags.map((tag: Tag) => {
|
||||||
|
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectSheet(styles)(translate("repos")(ChangesetDetails));
|
||||||
@@ -6,20 +6,42 @@ import type { Repository, Changeset } from "@scm-manager/ui-types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
changeset: Changeset
|
changeset: Changeset,
|
||||||
|
link: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ChangesetId extends React.Component<Props> {
|
export default class ChangesetId extends React.Component<Props> {
|
||||||
render() {
|
static defaultProps = {
|
||||||
const { repository, changeset } = this.props;
|
link: true
|
||||||
|
};
|
||||||
|
|
||||||
|
shortId = (changeset: Changeset) => {
|
||||||
|
return changeset.id.substr(0, 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderLink = () => {
|
||||||
|
const { changeset, repository } = this.props;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
|
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
|
||||||
changeset.id
|
changeset.id
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{changeset.id.substr(0, 7)}
|
{this.shortId(changeset)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderText = () => {
|
||||||
|
const { changeset } = this.props;
|
||||||
|
return this.shortId(changeset);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { link } = this.props;
|
||||||
|
if (link) {
|
||||||
|
return this.renderLink();
|
||||||
|
}
|
||||||
|
return this.renderText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import React from "react";
|
|||||||
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
|
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { translate, Interpolate } from "react-i18next";
|
import { translate, Interpolate } from "react-i18next";
|
||||||
import ChangesetAvatar from "./ChangesetAvatar";
|
|
||||||
import ChangesetId from "./ChangesetId";
|
import ChangesetId from "./ChangesetId";
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
import { DateFromNow } from "@scm-manager/ui-components";
|
import { DateFromNow } from "@scm-manager/ui-components";
|
||||||
import ChangesetAuthor from "./ChangesetAuthor";
|
import ChangesetAuthor from "./ChangesetAuthor";
|
||||||
import ChangesetTag from "./ChangesetTag";
|
import ChangesetTag from "./ChangesetTag";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
|
import { parseDescription } from "./changesets";
|
||||||
|
import AvatarWrapper from "./AvatarWrapper";
|
||||||
|
import AvatarImage from "./AvatarImage";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
pointer: {
|
pointer: {
|
||||||
@@ -46,14 +48,23 @@ class ChangesetRow extends React.Component<Props> {
|
|||||||
const changesetLink = this.createLink(changeset);
|
const changesetLink = this.createLink(changeset);
|
||||||
const dateFromNow = <DateFromNow date={changeset.date} />;
|
const dateFromNow = <DateFromNow date={changeset.date} />;
|
||||||
const authorLine = <ChangesetAuthor changeset={changeset} />;
|
const authorLine = <ChangesetAuthor changeset={changeset} />;
|
||||||
|
const description = parseDescription(changeset.description);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={classNames("media", classes.inner)}>
|
<article className={classNames("media", classes.inner)}>
|
||||||
<ChangesetAvatar changeset={changeset} />
|
<AvatarWrapper>
|
||||||
|
<div>
|
||||||
|
<figure className="media-left">
|
||||||
|
<p className="image is-64x64">
|
||||||
|
<AvatarImage changeset={changeset} />
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</AvatarWrapper>
|
||||||
<div className={classNames("media-content", classes.withOverflow)}>
|
<div className={classNames("media-content", classes.withOverflow)}>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<p className="is-ellipsis-overflow">
|
<p className="is-ellipsis-overflow">
|
||||||
{changeset.description}
|
<strong>{description.title}</strong>
|
||||||
<br />
|
<br />
|
||||||
<Interpolate
|
<Interpolate
|
||||||
i18nKey="changesets.changeset.summary"
|
i18nKey="changesets.changeset.summary"
|
||||||
|
|||||||
24
scm-ui/src/repos/components/changesets/changesets.js
Normal file
24
scm-ui/src/repos/components/changesets/changesets.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// @flow
|
||||||
|
export type Description = {
|
||||||
|
title: string,
|
||||||
|
message: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseDescription(description: string): Description {
|
||||||
|
const lineBreak = description.indexOf("\n");
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
if (lineBreak > 0) {
|
||||||
|
title = description.substring(0, lineBreak);
|
||||||
|
message = description.substring(lineBreak + 1);
|
||||||
|
} else {
|
||||||
|
title = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
}
|
||||||
16
scm-ui/src/repos/components/changesets/changesets.test.js
Normal file
16
scm-ui/src/repos/components/changesets/changesets.test.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import { parseDescription } from "./changesets";
|
||||||
|
|
||||||
|
describe("parseDescription tests", () => {
|
||||||
|
it("should return a description with title and message", () => {
|
||||||
|
const desc = parseDescription("Hello\nTrillian");
|
||||||
|
expect(desc.title).toBe("Hello");
|
||||||
|
expect(desc.message).toBe("Trillian");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a description with title and without message", () => {
|
||||||
|
const desc = parseDescription("Hello Trillian");
|
||||||
|
expect(desc.title).toBe("Hello Trillian");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
getFetchChangesetFailure,
|
getFetchChangesetFailure,
|
||||||
isFetchChangesetPending
|
isFetchChangesetPending
|
||||||
} from "../modules/changesets";
|
} from "../modules/changesets";
|
||||||
import ChangesetDetails from "../components/ChangesetDetails";
|
import ChangesetDetails from "../components/changesets/ChangesetDetails";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user