diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index f0f2d5d971..a034b4afbb 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -4,6 +4,8 @@ import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; import Subtitle from "./Subtitle"; +import injectSheet from "react-jss"; +import classNames from "classnames"; type Props = { title?: string, @@ -11,17 +13,26 @@ type Props = { loading?: boolean, error?: Error, showContentOnError?: boolean, - children: React.Node + children: React.Node, + + // context props + classes: Object +}; + +const styles = { + spacing: { + marginTop: "1.25rem", + textAlign: "right" + } }; class Page extends React.Component { render() { - const { title, error, subtitle } = this.props; + const { error } = this.props; return (
- - <Subtitle subtitle={subtitle} /> + {this.renderPageHeader()} <ErrorNotification error={error} /> {this.renderContent()} </div> @@ -29,16 +40,55 @@ class Page extends React.Component<Props> { ); } + renderPageHeader() { + const { title, subtitle, children, classes } = this.props; + + let content = null; + let pageActionsExists = false; + React.Children.forEach(children, child => { + if (child && child.type.name === "PageActions") { + content = child; + pageActionsExists = true; + } + }); + let underline = pageActionsExists ? ( + <hr className="header-with-actions" /> + ) : null; + + return ( + <> + <div className="columns"> + <div className="column"> + <Title title={title} /> + <Subtitle subtitle={subtitle} /> + </div> + <div className="column is-two-fifths"> + <div className={classNames(classes.spacing, "is-mobile-create-button-spacing")}>{content}</div> + </div> + </div> + {underline} + </> + ); + } + renderContent() { const { loading, children, showContentOnError, error } = this.props; + if (error && !showContentOnError) { return null; } if (loading) { return <Loading />; } - return children; + + let content = []; + React.Children.forEach(children, child => { + if (child && child.type.name !== "PageActions") { + content.push(child); + } + }); + return content; } } -export default Page; +export default injectSheet(styles)(Page); diff --git a/scm-ui-components/packages/ui-components/src/layout/PageActions.js b/scm-ui-components/packages/ui-components/src/layout/PageActions.js new file mode 100644 index 0000000000..eb055a5605 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/layout/PageActions.js @@ -0,0 +1,28 @@ +//@flow +import * as React from "react"; +import Loading from "./../Loading"; + +type Props = { + loading?: boolean, + error?: Error, + children: React.Node +}; + +class PageActions extends React.Component<Props> { + render() { + return <>{this.renderContent()}</>; + } + + renderContent() { + const { loading, children, error } = this.props; + if (error) { + return null; + } + if (loading) { + return <Loading />; + } + return children; + } +} + +export default PageActions; diff --git a/scm-ui-components/packages/ui-components/src/layout/Subtitle.js b/scm-ui-components/packages/ui-components/src/layout/Subtitle.js index 249c34023f..4558faeb30 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Subtitle.js +++ b/scm-ui-components/packages/ui-components/src/layout/Subtitle.js @@ -9,7 +9,7 @@ class Subtitle extends React.Component<Props> { render() { const { subtitle } = this.props; if (subtitle) { - return <h1 className="subtitle">{subtitle}</h1>; + return <h2 className="subtitle">{subtitle}</h2>; } return null; } diff --git a/scm-ui-components/packages/ui-components/src/layout/index.js b/scm-ui-components/packages/ui-components/src/layout/index.js index c93fe3b496..7708c45c99 100644 --- a/scm-ui-components/packages/ui-components/src/layout/index.js +++ b/scm-ui-components/packages/ui-components/src/layout/index.js @@ -3,6 +3,7 @@ export { default as Footer } from "./Footer.js"; export { default as Header } from "./Header.js"; export { default as Page } from "./Page.js"; +export { default as PageActions } from "./PageActions.js"; export { default as Subtitle } from "./Subtitle.js"; export { default as Title } from "./Title.js"; diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 984055c60f..7bc31a8b79 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -5,7 +5,12 @@ import { translate } from "react-i18next"; import type { Group } from "@scm-manager/ui-types"; import type { PagedCollection } from "@scm-manager/ui-types"; import type { History } from "history"; -import { Page, Paginator } from "@scm-manager/ui-components"; +import { + Page, + PageActions, + Button, + Paginator +} from "@scm-manager/ui-components"; import { GroupTable } from "./../components/table"; import CreateGroupButton from "../components/buttons/CreateGroupButton"; @@ -73,6 +78,13 @@ class Groups extends React.Component<Props> { <GroupTable groups={groups} /> {this.renderPaginator()} {this.renderCreateButton()} + <PageActions> + <Button + label={t("create-group-button.label")} + link="/groups/add" + color="primary" + /> + </PageActions> </Page> ); } diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index 598b6c94f2..ac044b5225 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -14,7 +14,13 @@ import { isFetchReposPending } from "../modules/repos"; import { translate } from "react-i18next"; -import { CreateButton, Page, Paginator } from "@scm-manager/ui-components"; +import { + Page, + PageActions, + Button, + CreateButton, + Paginator +} from "@scm-manager/ui-components"; import RepositoryList from "../components/list"; import { withRouter } from "react-router-dom"; import type { History } from "history"; @@ -67,6 +73,13 @@ class Overview extends React.Component<Props> { error={error} > {this.renderList()} + <PageActions> + <Button + label={t("overview.createButton")} + link="/repos/create" + color="primary" + /> + </PageActions> </Page> ); } @@ -89,10 +102,7 @@ class Overview extends React.Component<Props> { const { showCreateButton, t } = this.props; if (showCreateButton) { return ( - <CreateButton - label={t("overview.createButton")} - link="/repos/create" - /> + <CreateButton label={t("overview.createButton")} link="/repos/create" /> ); } return null; diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 041ac226b4..10dedde32d 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -14,7 +14,13 @@ import { getFetchUsersFailure } from "../modules/users"; -import { Page, CreateButton, Paginator } from "@scm-manager/ui-components"; +import { + Page, + PageActions, + Button, + CreateButton, + Paginator +} from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; import type { User, PagedCollection } from "@scm-manager/ui-types"; import { getUsersLink } from "../../modules/indexResource"; @@ -72,6 +78,13 @@ class Users extends React.Component<Props> { <UserTable users={users} /> {this.renderPaginator()} {this.renderCreateButton()} + <PageActions> + <Button + label={t("users.createButton")} + link="/users/add" + color="primary" + /> + </PageActions> </Page> ); } diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 0e7b0fc420..df42781c64 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -40,6 +40,25 @@ $info: $blue; .main { min-height: calc(100vh - 260px); } + +// shown in top section when pageactions set +hr.header-with-actions { + margin-top: -10px; + + @media screen and (max-width: 768px) { + display: none; + } +} +.is-mobile-create-button-spacing { + @media screen and (max-width: 768px) { + border: 2px solid #e9f7fd; + padding: 1em 1em; + margin-top: 0 !important; + width: 100%; + text-align: center !important; + } +} + .footer { height: 50px; }