This commit is contained in:
Eduard Heimbuch
2019-07-09 17:05:35 +02:00
12 changed files with 319 additions and 353 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 SyntaxHighlighter } from "./SyntaxHighlighter";
export { default as ErrorBoundary } from "./ErrorBoundary"; export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions.js"; 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 { apiClient } from "./apiclient.js";
export * from "./errors"; export * from "./errors";

View File

@@ -1,86 +1,44 @@
//@flow //@flow
import React from "react"; 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 type { Plugin } from "@scm-manager/ui-types";
import { CardColumn } from "@scm-manager/ui-components";
import PluginAvatar from "./PluginAvatar"; 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 = { type Props = {
plugin: Plugin, plugin: Plugin
fullColumnWidth?: boolean,
// context props
classes: any
}; };
class PluginEntry extends React.Component<Props> { 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() { render() {
const { plugin, classes, fullColumnWidth } = this.props; const { plugin } = this.props;
const halfColumn = fullColumnWidth ? "is-full" : "is-half"; const avatar = this.createAvatar(plugin);
const overlayLinkClass = fullColumnWidth const footerLeft = this.createFooterLeft(plugin);
? "overlay-full-column" const footerRight = this.createFooterRight(plugin);
: "overlay-half-column";
// TODO: Add link to plugin page below // TODO: Add link to plugin page below
return ( return (
<div <CardColumn
className={classNames( link="#"
"box", avatar={avatar}
"box-link-shadow", title={plugin.name}
"column", description={plugin.description}
"is-clipped", footerLeft={footerLeft}
halfColumn footerRight={footerRight}
)}
>
<Link
className={classNames(overlayLinkClass, "is-plugin-page")}
to="#"
/> />
<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 //@flow
import React from "react"; import React from "react";
import injectSheet from "react-jss"; import { CardColumnGroup } from "@scm-manager/ui-components";
import classNames from "classnames"; import type { PluginGroup } from "@scm-manager/ui-types";
import type { PluginGroup, Plugin } from "@scm-manager/ui-types";
import PluginEntry from "./PluginEntry"; 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 = { type Props = {
group: PluginGroup, group: PluginGroup
// context props
classes: any
}; };
type State = { class PluginGroupEntry extends React.Component<Props> {
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);
};
render() { render() {
const { group, classes } = this.props; const { group } = this.props;
const { collapsed } = this.state; const entries = group.plugins.map((plugin, index) => {
return <PluginEntry plugin={plugin} key={index} />;
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}
/>
);
}); });
} return <CardColumnGroup name={group.name} elements={entries} />;
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>
);
} }
} }
export default injectSheet(styles)(PluginGroupEntry); export default PluginGroupEntry;

View File

@@ -14,7 +14,7 @@ class PluginList extends React.Component<Props> {
const groups = groupByCategory(plugins); const groups = groupByCategory(plugins);
return ( return (
<div className="content"> <div className="content is-plugin-page">
{groups.map(group => { {groups.map(group => {
return <PluginGroupEntry group={group} key={group.name} />; return <PluginGroupEntry group={group} key={group.name} />;
})} })}

View File

@@ -144,8 +144,7 @@ export default function reducer(
switch (action.type) { switch (action.type) {
case FETCH_PLUGINS_SUCCESS: case FETCH_PLUGINS_SUCCESS:
const t = normalizeByName(action.payload); return normalizeByName(action.payload);
return t;
case FETCH_PLUGIN_SUCCESS: case FETCH_PLUGIN_SUCCESS:
return reducerByNames(state, action.payload); return reducerByNames(state, action.payload);
default: default:
@@ -190,8 +189,3 @@ export function isFetchPluginPending(state: Object, name: string) {
export function getFetchPluginFailure(state: Object, name: string) { export function getFetchPluginFailure(state: Object, name: string) {
return getFailure(state, FETCH_PLUGIN, name); return getFailure(state, FETCH_PLUGIN, name);
} }
export function getPermissionsLink(state: Object, name: string) {
const plugin = getPlugin(state, name);
return plugin && plugin._links ? plugin._links.permissions.href : undefined;
}

View File

@@ -23,10 +23,7 @@ import reducer, {
isFetchPluginPending, isFetchPluginPending,
getFetchPluginFailure getFetchPluginFailure
} from "./plugins"; } from "./plugins";
import type { import type { Plugin, PluginCollection } from "@scm-manager/ui-types";
Plugin,
PluginCollection
} from "@scm-manager/ui-types";
const groupManagerPlugin: Plugin = { const groupManagerPlugin: Plugin = {
name: "scm-groupmanager-plugin", name: "scm-groupmanager-plugin",
@@ -37,8 +34,7 @@ const groupManagerPlugin: Plugin = {
description: "Notify a remote webserver whenever a plugin is pushed to.", description: "Notify a remote webserver whenever a plugin is pushed to.",
_links: { _links: {
self: { self: {
href: href: "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin"
"http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin"
} }
} }
}; };
@@ -52,8 +48,7 @@ const scriptPlugin: Plugin = {
description: "Script support for scm-manager.", description: "Script support for scm-manager.",
_links: { _links: {
self: { self: {
href: href: "http://localhost:8081/api/v2/ui/plugins/scm-script-plugin"
"http://localhost:8081/api/v2/ui/plugins/scm-script-plugin"
} }
} }
}; };
@@ -67,8 +62,7 @@ const branchwpPlugin: Plugin = {
description: "This plugin adds branch write protection for plugins.", description: "This plugin adds branch write protection for plugins.",
_links: { _links: {
self: { self: {
href: href: "http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin"
"http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin"
} }
} }
}; };
@@ -166,12 +160,9 @@ describe("plugins fetch", () => {
}); });
it("should dispatch FETCH_PLUGIN_FAILURE, if the request for scm-groupmanager-plugin by name fails", () => { it("should dispatch FETCH_PLUGIN_FAILURE, if the request for scm-groupmanager-plugin by name fails", () => {
fetchMock.getOnce( fetchMock.getOnce(PLUGINS_URL + "/scm-groupmanager-plugin", {
PLUGINS_URL + "/scm-groupmanager-plugin",
{
status: 500 status: 500
} });
);
const store = mockStore({}); const store = mockStore({});
return store return store
@@ -331,8 +322,7 @@ describe("plugins selectors", () => {
it("should return true, when fetch plugin is pending", () => { it("should return true, when fetch plugin is pending", () => {
const state = { const state = {
pending: { pending: {
[FETCH_PLUGIN + [FETCH_PLUGIN + "/scm-groupmanager-plugin"]: true
"/scm-groupmanager-plugin"]: true
} }
}; };
expect(isFetchPluginPending(state, "scm-groupmanager-plugin")).toEqual( expect(isFetchPluginPending(state, "scm-groupmanager-plugin")).toEqual(
@@ -347,8 +337,7 @@ describe("plugins selectors", () => {
it("should return error when fetch plugin did fail", () => { it("should return error when fetch plugin did fail", () => {
const state = { const state = {
failure: { failure: {
[FETCH_PLUGIN + [FETCH_PLUGIN + "/scm-groupmanager-plugin"]: error
"/scm-groupmanager-plugin"]: error
} }
}; };
expect(getFetchPluginFailure(state, "scm-groupmanager-plugin")).toEqual( expect(getFetchPluginFailure(state, "scm-groupmanager-plugin")).toEqual(

View File

@@ -1,33 +1,12 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Link } from "react-router-dom";
import injectSheet from "react-jss";
import type { Repository } from "@scm-manager/ui-types"; 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 RepositoryEntryLink from "./RepositoryEntryLink";
import classNames from "classnames";
import RepositoryAvatar from "./RepositoryAvatar"; 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 = { type Props = {
repository: Repository, repository: Repository
fullColumnWidth?: boolean,
// context props
classes: any
}; };
class RepositoryEntry extends React.Component<Props> { class RepositoryEntry extends React.Component<Props> {
@@ -83,53 +62,41 @@ class RepositoryEntry extends React.Component<Props> {
return null; return null;
}; };
render() { createFooterLeft = (repository: Repository, repositoryLink: string) => {
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";
return ( 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.renderBranchesLink(repository, repositoryLink)}
{this.renderChangesetsLink(repository, repositoryLink)} {this.renderChangesetsLink(repository, repositoryLink)}
{this.renderSourcesLink(repository, repositoryLink)} {this.renderSourcesLink(repository, repositoryLink)}
{this.renderModifyLink(repository, repositoryLink)} {this.renderModifyLink(repository, repositoryLink)}
</div> </>
<div className="level-right is-hidden-mobile"> );
};
createFooterRight = (repository: Repository) => {
return (
<small className="level-item"> <small className="level-item">
<DateFromNow date={repository.creationDate} /> <DateFromNow date={repository.creationDate} />
</small> </small>
</div> );
</nav> };
</div>
</article> render() {
</div> 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 //@flow
import React from "react"; import React from "react";
import type { RepositoryGroup, Repository } from "@scm-manager/ui-types"; import { CardColumnGroup } from "@scm-manager/ui-components";
import injectSheet from "react-jss"; import type { RepositoryGroup } from "@scm-manager/ui-types";
import classNames from "classnames";
import RepositoryEntry from "./RepositoryEntry"; 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 = { type Props = {
group: RepositoryGroup, group: RepositoryGroup
// context props
classes: any
}; };
type State = { class RepositoryGroupEntry extends React.Component<Props> {
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);
};
render() { render() {
const { group, classes } = this.props; const { group } = this.props;
const { collapsed } = this.state; const entries = group.repositories.map((repository, index) => {
return <RepositoryEntry repository={repository} key={index} />;
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} />
);
}); });
} return <CardColumnGroup name={group.name} elements={entries} />;
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>
);
} }
} }
export default injectSheet(styles)(RepositoryGroupEntry); export default RepositoryGroupEntry;

View File

@@ -159,33 +159,31 @@ ul.is-separated {
// multiline Columns // multiline Columns
.columns.is-multiline { .columns.is-multiline {
.column {
height: 120px;
.overlay-column {
position: absolute;
height: calc(120px - 1.5rem);
}
}
.column.is-half { .column.is-half {
width: calc(50% - 0.75rem); width: calc(50% - 0.75rem);
max-height: 120px;
&:nth-child(odd) { &:nth-child(odd) {
margin-right: 1.5rem; margin-right: 1.5rem;
} }
.overlay-half-column { .overlay-column {
position: absolute;
height: calc(120px - 1.5rem);
width: calc(50% - 3rem); width: calc(50% - 3rem);
} }
.overlay-half-column.is-plugin-page {
width: calc(37.5% - 1.5rem);
} }
}
.column.is-full { .column.is-full .overlay-column {
.overlay-full-column {
position: absolute;
height: calc(120px - 0.5rem);
width: calc(100% - 1.5rem); width: calc(100% - 1.5rem);
} }
.overlay-full-column.is-plugin-page {
width: calc(75% - 1.5rem);
}
}
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.column.is-half { .column.is-half {
width: 100%; width: 100%;
@@ -194,15 +192,29 @@ ul.is-separated {
margin-right: 0; margin-right: 0;
} }
.overlay-half-column, .overlay-column {
.overlay-half-column.is-plugin-page {
width: calc(100% - 1.5rem); width: calc(100% - 1.5rem);
} }
} }
.column.is-full .overlay-full-column.is-plugin-page { }
}
.content.is-plugin-page {
.columns.is-multiline {
.column.is-half .overlay-column {
width: calc(37.5% - 1.5rem);
}
.column.is-full .overlay-column {
width: calc(75% - 1.5rem);
}
@media screen and (max-width: 768px) {
.column.is-half .overlay-column,
.column.is-full .overlay-column {
width: calc(100% - 1.5rem); width: calc(100% - 1.5rem);
} }
} }
}
} }
.text-box { .text-box {

View File

@@ -29,7 +29,7 @@ public class UIPluginDtoMapper {
UIPluginDto dto = new UIPluginDto(); UIPluginDto dto = new UIPluginDto();
dto.setName(plugin.getPlugin().getInformation().getName()); dto.setName(plugin.getPlugin().getInformation().getName());
dto.setBundles(getScriptResources(plugin)); dto.setBundles(getScriptResources(plugin));
dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Sonstige/Miscellaneous"); dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Miscellaneous");
dto.setVersion(plugin.getPlugin().getInformation().getVersion()); dto.setVersion(plugin.getPlugin().getInformation().getVersion());
dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); dto.setAuthor(plugin.getPlugin().getInformation().getAuthor());
dto.setDescription(plugin.getPlugin().getInformation().getDescription()); dto.setDescription(plugin.getPlugin().getInformation().getDescription());