use same components for plugin and repository overview

Created CardColumn and CardColumnGroup which encapsulate the layout for the two column card layout and use them for repository and plugin overview.
This commit is contained in:
Sebastian Sdorra
2019-07-09 13:29:25 +02:00
parent 9b1867862f
commit f2fb17d9b5
8 changed files with 288 additions and 314 deletions

View File

@@ -0,0 +1,87 @@
//@flow
import * as React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import { Link } from "react-router-dom";
const styles = {
inner: {
position: "relative",
pointerEvents: "none",
zIndex: 1
},
innerLink: {
pointerEvents: "all"
},
centerImage: {
marginTop: "0.8em",
marginLeft: "1em !important"
},
flexFullHeight: {
display: "flex",
flexDirection: "column",
alignSelf: "stretch"
},
content: {
display: "flex",
flexGrow: 1
},
footer: {
display: "flex",
marginTop: "auto",
paddingBottom: "1.5rem"
}
};
type Props = {
title: string,
description: string,
avatar: React.Node,
footerLeft: React.Node,
footerRight: React.Node,
link: string,
// context props
classes: any
};
class CardColumn extends React.Component<Props> {
createLink = () => {
const { link } = this.props;
if (link) {
return <Link className="overlay-column" to={link} />;
}
return null;
};
render() {
const { avatar, title, description, footerLeft, footerRight, classes } = this.props;
const link = this.createLink();
return (
<>
{link}
<article className={classNames("media", classes.inner)}>
<figure className={classNames(classes.centerImage, "media-left")}>
{avatar}
</figure>
<div className={classNames("media-content", "text-box", classes.flexFullHeight)}>
<div className={classes.content}>
<div className="content shorten-text">
<p className="is-marginless">
<strong>{title}</strong>
</p>
<p className="shorten-text">{description}</p>
</div>
</div>
<div className={classNames(classes.footer, "level")}>
<div className="level-left is-hidden-mobile">{footerLeft}</div>
<div className="level-right is-mobile">{footerRight}</div>
</div>
</div>
</article>
</>
);
}
}
export default injectSheet(styles)(CardColumn);

View File

@@ -0,0 +1,103 @@
//@flow
import * as React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
const styles = {
pointer: {
cursor: "pointer",
fontSize: "1.5rem"
},
repoGroup: {
marginBottom: "1em"
},
wrapper: {
padding: "0 0.75rem"
},
clearfix: {
clear: "both"
}
};
type Props = {
name: string,
elements: React.Node[],
// context props
classes: any
};
type State = {
collapsed: boolean
};
class CardColumnGroup extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: false
};
}
toggleCollapse = () => {
this.setState(prevState => ({
collapsed: !prevState.collapsed
}));
};
isLastEntry = (array: React.Node[], index: number) => {
return index === array.length - 1;
};
isLengthOdd = (array: React.Node[]) => {
return array.length % 2 !== 0;
};
isFullSize = (array: React.Node[], index: number) => {
return this.isLastEntry(array, index) && this.isLengthOdd(array);
};
render() {
const { name, elements, classes } = this.props;
const { collapsed } = this.state;
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
let content = null;
if (!collapsed) {
content = elements.map((entry, index) => {
const fullColumnWidth = this.isFullSize(elements, index);
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
return (
<div
className={classNames(
"box",
"box-link-shadow",
"column",
"is-clipped",
sizeClass
)}
key={index}
>
{entry}
</div>
);
});
}
return (
<div className={classes.repoGroup}>
<h2>
<span className={classes.pointer} onClick={this.toggleCollapse}>
<i className={classNames("fa", icon)} /> {name}
</span>
</h2>
<hr />
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
{content}
</div>
<div className={classes.clearfix} />
</div>
);
}
}
export default injectSheet(styles)(CardColumnGroup);

View File

@@ -32,6 +32,8 @@ export { default as MarkdownView } from "./MarkdownView";
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions.js";
export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn";
export { apiClient } from "./apiclient.js";
export * from "./errors";

View File

@@ -1,86 +1,44 @@
//@flow
import React from "react";
import { Link } from "react-router-dom";
import injectSheet from "react-jss";
import classNames from "classnames";
import type { Plugin } from "@scm-manager/ui-types";
import { CardColumn } from "@scm-manager/ui-components";
import PluginAvatar from "./PluginAvatar";
const styles = {
inner: {
position: "relative",
pointerEvents: "none",
zIndex: 1
},
centerImage: {
marginTop: "0.8em",
marginLeft: "1em !important"
},
marginBottom: {
marginBottom: "0.75rem !important"
}
};
type Props = {
plugin: Plugin,
fullColumnWidth?: boolean,
// context props
classes: any
plugin: Plugin
};
class PluginEntry extends React.Component<Props> {
createAvatar = (plugin: Plugin) => {
return <PluginAvatar plugin={plugin} />;
};
createFooterLeft = (plugin: Plugin) => {
return <small className="level-item">{plugin.author}</small>;
};
createFooterRight = (plugin: Plugin) => {
return <p className="level-item">{plugin.version}</p>;
};
render() {
const { plugin, classes, fullColumnWidth } = this.props;
const halfColumn = fullColumnWidth ? "is-full" : "is-half";
const overlayLinkClass = fullColumnWidth
? "overlay-full-column"
: "overlay-half-column";
const { plugin } = this.props;
const avatar = this.createAvatar(plugin);
const footerLeft = this.createFooterLeft(plugin);
const footerRight = this.createFooterRight(plugin);
// TODO: Add link to plugin page below
return (
<div
className={classNames(
"box",
"box-link-shadow",
"column",
"is-clipped",
halfColumn
)}
>
<Link
className={classNames(overlayLinkClass, "is-plugin-page")}
to="#"
<CardColumn
link="#"
avatar={avatar}
title={plugin.name}
description={plugin.description}
footerLeft={footerLeft}
footerRight={footerRight}
/>
<article className={classNames("media", classes.inner)}>
<figure className={classNames(classes.centerImage, "media-left")}>
<PluginAvatar plugin={plugin} />
</figure>
<div className={classNames("media-content", "text-box")}>
<div className="content">
<nav
className={classNames(
"level",
"is-mobile",
classes.marginBottom
)}
>
<div className="level-left">
<strong>{plugin.name}</strong>
</div>
<div className="level-right is-hidden-mobile">
{plugin.version}
</div>
</nav>
<p className="shorten-text is-marginless">{plugin.description}</p>
<p>
<small>{plugin.author}</small>
</p>
</div>
</div>
</article>
</div>
);
}
}
export default injectSheet(styles)(PluginEntry);
export default PluginEntry;

View File

@@ -1,96 +1,21 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import type { PluginGroup, Plugin } from "@scm-manager/ui-types";
import { CardColumnGroup } from "@scm-manager/ui-components";
import type { PluginGroup } from "@scm-manager/ui-types";
import PluginEntry from "./PluginEntry";
const styles = {
pointer: {
cursor: "pointer",
fontSize: "1.5rem"
},
pluginGroup: {
marginBottom: "1em"
},
wrapper: {
padding: "0 0.75rem"
},
clearfix: {
clear: "both"
}
};
type Props = {
group: PluginGroup,
// context props
classes: any
group: PluginGroup
};
type State = {
collapsed: boolean
};
class PluginGroupEntry extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: false
};
}
toggleCollapse = () => {
this.setState(prevState => ({
collapsed: !prevState.collapsed
}));
};
isLastEntry = (array: Plugin[], index: number) => {
return index === array.length - 1;
};
isLengthOdd = (array: Plugin[]) => {
return array.length % 2 !== 0;
};
isFullSize = (array: Plugin[], index: number) => {
return this.isLastEntry(array, index) && this.isLengthOdd(array);
};
class PluginGroupEntry extends React.Component<Props> {
render() {
const { group, classes } = this.props;
const { collapsed } = this.state;
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
let content = null;
if (!collapsed) {
content = group.plugins.map((plugin, index) => {
const fullColumnWidth = this.isFullSize(group.plugins, index);
return (
<PluginEntry
plugin={plugin}
fullColumnWidth={fullColumnWidth}
key={index}
/>
);
const { group } = this.props;
const entries = group.plugins.map((plugin, index) => {
return <PluginEntry plugin={plugin} key={index} />;
});
}
return (
<div className={classes.pluginGroup}>
<h2>
<span className={classes.pointer} onClick={this.toggleCollapse}>
<i className={classNames("fa", icon)} /> {group.name}
</span>
</h2>
<hr />
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
{content}
</div>
<div className={classes.clearfix} />
</div>
);
return <CardColumnGroup name={group.name} elements={entries} />;
}
}
export default injectSheet(styles)(PluginGroupEntry);
export default PluginGroupEntry;

View File

@@ -1,33 +1,12 @@
//@flow
import React from "react";
import { Link } from "react-router-dom";
import injectSheet from "react-jss";
import type { Repository } from "@scm-manager/ui-types";
import { DateFromNow } from "@scm-manager/ui-components";
import { CardColumn, DateFromNow } from "@scm-manager/ui-components";
import RepositoryEntryLink from "./RepositoryEntryLink";
import classNames from "classnames";
import RepositoryAvatar from "./RepositoryAvatar";
const styles = {
inner: {
position: "relative",
pointerEvents: "none",
zIndex: 1
},
innerLink: {
pointerEvents: "all"
},
centerImage: {
marginTop: "0.8em",
marginLeft: "1em !important"
}
};
type Props = {
repository: Repository,
fullColumnWidth?: boolean,
// context props
classes: any
repository: Repository
};
class RepositoryEntry extends React.Component<Props> {
@@ -83,53 +62,41 @@ class RepositoryEntry extends React.Component<Props> {
return null;
};
render() {
const { repository, classes, fullColumnWidth } = this.props;
const repositoryLink = this.createLink(repository);
const halfColumn = fullColumnWidth ? "is-full" : "is-half";
const overlayLinkClass = fullColumnWidth
? "overlay-full-column"
: "overlay-half-column";
createFooterLeft = (repository: Repository, repositoryLink: string) => {
return (
<div
className={classNames(
"box",
"box-link-shadow",
"column",
"is-clipped",
halfColumn
)}
>
<Link className={classNames(overlayLinkClass)} to={repositoryLink} />
<article className={classNames("media", classes.inner)}>
<figure className={classNames(classes.centerImage, "media-left")}>
<RepositoryAvatar repository={repository} />
</figure>
<div className={classNames("media-content", "text-box")}>
<div className="content">
<p className="is-marginless">
<strong>{repository.name}</strong>
</p>
<p className="shorten-text">{repository.description}</p>
</div>
<nav className="level is-mobile">
<div className="level-left">
<>
{this.renderBranchesLink(repository, repositoryLink)}
{this.renderChangesetsLink(repository, repositoryLink)}
{this.renderSourcesLink(repository, repositoryLink)}
{this.renderModifyLink(repository, repositoryLink)}
</div>
<div className="level-right is-hidden-mobile">
</>
);
};
createFooterRight = (repository: Repository) => {
return (
<small className="level-item">
<DateFromNow date={repository.creationDate} />
</small>
</div>
</nav>
</div>
</article>
</div>
);
};
render() {
const { repository } = this.props;
const repositoryLink = this.createLink(repository);
const footerLeft = this.createFooterLeft(repository, repositoryLink);
const footerRight = this.createFooterRight(repository);
return (
<CardColumn
avatar={<RepositoryAvatar repository={repository} />}
title={repository.name}
description={repository.description}
link={repositoryLink}
footerLeft={footerLeft}
footerRight={footerRight}
/>
);
}
}
export default injectSheet(styles)(RepositoryEntry);
export default RepositoryEntry;

View File

@@ -1,92 +1,21 @@
//@flow
import React from "react";
import type { RepositoryGroup, Repository } from "@scm-manager/ui-types";
import injectSheet from "react-jss";
import classNames from "classnames";
import { CardColumnGroup } from "@scm-manager/ui-components";
import type { RepositoryGroup } from "@scm-manager/ui-types";
import RepositoryEntry from "./RepositoryEntry";
const styles = {
pointer: {
cursor: "pointer",
fontSize: "1.5rem"
},
repoGroup: {
marginBottom: "1em"
},
wrapper: {
padding: "0 0.75rem"
},
clearfix: {
clear: "both"
}
};
type Props = {
group: RepositoryGroup,
// context props
classes: any
group: RepositoryGroup
};
type State = {
collapsed: boolean
};
class RepositoryGroupEntry extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: false
};
}
toggleCollapse = () => {
this.setState(prevState => ({
collapsed: !prevState.collapsed
}));
};
isLastEntry = (array: Repository[], index: number) => {
return index === array.length - 1;
};
isLengthOdd = (array: Repository[]) => {
return array.length % 2 !== 0;
};
isFullSize = (array: Repository[], index: number) => {
return this.isLastEntry(array, index) && this.isLengthOdd(array);
};
class RepositoryGroupEntry extends React.Component<Props> {
render() {
const { group, classes } = this.props;
const { collapsed } = this.state;
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
let content = null;
if (!collapsed) {
content = group.repositories.map((repository, index) => {
const fullColumnWidth = this.isFullSize(group.repositories, index);
return (
<RepositoryEntry repository={repository} fullColumnWidth={fullColumnWidth} key={index} />
);
const { group } = this.props;
const entries = group.repositories.map((repository, index) => {
return <RepositoryEntry repository={repository} key={index} />;
});
}
return (
<div className={classes.repoGroup}>
<h2>
<span className={classes.pointer} onClick={this.toggleCollapse}>
<i className={classNames("fa", icon)} /> {group.name}
</span>
</h2>
<hr />
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
{content}
</div>
<div className={classes.clearfix} />
</div>
);
return <CardColumnGroup name={group.name} elements={entries} />;
}
}
export default injectSheet(styles)(RepositoryGroupEntry);
export default RepositoryGroupEntry;

View File

@@ -159,30 +159,33 @@ ul.is-separated {
// multiline Columns
.columns.is-multiline {
.column.is-half {
height: 120px;
width: calc(50% - 0.75rem);
max-height: 120px;
&:nth-child(odd) {
margin-right: 1.5rem;
}
.overlay-half-column {
.overlay-column {
position: absolute;
height: calc(120px - 1.5rem);
height: 120px;
width: calc(50% - 3rem);
}
.overlay-half-column.is-plugin-page {
.overlay-column.is-plugin-page {
width: calc(37.5% - 1.5rem);
}
}
.column.is-full {
.overlay-full-column {
height: 120px;
.overlay-column {
position: absolute;
height: calc(120px - 0.5rem);
height: 120px;
width: calc(100% - 1.5rem);
}
.overlay-full-column.is-plugin-page {
.overlay-column.is-plugin-page {
width: calc(75% - 1.5rem);
}
}
@@ -194,12 +197,12 @@ ul.is-separated {
margin-right: 0;
}
.overlay-half-column,
.overlay-half-column.is-plugin-page {
.overlay-column,
.overlay-column.is-plugin-page {
width: calc(100% - 1.5rem);
}
}
.column.is-full .overlay-full-column.is-plugin-page {
.column.is-full .overlay-column.is-plugin-page {
width: calc(100% - 1.5rem);
}
}