make secondary navigation collapsable // save collapse status in local storage

This commit is contained in:
Eduard Heimbuch
2020-02-25 09:49:23 +01:00
parent 1c681ea588
commit 7fe8b58e7d
5 changed files with 122 additions and 39 deletions

View File

@@ -10,6 +10,7 @@ type Props = {
label: string; label: string;
activeOnlyWhenExact?: boolean; activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean; activeWhenMatch?: (route: any) => boolean;
collapsed: boolean;
}; };
class NavLink extends React.Component<Props> { class NavLink extends React.Component<Props> {
@@ -23,7 +24,7 @@ class NavLink extends React.Component<Props> {
} }
renderLink = (route: any) => { renderLink = (route: any) => {
const { to, icon, label } = this.props; const { to, icon, label, collapsed } = this.props;
let showIcon = null; let showIcon = null;
if (icon) { if (icon) {
@@ -36,9 +37,12 @@ class NavLink extends React.Component<Props> {
return ( return (
<li> <li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}> <Link
className={classNames(this.isActive(route) ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
>
{showIcon} {showIcon}
{label} {collapsed ? null : label}
</Link> </Link>
</li> </li>
); );

View File

@@ -1,20 +1,44 @@
import React, { ReactNode } from "react"; import React, { FC, ReactNode } from "react";
import Icon from "../Icon";
import { Button } from "../buttons";
import styled from "styled-components";
type Props = { type Props = {
label: string; label: string;
children?: ReactNode; children?: ReactNode;
collapsed?: boolean;
onCollapse?: (newStatus: boolean) => void;
}; };
class Section extends React.Component<Props> { const SmallButton = styled(Button)`
render() { height: 1.5rem;
const { label, children } = this.props; width: 1rem;
return ( position: absolute;
<div> right: 1.5rem;
<p className="menu-label">{label}</p> > {
<ul className="menu-list">{children}</ul> outline: none;
</div>
);
} }
} `;
const MenuLabel = styled.p`
min-height: 2.5rem;
`;
const Section: FC<Props> = ({ label, children, collapsed, onCollapse }) => {
const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { collapsed: collapsed }));
return (
<div>
<MenuLabel className="menu-label">
{collapsed ? "" : label}
{onCollapse && (
<SmallButton color="info" className="is-small" action={onCollapse}>
<Icon name={collapsed ? "arrow-left" : "arrow-right"} color="white" />
</SmallButton>
)}
</MenuLabel>
<ul className="menu-list">{childrenWithProps}</ul>
</div>
);
};
export default Section; export default Section;

View File

@@ -9,6 +9,8 @@ type Props = {
activeOnlyWhenExact?: boolean; activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean; activeWhenMatch?: (route: any) => boolean;
children?: ReactNode; children?: ReactNode;
collapsed?: boolean;
onCollapsed?: (newStatus: boolean) => void;
}; };
class SubNavigation extends React.Component<Props> { class SubNavigation extends React.Component<Props> {
@@ -22,7 +24,7 @@ class SubNavigation extends React.Component<Props> {
} }
renderLink = (route: any) => { renderLink = (route: any) => {
const { to, icon, label } = this.props; const { to, icon, label, collapsed } = this.props;
let defaultIcon = "fas fa-cog"; let defaultIcon = "fas fa-cog";
if (icon) { if (icon) {
@@ -36,8 +38,11 @@ class SubNavigation extends React.Component<Props> {
return ( return (
<li> <li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}> <Link
<i className={classNames(defaultIcon, "fa-fw")} /> {label} className={classNames(this.isActive(route) ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
>
<i className={classNames(defaultIcon, "fa-fw")} /> {collapsed ? "" : label}
</Link> </Link>
{children} {children}
</li> </li>

View File

@@ -1,12 +1,18 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Redirect, Route, Switch } from "react-router-dom"; import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { Repository } from "@scm-manager/ui-types"; import { Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components"; import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; import {
fetchRepoByName,
getFetchRepoFailure,
getRepository,
isFetchRepoPending,
isRepositoryMenuCollapsed,
switchRepositoryMenuCollapsed
} from "../modules/repos";
import RepositoryDetails from "../components/RepositoryDetails"; import RepositoryDetails from "../components/RepositoryDetails";
import EditRepo from "./EditRepo"; import EditRepo from "./EditRepo";
import BranchesOverview from "../branches/containers/BranchesOverview"; import BranchesOverview from "../branches/containers/BranchesOverview";
@@ -21,29 +27,46 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
import ChangesetView from "./ChangesetView"; import ChangesetView from "./ChangesetView";
import SourceExtensions from "../sources/containers/SourceExtensions"; import SourceExtensions from "../sources/containers/SourceExtensions";
type Props = WithTranslation & { type Props = RouteComponentProps &
namespace: string; WithTranslation & {
name: string; namespace: string;
repository: Repository; name: string;
loading: boolean; repository: Repository;
error: Error; loading: boolean;
repoLink: string; error: Error;
indexLinks: object; repoLink: string;
indexLinks: object;
// dispatch functions // dispatch functions
fetchRepoByName: (link: string, namespace: string, name: string) => void; fetchRepoByName: (link: string, namespace: string, name: string) => void;
};
// context props type State = {
history: History; collapsedMenu: boolean;
match: any;
}; };
class RepositoryRoot extends React.Component<Props> { class RepositoryRoot extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsedMenu: isRepositoryMenuCollapsed()
};
}
componentDidMount() { componentDidMount() {
const { fetchRepoByName, namespace, name, repoLink } = this.props; const { fetchRepoByName, namespace, name, repoLink } = this.props;
fetchRepoByName(repoLink, namespace, name); fetchRepoByName(repoLink, namespace, name);
} }
componentDidUpdate() {
if (this.state.collapsedMenu && this.isCollapseForbidden()) {
this.onCollapse(false);
}
}
isCollapseForbidden= () => {
return this.props.location.pathname.includes("/settings/");
};
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
if (url.endsWith("/")) { if (url.endsWith("/")) {
return url.substring(0, url.length - 1); return url.substring(0, url.length - 1);
@@ -87,8 +110,13 @@ class RepositoryRoot extends React.Component<Props> {
return `${url}/changesets`; return `${url}/changesets`;
}; };
onCollapse = (newStatus: boolean) => {
this.setState({ collapsedMenu: newStatus }, () => switchRepositoryMenuCollapsed(newStatus));
};
render() { render() {
const { loading, error, indexLinks, repository, t } = this.props; const { loading, error, indexLinks, repository, t } = this.props;
const { collapsedMenu } = this.state;
if (error) { if (error) {
return ( return (
@@ -119,7 +147,7 @@ class RepositoryRoot extends React.Component<Props> {
return ( return (
<Page title={repository.namespace + "/" + repository.name}> <Page title={repository.namespace + "/" + repository.name}>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column">
<Switch> <Switch>
<Redirect exact from={this.props.match.url} to={redirectedUrl} /> <Redirect exact from={this.props.match.url} to={redirectedUrl} />
@@ -169,9 +197,13 @@ class RepositoryRoot extends React.Component<Props> {
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
</Switch> </Switch>
</div> </div>
<div className="column"> <div className={collapsedMenu ? "column is-1" : "column is-3"}>
<Navigation> <Navigation>
<Section label={t("repositoryRoot.menu.navigationLabel")}> <Section
label={t("repositoryRoot.menu.navigationLabel")}
onCollapse={!this.isCollapseForbidden() ? () => this.onCollapse(!collapsedMenu) : undefined}
collapsed={collapsedMenu}
>
<ExtensionPoint name="repository.navigation.topLevel" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.navigation.topLevel" props={extensionProps} renderAll={true} />
<NavLink <NavLink
to={`${url}/info`} to={`${url}/info`}
@@ -197,7 +229,11 @@ class RepositoryRoot extends React.Component<Props> {
activeOnlyWhenExact={false} activeOnlyWhenExact={false}
/> />
<ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} />
<SubNavigation to={`${url}/settings/general`} label={t("repositoryRoot.menu.settingsNavLink")}> <SubNavigation
to={`${url}/settings/general`}
label={t("repositoryRoot.menu.settingsNavLink")}
onCollapsed={() => this.onCollapse(false)}
>
<EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} /> <EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} />
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} /> <PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} />
<ExtensionPoint name="repository.setting" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.setting" props={extensionProps} renderAll={true} />

View File

@@ -155,7 +155,12 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error):
// create repo // create repo
export function createRepo(link: string, repository: Repository, initRepository: boolean, callback?: (repo: Repository) => void) { export function createRepo(
link: string,
repository: Repository,
initRepository: boolean,
callback?: (repo: Repository) => void
) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(createRepoPending()); dispatch(createRepoPending());
const repoLink = initRepository ? link + "?initialize=true" : link; const repoLink = initRepository ? link + "?initialize=true" : link;
@@ -436,3 +441,12 @@ export function getPermissionsLink(state: object, namespace: string, name: strin
const repo = getRepository(state, namespace, name); const repo = getRepository(state, namespace, name);
return repo && repo._links ? repo._links.permissions.href : undefined; return repo && repo._links ? repo._links.permissions.href : undefined;
} }
const REPOSITORY_NAVIGATION_COLLAPSED = "repository-menu-collapsed";
export function isRepositoryMenuCollapsed() {
return localStorage.getItem(REPOSITORY_NAVIGATION_COLLAPSED) === "true";
}
export function switchRepositoryMenuCollapsed(newStatus: boolean) {
localStorage.setItem(REPOSITORY_NAVIGATION_COLLAPSED, String(newStatus));
}