Save collapse status of secondary navigation

Pushed-by: Florian Scholdei<florian.scholdei@cloudogu.com>
Co-authored-by: Florian Scholdei<florian.scholdei@cloudogu.com>
Co-authored-by: Konstantin Schaper<konstantin.schaper@cloudogu.com>
Committed-by: Florian Scholdei<florian.scholdei@cloudogu.com>
This commit is contained in:
Florian Scholdei
2023-12-12 09:18:50 +01:00
parent 4f053028b9
commit 72886bd204
26 changed files with 675 additions and 594 deletions

View File

@@ -0,0 +1,2 @@
- type: changed
description: Save collapse status of secondary navigation

View File

@@ -22,11 +22,12 @@
* SOFTWARE.
*/
import React, { createContext, FC, useCallback, useContext, useMemo, useState } from "react";
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from "react";
type LocalStorage = {
getItem: <T>(key: string, initialValue: T) => T;
getItem: <T>(key: string, fallback: T) => T;
setItem: <T>(key: string, value: T) => void;
preload: <T>(key: string, initialValue: T) => void;
};
const LocalStorageContext = createContext<LocalStorage>(null as unknown as LocalStorage);
@@ -50,9 +51,14 @@ export const LocalStorageProvider: FC = ({ children }) => {
}, []);
const getItem = useCallback(
<T,>(key: string, initialValue: T): T => {
let initialLoadResult: T | undefined;
<T,>(key: string, fallback: T): T => (key in localStorageCache ? (localStorageCache[key] as T) : fallback),
[localStorageCache]
);
const preload = useCallback(
<T,>(key: string, initialValue: T) => {
if (!(key in localStorageCache)) {
let initialLoadResult: T | undefined;
try {
const item = localStorage.getItem(key);
initialLoadResult = item ? JSON.parse(item) : initialValue;
@@ -63,13 +69,12 @@ export const LocalStorageProvider: FC = ({ children }) => {
}
setItem(key, initialLoadResult);
}
return initialLoadResult ?? (localStorageCache[key] as T);
},
[localStorageCache, setItem]
);
return (
<LocalStorageContext.Provider value={useMemo(() => ({ getItem, setItem }), [getItem, setItem])}>
<LocalStorageContext.Provider value={useMemo(() => ({ getItem, setItem, preload }), [getItem, preload, setItem])}>
{children}
</LocalStorageContext.Provider>
);
@@ -85,7 +90,7 @@ export function useLocalStorage<T>(
key: string,
initialValue: T
): [value: T, setValue: (value: T | ((previousConfig: T) => T)) => void] {
const { getItem, setItem } = useContext(LocalStorageContext);
const { getItem, setItem, preload } = useContext(LocalStorageContext);
const value = useMemo(() => getItem(key, initialValue), [getItem, initialValue, key]);
const setValue = useCallback(
(newValue: T | ((previousConfig: T) => T)) =>
@@ -95,5 +100,6 @@ export function useLocalStorage<T>(
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, setItem, value]
);
useEffect(() => preload(key, initialValue), [initialValue, key, preload]);
return useMemo(() => [value, setValue], [setValue, value]);
}

View File

@@ -72218,7 +72218,7 @@ exports[`Storyshots Secondary Navigation Active when match 1`] = `
className="column is-3"
>
<aside
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 cZyrQa menu"
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 ioXCUt menu"
>
<div>
<p
@@ -72237,7 +72237,6 @@ exports[`Storyshots Secondary Navigation Active when match 1`] = `
</p>
<ul
className="menu-list"
onClick={[Function]}
>
<li>
<a
@@ -72280,7 +72279,7 @@ exports[`Storyshots Secondary Navigation Default 1`] = `
className="column is-3"
>
<aside
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 cZyrQa menu"
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 ioXCUt menu"
>
<div>
<p
@@ -72299,7 +72298,6 @@ exports[`Storyshots Secondary Navigation Default 1`] = `
</p>
<ul
className="menu-list"
onClick={[Function]}
>
<li>
<a
@@ -72342,7 +72340,7 @@ exports[`Storyshots Secondary Navigation Extension Point 1`] = `
className="column is-3"
>
<aside
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 cZyrQa menu"
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 ioXCUt menu"
>
<div>
<p
@@ -72361,7 +72359,6 @@ exports[`Storyshots Secondary Navigation Extension Point 1`] = `
</p>
<ul
className="menu-list"
onClick={[Function]}
>
<li>
<a
@@ -72430,7 +72427,7 @@ exports[`Storyshots Secondary Navigation Sub Navigation 1`] = `
className="column is-3"
>
<aside
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 cZyrQa menu"
className="SecondaryNavigation__SectionContainer-sc-8p1rgi-0 ioXCUt menu"
>
<div>
<p
@@ -72449,7 +72446,6 @@ exports[`Storyshots Secondary Navigation Sub Navigation 1`] = `
</p>
<ul
className="menu-list"
onClick={[Function]}
>
<li>
<a

View File

@@ -89,6 +89,7 @@ export * from "./markdown/PluginApi";
export * from "./devices";
export { default as copyToClipboard } from "./CopyToClipboard";
export { createA11yId } from "./createA11yId";
export { useSecondaryNavigation } from "./useSecondaryNavigation";
export { default as comparators } from "./comparators";

View File

@@ -26,6 +26,7 @@ import { storiesOf } from "@storybook/react";
import Footer from "./Footer";
import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions";
import { Me } from "@scm-manager/ui-types";
import { LocalStorageProvider } from "@scm-manager/ui-api";
import { EXTENSION_POINT } from "../avatar/Avatar";
// @ts-ignore ignore unknown png
import hitchhiker from "../__resources__/hitchhiker.png";
@@ -62,6 +63,7 @@ const withBinder = (binder: Binder) => {
};
storiesOf("Footer", module)
.addDecorator((story) => <LocalStorageProvider>{story()}</LocalStorageProvider>)
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.add("Default", () => {
return <Footer me={trillian} version="2.0.0" links={{}} />;

View File

@@ -22,26 +22,9 @@
* SOFTWARE.
*/
import React, { FC } from "react";
import styled from "styled-components";
import useMenuContext from "../navigation/MenuContext";
const PrimaryColumn = styled.div<{ collapsed: boolean }>`
/* This is the counterpart to the specific column in SecondaryNavigationColumn. */
flex: none;
width: ${(props: { collapsed: boolean }) => (props.collapsed ? "89.7%" : "75%")};
/* Render this column to full size if column construct breaks (page size too small). */
@media (max-width: 785px) {
width: 100%;
}
`;
const PrimaryContentColumn: FC = ({ children }) => {
const context = useMenuContext();
return (
<PrimaryColumn className="column" collapsed={context.isCollapsed()}>
{children}
</PrimaryColumn>
);
return <div className="column">{children}</div>;
};
export default PrimaryContentColumn;

View File

@@ -23,28 +23,13 @@
*/
import React, { FC } from "react";
import styled from "styled-components";
import useMenuContext from "../navigation/MenuContext";
const SecondaryColumn = styled.div<{ collapsed: boolean }>`
/* In Bulma there is unfortunately no intermediate step between .is-1 and .is-2, hence the size.
Navigation size should be as constant as possible. */
flex: none;
width: ${(props) => (props.collapsed ? "5.5rem" : "20.5rem")};
max-width: ${(props: { collapsed: boolean }) => (props.collapsed ? "11.3%" : "25%")};
/* Render this column to full size if column construct breaks (page size too small). */
@media (max-width: 785px) {
width: 100%;
max-width: 100%;
}
const SecondaryColumn = styled.div`
flex: 0 0 auto;
`;
const SecondaryNavigationColumn: FC = ({ children }) => {
const context = useMenuContext();
return (
<SecondaryColumn className="column" collapsed={context.isCollapsed()}>
{children}
</SecondaryColumn>
);
return <SecondaryColumn className="column">{children}</SecondaryColumn>;
};
export default SecondaryNavigationColumn;

View File

@@ -21,10 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React, { FC, useContext } from "react";
import classNames from "classnames";
import { useSecondaryNavigation } from "@scm-manager/ui-components";
import ExternalLink from "./ExternalLink";
import useMenuContext from "./MenuContext";
import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
type Props = {
to: string;
@@ -33,8 +34,8 @@ type Props = {
};
const ExternalNavLink: FC<Props> = ({ to, icon, label }) => {
const context = useMenuContext();
const collapsed = context.isCollapsed();
const { collapsed } = useSecondaryNavigation();
const isSecondaryNavigation = useContext(SecondaryNavigationContext);
let showIcon;
if (icon) {
@@ -49,7 +50,7 @@ const ExternalNavLink: FC<Props> = ({ to, icon, label }) => {
<li title={collapsed ? label : undefined}>
<ExternalLink to={to} className={collapsed ? "has-text-centered" : ""}>
{showIcon}
{collapsed ? null : label}
{isSecondaryNavigation && collapsed ? null : label}
</ExternalLink>
</li>
);

View File

@@ -29,6 +29,9 @@ export type MenuContext = {
setCollapsed: (collapsed: boolean) => void;
};
/**
* @deprecated
*/
export const MenuContext = React.createContext<MenuContext>({
isCollapsed() {
return false;

View File

@@ -21,13 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React, { FC, useContext, useEffect } from "react";
import classNames from "classnames";
import { Link } from "react-router-dom";
import { useSecondaryNavigation } from "@scm-manager/ui-components";
import { RoutingProps } from "./RoutingProps";
import useMenuContext from "./MenuContext";
import useActiveMatch from "./useActiveMatch";
import { createAttributesForTesting } from "../devBuild";
import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
import { SubNavigationContext } from "./SubNavigationContext";
type Props = RoutingProps & {
label: string;
@@ -55,9 +57,15 @@ const NavLinkContent: FC<NavLinkContentProp> = ({ label, icon, collapsed }) => (
const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, title, testId, children, ...contentProps }) => {
const active = useActiveMatch({ to, activeWhenMatch, activeOnlyWhenExact });
const { collapsed, setCollapsible } = useSecondaryNavigation();
const isSecondaryNavigation = useContext(SecondaryNavigationContext);
const isSubNavigation = useContext(SubNavigationContext);
const context = useMenuContext();
const collapsed = context.isCollapsed();
useEffect(() => {
if (isSecondaryNavigation && active) {
setCollapsible(!isSubNavigation);
}
}, [active, isSecondaryNavigation, isSubNavigation, setCollapsible]);
return (
<li title={collapsed ? title : undefined}>
@@ -66,7 +74,11 @@ const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, title, t
to={to}
{...createAttributesForTesting(testId)}
>
{children ? children : <NavLinkContent {...contentProps} collapsed={collapsed} />}
{children ? (
children
) : (
<NavLinkContent {...contentProps} collapsed={(isSecondaryNavigation && collapsed) ?? false} />
)}
</Link>
</li>
);

View File

@@ -24,13 +24,13 @@
import { storiesOf } from "@storybook/react";
import React, { ReactElement } from "react";
import { LocalStorageProvider } from "@scm-manager/ui-api";
import SecondaryNavigation from "./SecondaryNavigation";
import SecondaryNavigationItem from "./SecondaryNavigationItem";
import styled from "styled-components";
import SubNavigation from "./SubNavigation";
import { Binder, ExtensionPoint, BinderContext } from "@scm-manager/ui-extensions";
import { MemoryRouter } from "react-router-dom";
import { StateMenuContextProvider } from "./MenuContext";
const Columns = styled.div`
margin: 2rem;
@@ -53,8 +53,8 @@ const withRoute = (route: string) => {
};
storiesOf("Secondary Navigation", module)
.addDecorator(story => <StateMenuContextProvider>{story()}</StateMenuContextProvider>)
.addDecorator(story => (
.addDecorator((story) => <LocalStorageProvider>{story()}</LocalStorageProvider>)
.addDecorator((story) => (
<Columns className="columns">
<div className="column is-3">{story()}</div>
</Columns>
@@ -92,7 +92,7 @@ storiesOf("Secondary Navigation", module)
<SecondaryNavigation label="Hitchhiker">
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
<SecondaryNavigationItem
activeWhenMatch={route => route.location?.pathname === "/hog"}
activeWhenMatch={(route) => route.location?.pathname === "/hog"}
to="/heart-of-gold"
icon="fas fa-star"
label="Heart Of Gold"

View File

@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import styled from "styled-components";
import useMenuContext from "./MenuContext";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { useSecondaryNavigation } from "@scm-manager/ui-components";
import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
type Props = {
label: string;
@@ -37,11 +37,12 @@ type CollapsedProps = {
collapsed: boolean;
};
const SectionContainer = styled.aside`
const SectionContainer = styled.aside<{ collapsed: boolean }>`
flex: 0 0 auto;
position: sticky;
position: -webkit-sticky; /* Safari */
top: 5rem;
width: 100%;
min-width: ${(props: { collapsed: boolean }) => (props.collapsed ? "0" : "200px")};
@media (max-height: 900px) {
position: relative;
@@ -63,42 +64,29 @@ const MenuLabel = styled.p<CollapsedProps>`
const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true }) => {
const [t] = useTranslation("commons");
const menuContext = useMenuContext();
const isCollapsed = collapsible && menuContext.isCollapsed();
const { collapsible: isCollapsible, collapsed, toggleCollapse } = useSecondaryNavigation(collapsible);
const toggleCollapseState = () => {
if (collapsible) {
menuContext.setCollapsed(!isCollapsed);
}
};
const uncollapseMenu = () => {
if (collapsible && isCollapsed) {
menuContext.setCollapsed(false);
}
};
const arrowIcon = isCollapsed ? <i className="fas fa-caret-left" /> : <i className="fas fa-caret-down" />;
const menuAriaLabel = isCollapsed ? t("secondaryNavigation.showContent") : t("secondaryNavigation.hideContent");
const arrowIcon = collapsed ? <i className="fas fa-caret-left" /> : <i className="fas fa-caret-down" />;
const menuAriaLabel = collapsed ? t("secondaryNavigation.showContent") : t("secondaryNavigation.hideContent");
return (
<SectionContainer className="menu">
<SectionContainer className="menu" collapsed={collapsed ?? false}>
<div>
<MenuLabel
className={classNames("menu-label", { "is-clickable": collapsible })}
collapsed={isCollapsed}
onClick={toggleCollapseState}
className={classNames("menu-label", { "is-clickable": isCollapsible })}
collapsed={collapsed}
onClick={toggleCollapse}
aria-label={menuAriaLabel}
>
{collapsible ? (
<Icon className="is-medium" collapsed={isCollapsed}>
{isCollapsible ? (
<Icon className="is-medium" collapsed={collapsed}>
{arrowIcon}
</Icon>
) : null}
{isCollapsed ? "" : label}
{collapsed ? "" : label}
</MenuLabel>
<ul className="menu-list" onClick={uncollapseMenu}>
{children}
<ul className="menu-list">
<SecondaryNavigationContext.Provider value={true}>{children}</SecondaryNavigationContext.Provider>
</ul>
</div>
</SectionContainer>

View File

@@ -0,0 +1,26 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
export const SecondaryNavigationContext = React.createContext(false);

View File

@@ -21,13 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React, { FC, useEffect } from "react";
import { Link } from "react-router-dom";
import classNames from "classnames";
import useMenuContext from "./MenuContext";
import { useSecondaryNavigation } from "@scm-manager/ui-components";
import { RoutingProps } from "./RoutingProps";
import useActiveMatch from "./useActiveMatch";
import { createAttributesForTesting } from "../devBuild";
import { SubNavigationContext } from "./SubNavigationContext";
type Props = RoutingProps & {
label: string;
@@ -46,9 +47,7 @@ const SubNavigation: FC<Props> = ({
children,
testId,
}) => {
const context = useMenuContext();
const collapsed = context.isCollapsed();
const { collapsed, setCollapsible } = useSecondaryNavigation();
const parents = to.split("/");
parents.splice(-1, 1);
const parent = parents.join("/");
@@ -59,6 +58,12 @@ const SubNavigation: FC<Props> = ({
activeWhenMatch,
});
useEffect(() => {
if (active) {
setCollapsible(false);
}
}, [active, setCollapsible]);
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
@@ -70,16 +75,18 @@ const SubNavigation: FC<Props> = ({
}
return (
<li title={collapsed ? title : undefined}>
<Link
className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
{...createAttributesForTesting(testId)}
>
<i className={classNames(defaultIcon, "fa-fw")} /> {collapsed ? "" : label}
</Link>
{childrenList}
</li>
<SubNavigationContext.Provider value={true}>
<li title={collapsed ? title : undefined}>
<Link
className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
{...createAttributesForTesting(testId)}
>
<i className={classNames(defaultIcon, "fa-fw")} /> {collapsed ? "" : label}
</Link>
{childrenList}
</li>
</SubNavigationContext.Provider>
);
};

View File

@@ -0,0 +1,26 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
export const SubNavigationContext = React.createContext(false);

View File

@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { useLocalStorage } from "@scm-manager/ui-api";
import { useCallback, useMemo } from "react";
export const useSecondaryNavigation = (isNavigationCollapsible = true) => {
const [isCollapsed, setCollapsed] = useLocalStorage<boolean>("secondaryNavigation.collapsed", false);
const [isRouteCollapsible, setRouteCollapsible] = useLocalStorage<boolean>("secondaryNavigation.collapsible", true);
const collapsible = useMemo(
() => isRouteCollapsible && isNavigationCollapsible,
[isNavigationCollapsible, isRouteCollapsible]
);
const collapsed = useMemo(() => collapsible && isCollapsed, [collapsible, isCollapsed]);
const toggleCollapse = useCallback(() => {
if (collapsible) {
setCollapsed((previousIsCollapsed) => !previousIsCollapsed);
}
}, [collapsible, setCollapsed]);
return useMemo(
() => ({
collapsed,
collapsible,
setCollapsible: setRouteCollapsible,
toggleCollapse,
}),
[collapsed, collapsible, setRouteCollapsible, toggleCollapse]
);
};

View File

@@ -32,9 +32,8 @@ import {
PrimaryContentColumn,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls
urls,
} from "@scm-manager/ui-components";
import AdminDetails from "./AdminDetails";
import PluginsOverview from "../plugins/containers/PluginsOverview";
@@ -60,115 +59,113 @@ const Admin: FC = () => {
const url = urls.matchedUrlFromMatch(match);
const extensionProps = {
links,
url
url,
};
return (
<StateMenuContextProvider>
<Page>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`} exact component={AdminDetails} />
<Route path={`${url}/settings/general`} exact component={GlobalConfig} />
<Redirect exact from={`${url}/plugins`} to={`${url}/plugins/installed/`} />
<Route path={`${url}/plugins/installed`} exact>
<PluginsOverview installed={true} />
</Route>
<Route path={`${url}/plugins/installed/:page`} exact>
<PluginsOverview installed={true} />
</Route>
<Route path={`${url}/plugins/available`} exact>
<PluginsOverview installed={false} />
</Route>
<Route path={`${url}/plugins/available/:page`} exact>
<PluginsOverview installed={false} />
</Route>
<Route path={`${url}/role/:role`}>
<SingleRepositoryRole />
</Route>
<Route path={`${url}/roles`} exact>
<RepositoryRoles baseUrl={`${url}/roles`} />
</Route>
<Route path={`${url}/roles/create`}>
<CreateRepositoryRole />
</Route>
<Route path={`${url}/roles/:page`} exact>
<RepositoryRoles baseUrl={`${url}/roles`} />
</Route>
<ExtensionPoint<extensionPoints.AdminRoute> name="admin.route" props={extensionProps} renderAll={true} />
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("admin.menu.navigationLabel")}>
<Page>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`} exact component={AdminDetails} />
<Route path={`${url}/settings/general`} exact component={GlobalConfig} />
<Redirect exact from={`${url}/plugins`} to={`${url}/plugins/installed/`} />
<Route path={`${url}/plugins/installed`} exact>
<PluginsOverview installed={true} />
</Route>
<Route path={`${url}/plugins/installed/:page`} exact>
<PluginsOverview installed={true} />
</Route>
<Route path={`${url}/plugins/available`} exact>
<PluginsOverview installed={false} />
</Route>
<Route path={`${url}/plugins/available/:page`} exact>
<PluginsOverview installed={false} />
</Route>
<Route path={`${url}/role/:role`}>
<SingleRepositoryRole />
</Route>
<Route path={`${url}/roles`} exact>
<RepositoryRoles baseUrl={`${url}/roles`} />
</Route>
<Route path={`${url}/roles/create`}>
<CreateRepositoryRole />
</Route>
<Route path={`${url}/roles/:page`} exact>
<RepositoryRoles baseUrl={`${url}/roles`} />
</Route>
<ExtensionPoint<extensionPoints.AdminRoute> name="admin.route" props={extensionProps} renderAll={true} />
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("admin.menu.navigationLabel")}>
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("admin.menu.informationNavLink")}
title={t("admin.menu.informationNavLink")}
testId="admin-information-link"
/>
{(availablePluginsLink || installedPluginsLink) && (
<SubNavigation
to={`${url}/plugins/`}
icon="fas fa-puzzle-piece"
label={t("plugins.menu.pluginsNavLink")}
title={t("plugins.menu.pluginsNavLink")}
testId="admin-plugins-link"
>
{installedPluginsLink && (
<NavLink
to={`${url}/plugins/installed/`}
label={t("plugins.menu.installedNavLink")}
testId="admin-installed-plugins-link"
/>
)}
{availablePluginsLink && (
<NavLink
to={`${url}/plugins/available/`}
label={t("plugins.menu.availableNavLink")}
testId="admin-available-plugins-link"
/>
)}
</SubNavigation>
)}
<NavLink
to={`${url}/roles/`}
icon="fas fa-user-shield"
label={t("repositoryRole.navLink")}
title={t("repositoryRole.navLink")}
testId="admin-repository-role-link"
activeWhenMatch={matchesRoles}
activeOnlyWhenExact={false}
/>
<ExtensionPoint<extensionPoints.AdminNavigation>
name="admin.navigation"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("admin.menu.settingsNavLink")}
title={t("admin.menu.settingsNavLink")}
testId="admin-settings-link"
>
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("admin.menu.informationNavLink")}
title={t("admin.menu.informationNavLink")}
testId="admin-information-link"
to={`${url}/settings/general`}
label={t("admin.menu.generalNavLink")}
testId="admin-settings-general-link"
/>
{(availablePluginsLink || installedPluginsLink) && (
<SubNavigation
to={`${url}/plugins/`}
icon="fas fa-puzzle-piece"
label={t("plugins.menu.pluginsNavLink")}
title={t("plugins.menu.pluginsNavLink")}
testId="admin-plugins-link"
>
{installedPluginsLink && (
<NavLink
to={`${url}/plugins/installed/`}
label={t("plugins.menu.installedNavLink")}
testId="admin-installed-plugins-link"
/>
)}
{availablePluginsLink && (
<NavLink
to={`${url}/plugins/available/`}
label={t("plugins.menu.availableNavLink")}
testId="admin-available-plugins-link"
/>
)}
</SubNavigation>
)}
<NavLink
to={`${url}/roles/`}
icon="fas fa-user-shield"
label={t("repositoryRole.navLink")}
title={t("repositoryRole.navLink")}
testId="admin-repository-role-link"
activeWhenMatch={matchesRoles}
activeOnlyWhenExact={false}
/>
<ExtensionPoint<extensionPoints.AdminNavigation>
name="admin.navigation"
<ExtensionPoint<extensionPoints.AdminSetting>
name="admin.setting"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("admin.menu.settingsNavLink")}
title={t("admin.menu.settingsNavLink")}
testId="admin-settings-link"
>
<NavLink
to={`${url}/settings/general`}
label={t("admin.menu.generalNavLink")}
testId="admin-settings-general-link"
/>
<ExtensionPoint<extensionPoints.AdminSetting>
name="admin.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</StateMenuContextProvider>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
);
};

View File

@@ -32,7 +32,6 @@ import {
PrimaryContentColumn,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls,
} from "@scm-manager/ui-components";
@@ -76,67 +75,61 @@ const Profile: FC = () => {
};
return (
<StateMenuContextProvider>
<Page title={me.displayName}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact>
<ProfileInfo me={me} />
<Page title={me.displayName}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact>
<ProfileInfo me={me} />
</Route>
<Route path={`${url}/settings/theme`} exact>
<Theme />
</Route>
<Route path={`${url}/settings/accessibility`} exact>
<Accessibility />
</Route>
{mayChangePassword && (
<Route path={`${url}/settings/password`}>
<ChangeUserPassword me={me} />
</Route>
<Route path={`${url}/settings/theme`} exact>
<Theme />
)}
{canManagePublicKeys && (
<Route path={`${url}/settings/publicKeys`}>
<SetPublicKeys user={me} />
</Route>
<Route path={`${url}/settings/accessibility`} exact>
<Accessibility />
)}
{canManageApiKeys && (
<Route path={`${url}/settings/apiKeys`}>
<SetApiKeys user={me} />
</Route>
{mayChangePassword && (
<Route path={`${url}/settings/password`}>
<ChangeUserPassword me={me} />
</Route>
)}
{canManagePublicKeys && (
<Route path={`${url}/settings/publicKeys`}>
<SetPublicKeys user={me} />
</Route>
)}
{canManageApiKeys && (
<Route path={`${url}/settings/apiKeys`}>
<SetApiKeys user={me} />
</Route>
)}
<ExtensionPoint<extensionPoints.ProfileRoute>
name="profile.route"
props={extensionProps}
renderAll={true}
)}
<ExtensionPoint<extensionPoints.ProfileRoute> name="profile.route" props={extensionProps} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("profile.navigationLabel")}>
<NavLink
to={url}
icon="fas fa-info-circle"
label={t("profile.informationNavLink")}
title={t("profile.informationNavLink")}
/>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("profile.navigationLabel")}>
<NavLink
to={url}
icon="fas fa-info-circle"
label={t("profile.informationNavLink")}
title={t("profile.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/theme`}
label={t("profile.settingsNavLink")}
title={t("profile.settingsNavLink")}
>
<NavLink to={`${url}/settings/theme`} label={t("profile.theme.navLink")} />
<NavLink to={`${url}/settings/accessibility`} label={t("profile.accessibility.navLink")} />
{mayChangePassword && (
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
)}
<SetPublicKeysNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
<SetApiKeysNavLink user={me} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</StateMenuContextProvider>
<SubNavigation
to={`${url}/settings/theme`}
label={t("profile.settingsNavLink")}
title={t("profile.settingsNavLink")}
>
<NavLink to={`${url}/settings/theme`} label={t("profile.theme.navLink")} />
<NavLink to={`${url}/settings/accessibility`} label={t("profile.accessibility.navLink")} />
{mayChangePassword && (
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
)}
<SetPublicKeysNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
<SetApiKeysNavLink user={me} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
);
};

View File

@@ -34,9 +34,8 @@ import {
PrimaryContentColumn,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls
urls,
} from "@scm-manager/ui-components";
import { Details } from "./../components/table";
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
@@ -63,63 +62,61 @@ const SingleGroup: FC = () => {
const extensionProps = {
group,
url
url,
};
return (
<StateMenuContextProvider>
<Page title={group.name}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={escapedUrl} exact>
<Details group={group} />
</Route>
<Route path={`${escapedUrl}/settings/general`} exact>
<EditGroup group={group} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`} exact>
<SetGroupPermissions group={group} />
</Route>
<ExtensionPoint<extensionPoints.GroupRoute>
name="group.route"
props={{
group,
url: escapedUrl
}}
<Page title={group.name}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={escapedUrl} exact>
<Details group={group} />
</Route>
<Route path={`${escapedUrl}/settings/general`} exact>
<EditGroup group={group} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`} exact>
<SetGroupPermissions group={group} />
</Route>
<ExtensionPoint<extensionPoints.GroupRoute>
name="group.route"
props={{
group,
url: escapedUrl,
}}
renderAll={true}
/>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleGroup.menu.navigationLabel")}>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("singleGroup.menu.informationNavLink")}
title={t("singleGroup.menu.informationNavLink")}
/>
<ExtensionPoint<extensionPoints.GroupNavigation>
name="group.navigation"
props={extensionProps}
renderAll={true}
/>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleGroup.menu.navigationLabel")}>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("singleGroup.menu.informationNavLink")}
title={t("singleGroup.menu.informationNavLink")}
/>
<ExtensionPoint<extensionPoints.GroupNavigation>
name="group.navigation"
<SubNavigation
to={`${url}/settings/general`}
label={t("singleGroup.menu.settingsNavLink")}
title={t("singleGroup.menu.settingsNavLink")}
>
<EditGroupNavLink group={group} editUrl={`${url}/settings/general`} />
<SetPermissionsNavLink group={group} permissionsUrl={`${url}/settings/permissions`} />
<ExtensionPoint<extensionPoints.GroupSetting>
name="group.setting"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleGroup.menu.settingsNavLink")}
title={t("singleGroup.menu.settingsNavLink")}
>
<EditGroupNavLink group={group} editUrl={`${url}/settings/general`} />
<SetPermissionsNavLink group={group} permissionsUrl={`${url}/settings/permissions`} />
<ExtensionPoint<extensionPoints.GroupSetting>
name="group.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</StateMenuContextProvider>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
);
};

View File

@@ -24,6 +24,7 @@
import React from "react";
import EditRepoNavLink from "./EditRepoNavLink";
import { mount, shallow } from "@scm-manager/ui-tests";
import { LocalStorageProvider } from "@scm-manager/ui-api";
describe("GeneralNavLink", () => {
it("should render nothing, if the modify link is missing", () => {
@@ -31,7 +32,7 @@ describe("GeneralNavLink", () => {
namespace: "space",
name: "name",
type: "git",
_links: {}
_links: {},
};
const navLink = shallow(<EditRepoNavLink repository={repository} editUrl="" />);
@@ -45,12 +46,16 @@ describe("GeneralNavLink", () => {
type: "git",
_links: {
update: {
href: "/repositories"
}
}
href: "/repositories",
},
},
};
const navLink = mount(<EditRepoNavLink repository={repository} editUrl="" />);
const navLink = mount(
<LocalStorageProvider>
<EditRepoNavLink repository={repository} editUrl="" />
</LocalStorageProvider>
);
expect(navLink.text()).toBe("repositoryRoot.menu.generalNavLink");
});
});

View File

@@ -24,12 +24,13 @@
import React from "react";
import { mount, shallow } from "@scm-manager/ui-tests";
import "@scm-manager/ui-tests";
import { LocalStorageProvider } from "@scm-manager/ui-api";
import PermissionsNavLink from "./PermissionsNavLink";
describe("PermissionsNavLink", () => {
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {}
_links: {},
};
const navLink = shallow(<PermissionsNavLink repository={repository} permissionUrl="" />);
@@ -40,12 +41,16 @@ describe("PermissionsNavLink", () => {
const repository = {
_links: {
permissions: {
href: "/permissions"
}
}
href: "/permissions",
},
},
};
const navLink = mount(<PermissionsNavLink repository={repository} permissionUrl="" />);
const navLink = mount(
<LocalStorageProvider>
<PermissionsNavLink repository={repository} permissionUrl="" />
</LocalStorageProvider>
);
expect(navLink.text()).toBe("repositoryRoot.menu.permissionsNavLink");
});
});

View File

@@ -24,6 +24,7 @@
import React from "react";
import { mount, shallow } from "@scm-manager/ui-tests";
import "@scm-manager/ui-tests";
import { LocalStorageProvider } from "@scm-manager/ui-api";
import RepositoryNavLink from "./RepositoryNavLink";
describe("RepositoryNavLink", () => {
@@ -32,7 +33,7 @@ describe("RepositoryNavLink", () => {
namespace: "Namespace",
name: "Repo",
type: "GIT",
_links: {}
_links: {},
};
const navLink = shallow(
@@ -54,19 +55,21 @@ describe("RepositoryNavLink", () => {
type: "GIT",
_links: {
sources: {
href: "/sources"
}
}
href: "/sources",
},
},
};
const navLink = mount(
<RepositoryNavLink
repository={repository}
linkName="sources"
to="/sources"
label="Sources"
activeOnlyWhenExact={true}
/>
<LocalStorageProvider>
<RepositoryNavLink
repository={repository}
linkName="sources"
to="/sources"
label="Sources"
activeOnlyWhenExact={true}
/>
</LocalStorageProvider>
);
expect(navLink.text()).toBe("Sources");
});

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC } from "react";
import { Repository } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { RouteProps } from "react-router-dom";
@@ -40,16 +40,12 @@ type Props = {
/**
* Component renders only if the repository contains the link with the given name.
*/
class RepositoryNavLink extends React.Component<Props> {
render() {
const { repository, linkName } = this.props;
if (!repository._links[linkName]) {
return null;
}
return <NavLink {...this.props} />;
const RepositoryNavLink: FC<Props> = ({ repository, linkName, ...props }) => {
if (!repository._links[linkName]) {
return null;
}
}
return <NavLink {...props} />;
};
export default RepositoryNavLink;

View File

@@ -41,7 +41,6 @@ import {
RepositoryFlags,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls,
} from "@scm-manager/ui-components";
@@ -269,153 +268,151 @@ const RepositoryRoot = () => {
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<StateMenuContextProvider>
<RepositoryContextProvider repository={repository}>
<Page
title={titleComponent}
documentTitle={`${repository.namespace}/${repository.name}`}
afterTitle={
<MobileWrapped className="is-flex is-align-items-center">
<ExtensionPoint name="repository.afterTitle" props={{ repository }} />
<TagGroup className="has-text-weight-bold">
<RepositoryFlags repository={repository} tooltipLocation="bottom" />
</TagGroup>
</MobileWrapped>
}
>
{modal}
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={urls.escapeUrlForRoute(match.url)} to={urls.escapeUrlForRoute(redirectedUrl)} />
<RepositoryContextProvider repository={repository}>
<Page
title={titleComponent}
documentTitle={`${repository.namespace}/${repository.name}`}
afterTitle={
<MobileWrapped className="is-flex is-align-items-center">
<ExtensionPoint name="repository.afterTitle" props={{ repository }} />
<TagGroup className="has-text-weight-bold">
<RepositoryFlags repository={repository} tooltipLocation="bottom" />
</TagGroup>
</MobileWrapped>
}
>
{modal}
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={urls.escapeUrlForRoute(match.url)} to={urls.escapeUrlForRoute(redirectedUrl)} />
{/* redirect pre 2.0.0-rc2 links */}
<Redirect from={`${escapedUrl}/changeset/:id`} to={`${url}/code/changeset/:id`} />
<Redirect exact from={`${escapedUrl}/sources`} to={`${url}/code/sources`} />
<Redirect from={`${escapedUrl}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} />
<Redirect exact from={`${escapedUrl}/changesets`} to={`${url}/code/changesets`} />
<Redirect
from={`${escapedUrl}/branch/:branch/changesets`}
to={`${url}/code/branch/:branch/changesets/`}
/>
{/* redirect pre 2.0.0-rc2 links */}
<Redirect from={`${escapedUrl}/changeset/:id`} to={`${url}/code/changeset/:id`} />
<Redirect exact from={`${escapedUrl}/sources`} to={`${url}/code/sources`} />
<Redirect from={`${escapedUrl}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} />
<Redirect exact from={`${escapedUrl}/changesets`} to={`${url}/code/changesets`} />
<Redirect
from={`${escapedUrl}/branch/:branch/changesets`}
to={`${url}/code/branch/:branch/changesets/`}
/>
<Route path={`${escapedUrl}/info`} exact>
<RepositoryDetails repository={repository} />
</Route>
<Route path={`${escapedUrl}/settings/general`}>
<EditRepo repository={repository} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`}>
<Permissions namespaceOrRepository={repository} />
</Route>
<Route exact path={`${escapedUrl}/code/changeset/:id`}>
<ChangesetView repository={repository} fileControlFactoryFactory={fileControlFactoryFactory} />
</Route>
<Route path={`${escapedUrl}/code/sourceext/:extension`} exact={true}>
<SourceExtensions repository={repository} />
</Route>
<Route path={`${escapedUrl}/code/sourceext/:extension/:revision/:path*`}>
<SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />
</Route>
<Route path={`${escapedUrl}/code`}>
<CodeOverview baseUrl={`${url}/code`} repository={repository} />
</Route>
<Route path={`${escapedUrl}/branch/:branch`}>
<BranchRoot repository={repository} />
</Route>
<Route path={`${escapedUrl}/branches`} exact={true}>
<BranchesOverview repository={repository} baseUrl={`${url}/branch`} />
</Route>
<Route path={`${escapedUrl}/branches/create`}>
<CreateBranch repository={repository} />
</Route>
<Route path={`${escapedUrl}/tag/:tag`}>
<TagRoot repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${escapedUrl}/tags`} exact={true}>
<TagsOverview repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${escapedUrl}/compare/:sourceType/:sourceName`}>
<CompareRoot repository={repository} baseUrl={`${url}/compare`} />
</Route>
<ExtensionPoint<extensionPoints.RepositoryRoute>
name="repository.route"
props={{
repository,
url: urls.escapeUrlForRoute(url),
indexLinks,
}}
renderAll={true}
/>
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("repositoryRoot.menu.navigationLabel")}>
<ExtensionPoint<extensionPoints.RepositoryNavigationTopLevel>
name="repository.navigation.topLevel"
<Route path={`${escapedUrl}/info`} exact>
<RepositoryDetails repository={repository} />
</Route>
<Route path={`${escapedUrl}/settings/general`}>
<EditRepo repository={repository} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`}>
<Permissions namespaceOrRepository={repository} />
</Route>
<Route exact path={`${escapedUrl}/code/changeset/:id`}>
<ChangesetView repository={repository} fileControlFactoryFactory={fileControlFactoryFactory} />
</Route>
<Route path={`${escapedUrl}/code/sourceext/:extension`} exact={true}>
<SourceExtensions repository={repository} />
</Route>
<Route path={`${escapedUrl}/code/sourceext/:extension/:revision/:path*`}>
<SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />
</Route>
<Route path={`${escapedUrl}/code`}>
<CodeOverview baseUrl={`${url}/code`} repository={repository} />
</Route>
<Route path={`${escapedUrl}/branch/:branch`}>
<BranchRoot repository={repository} />
</Route>
<Route path={`${escapedUrl}/branches`} exact={true}>
<BranchesOverview repository={repository} baseUrl={`${url}/branch`} />
</Route>
<Route path={`${escapedUrl}/branches/create`}>
<CreateBranch repository={repository} />
</Route>
<Route path={`${escapedUrl}/tag/:tag`}>
<TagRoot repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${escapedUrl}/tags`} exact={true}>
<TagsOverview repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${escapedUrl}/compare/:sourceType/:sourceName`}>
<CompareRoot repository={repository} baseUrl={`${url}/compare`} />
</Route>
<ExtensionPoint<extensionPoints.RepositoryRoute>
name="repository.route"
props={{
repository,
url: urls.escapeUrlForRoute(url),
indexLinks,
}}
renderAll={true}
/>
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("repositoryRoot.menu.navigationLabel")}>
<ExtensionPoint<extensionPoints.RepositoryNavigationTopLevel>
name="repository.navigation.topLevel"
props={extensionProps}
renderAll={true}
/>
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("repositoryRoot.menu.informationNavLink")}
title={t("repositoryRoot.menu.informationNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="branches"
to={`${url}/branches/`}
icon="fas fa-code-branch"
label={t("repositoryRoot.menu.branchesNavLink")}
activeWhenMatch={matchesBranches}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="tags"
to={`${url}/tags/`}
icon="fas fa-tags"
label={t("repositoryRoot.menu.tagsNavLink")}
activeWhenMatch={matchesTags}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.tagsNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName={codeLinkname}
to={evaluateDestinationForCodeLink()}
icon="fas fa-code"
label={t("repositoryRoot.menu.sourcesNavLink")}
activeWhenMatch={matchesCode}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.sourcesNavLink")}
/>
<ExtensionPoint<extensionPoints.RepositoryNavigation>
name="repository.navigation"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("repositoryRoot.menu.settingsNavLink")}
title={t("repositoryRoot.menu.settingsNavLink")}
>
<EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} />
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} />
<ExtensionPoint<extensionPoints.RepositorySetting>
name="repository.setting"
props={extensionProps}
renderAll={true}
/>
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("repositoryRoot.menu.informationNavLink")}
title={t("repositoryRoot.menu.informationNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="branches"
to={`${url}/branches/`}
icon="fas fa-code-branch"
label={t("repositoryRoot.menu.branchesNavLink")}
activeWhenMatch={matchesBranches}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="tags"
to={`${url}/tags/`}
icon="fas fa-tags"
label={t("repositoryRoot.menu.tagsNavLink")}
activeWhenMatch={matchesTags}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.tagsNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName={codeLinkname}
to={evaluateDestinationForCodeLink()}
icon="fas fa-code"
label={t("repositoryRoot.menu.sourcesNavLink")}
activeWhenMatch={matchesCode}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.sourcesNavLink")}
/>
<ExtensionPoint<extensionPoints.RepositoryNavigation>
name="repository.navigation"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("repositoryRoot.menu.settingsNavLink")}
title={t("repositoryRoot.menu.settingsNavLink")}
>
<EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} />
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} />
<ExtensionPoint<extensionPoints.RepositorySetting>
name="repository.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</RepositoryContextProvider>
</StateMenuContextProvider>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</RepositoryContextProvider>
);
};

View File

@@ -33,7 +33,6 @@ import {
PrimaryContentColumn,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls,
} from "@scm-manager/ui-components";
@@ -78,46 +77,44 @@ const NamespaceRoot: FC = () => {
};
return (
<StateMenuContextProvider>
<Page title={namespace.namespace}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={`${url}/settings`} to={`${url}/settings/permissions`} />
<Route path={`${url}/settings/permissions`}>
<Permissions namespaceOrRepository={namespace} />
</Route>
<ExtensionPoint<extensionPoints.NamespaceRoute>
name="namespace.route"
<Page title={namespace.namespace}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={`${url}/settings`} to={`${url}/settings/permissions`} />
<Route path={`${url}/settings/permissions`}>
<Permissions namespaceOrRepository={namespace} />
</Route>
<ExtensionPoint<extensionPoints.NamespaceRoute>
name="namespace.route"
props={extensionProps}
renderAll={true}
/>
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("namespaceRoot.menu.navigationLabel")}>
<ExtensionPoint<extensionPoints.NamespaceTopLevelNavigation>
name="namespace.navigation.topLevel"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings`}
label={t("namespaceRoot.menu.settingsNavLink")}
title={t("namespaceRoot.menu.settingsNavLink")}
>
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} namespace={namespace} />
<ExtensionPoint<extensionPoints.NamespaceSetting>
name="namespace.setting"
props={extensionProps}
renderAll={true}
/>
</Switch>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("namespaceRoot.menu.navigationLabel")}>
<ExtensionPoint<extensionPoints.NamespaceTopLevelNavigation>
name="namespace.navigation.topLevel"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings`}
label={t("namespaceRoot.menu.settingsNavLink")}
title={t("namespaceRoot.menu.settingsNavLink")}
>
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} namespace={namespace} />
<ExtensionPoint<extensionPoints.NamespaceSetting>
name="namespace.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</StateMenuContextProvider>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
);
};

View File

@@ -33,9 +33,8 @@ import {
PrimaryContentColumn,
SecondaryNavigation,
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls
urls,
} from "@scm-manager/ui-components";
import { Details } from "./../components/table";
import EditUser from "./EditUser";
@@ -44,7 +43,7 @@ import {
SetApiKeysNavLink,
SetPasswordNavLink,
SetPermissionsNavLink,
SetPublicKeysNavLink
SetPublicKeysNavLink,
} from "./../components/navLinks";
import { useTranslation } from "react-i18next";
import SetUserPassword from "../components/SetUserPassword";
@@ -71,70 +70,72 @@ const SingleUser: FC = () => {
const extensionProps = {
user,
url
url,
};
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<StateMenuContextProvider>
<Page title={user.displayName}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={escapedUrl} exact>
<Details user={user} />
</Route>
<Route path={`${escapedUrl}/settings/general`}>
<EditUser user={user} />
</Route>
<Route path={`${escapedUrl}/settings/password`}>
<SetUserPassword user={user} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`}>
<SetUserPermissions user={user} />
</Route>
<Route path={`${escapedUrl}/settings/publickeys`}>
<SetPublicKeys user={user} />
</Route>
<Route path={`${escapedUrl}/settings/apiKeys`}>
<SetApiKeys user={user} />
</Route>
<ExtensionPoint<extensionPoints.UserRoute> name="user.route" props={{
<Page title={user.displayName}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={escapedUrl} exact>
<Details user={user} />
</Route>
<Route path={`${escapedUrl}/settings/general`}>
<EditUser user={user} />
</Route>
<Route path={`${escapedUrl}/settings/password`}>
<SetUserPassword user={user} />
</Route>
<Route path={`${escapedUrl}/settings/permissions`}>
<SetUserPermissions user={user} />
</Route>
<Route path={`${escapedUrl}/settings/publickeys`}>
<SetPublicKeys user={user} />
</Route>
<Route path={`${escapedUrl}/settings/apiKeys`}>
<SetApiKeys user={user} />
</Route>
<ExtensionPoint<extensionPoints.UserRoute>
name="user.route"
props={{
user,
url: escapedUrl
}} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleUser.menu.navigationLabel")}>
<NavLink
to={url}
icon="fas fa-info-circle"
label={t("singleUser.menu.informationNavLink")}
title={t("singleUser.menu.informationNavLink")}
testId="user-information-link"
url: escapedUrl,
}}
renderAll={true}
/>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleUser.menu.navigationLabel")}>
<NavLink
to={url}
icon="fas fa-info-circle"
label={t("singleUser.menu.informationNavLink")}
title={t("singleUser.menu.informationNavLink")}
testId="user-information-link"
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
title={t("singleUser.menu.settingsNavLink")}
testId="user-settings-link"
>
<EditUserNavLink user={user} editUrl={`${url}/settings/general`} />
<SetPasswordNavLink user={user} passwordUrl={`${url}/settings/password`} />
<SetPermissionsNavLink user={user} permissionsUrl={`${url}/settings/permissions`} />
<SetPublicKeysNavLink user={user} publicKeyUrl={`${url}/settings/publickeys`} />
<SetApiKeysNavLink user={user} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint<extensionPoints.UserSetting>
name="user.setting"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
title={t("singleUser.menu.settingsNavLink")}
testId="user-settings-link"
>
<EditUserNavLink user={user} editUrl={`${url}/settings/general`} />
<SetPasswordNavLink user={user} passwordUrl={`${url}/settings/password`} />
<SetPermissionsNavLink user={user} permissionsUrl={`${url}/settings/permissions`} />
<SetPublicKeysNavLink user={user} publicKeyUrl={`${url}/settings/publickeys`} />
<SetApiKeysNavLink user={user} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint<extensionPoints.UserSetting>
name="user.setting"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
</StateMenuContextProvider>
</SubNavigation>
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>
</Page>
);
};