mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-02 02:10:53 +01:00
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:
2
gradle/changelog/secondary_navigation_status.yaml
Normal file
2
gradle/changelog/secondary_navigation_status.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Save collapse status of secondary navigation
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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={{}} />;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,9 @@ export type MenuContext = {
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const MenuContext = React.createContext<MenuContext>({
|
||||
isCollapsed() {
|
||||
return false;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
26
scm-ui/ui-components/src/navigation/SubNavigationContext.ts
Normal file
26
scm-ui/ui-components/src/navigation/SubNavigationContext.ts
Normal 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);
|
||||
52
scm-ui/ui-components/src/useSecondaryNavigation.tsx
Normal file
52
scm-ui/ui-components/src/useSecondaryNavigation.tsx
Normal 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]
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user