diff --git a/CHANGELOG.md b/CHANGELOG.md index 846b69b283..97dff642cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed +- Simplified collapse state management of the secondary navigation ([#1086](https://github.com/scm-manager/scm-manager/pull/1086) + ### Fixed - Authentication for write requests for repositories with anonymous read access ([#108](https://github.com/scm-manager/scm-manager/pull/1081)) + ## 2.0.0-rc6 - 2020-03-26 ### Added - Extension point to add links to the repository cards from plug ins ([#1041](https://github.com/scm-manager/scm-manager/pull/1041)) diff --git a/scm-ui/tsconfig/tsconfig.json b/scm-ui/tsconfig/tsconfig.json index a2450080eb..1c3acc4351 100644 --- a/scm-ui/tsconfig/tsconfig.json +++ b/scm-ui/tsconfig/tsconfig.json @@ -59,6 +59,10 @@ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "skipLibCheck": true + "skipLibCheck": true, + "lib": [ + "dom", + "es2019" + ] } } diff --git a/scm-ui/ui-components/.storybook/config.js b/scm-ui/ui-components/.storybook/config.js index bfaea0245c..ae409e6478 100644 --- a/scm-ui/ui-components/.storybook/config.js +++ b/scm-ui/ui-components/.storybook/config.js @@ -70,7 +70,4 @@ addDecorator( }) ); -const RoutingDecorator = (story) => {story()}; -addDecorator(RoutingDecorator); - configure(require.context("../src", true, /\.stories\.tsx?$/), module); diff --git a/scm-ui/ui-components/src/CardColumn.tsx b/scm-ui/ui-components/src/CardColumn.tsx index 0ff63e31dd..42c1512690 100644 --- a/scm-ui/ui-components/src/CardColumn.tsx +++ b/scm-ui/ui-components/src/CardColumn.tsx @@ -99,9 +99,7 @@ export default class CardColumn extends React.Component {
-

- {title} -

+

{title}

{description}

{contentRight} diff --git a/scm-ui/ui-components/src/DateFromNow.test.ts b/scm-ui/ui-components/src/DateFromNow.test.ts index 8571864639..19acc08fda 100644 --- a/scm-ui/ui-components/src/DateFromNow.test.ts +++ b/scm-ui/ui-components/src/DateFromNow.test.ts @@ -25,7 +25,6 @@ import { chooseLocale, supportedLocales } from "./DateFromNow"; describe("test choose locale", () => { - it("should choose de", () => { const locale = chooseLocale("de_DE", ["de", "en"]); expect(locale).toBe(supportedLocales.de); @@ -45,5 +44,4 @@ describe("test choose locale", () => { const locale = chooseLocale("af", ["af", "be"]); expect(locale).toBe(supportedLocales.en); }); - }); diff --git a/scm-ui/ui-components/src/MarkdownView.stories.tsx b/scm-ui/ui-components/src/MarkdownView.stories.tsx index 464efe42cc..e43f9afd75 100644 --- a/scm-ui/ui-components/src/MarkdownView.stories.tsx +++ b/scm-ui/ui-components/src/MarkdownView.stories.tsx @@ -32,12 +32,14 @@ import MarkdownXmlCodeBlock from "./__resources__/markdown-xml-codeblock.md"; import MarkdownInlineXml from "./__resources__/markdown-inline-xml.md"; import Title from "./layout/Title"; import { Subtitle } from "./layout"; +import { MemoryRouter } from "react-router-dom"; const Spacing = styled.div` padding: 2em; `; storiesOf("MarkdownView", module) + .addDecorator(story => {story()}) .addDecorator(story => {story()}) .add("Default", () => ) .add("Code without Lang", () => ) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 77aec20677..0417014428 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -34188,6 +34188,218 @@ exports[`Storyshots MarkdownView Xml Code Block 1`] = `
`; +exports[`Storyshots Navigation|Secondary Default 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Navigation|Secondary Extension Point 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Navigation|Secondary Sub Navigation 1`] = ` +
+
+ +
+
+`; + exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
ReactNode) => {story()}; +const RoutingDecorator = (story: () => ReactNode) => {story()}; storiesOf("Buttons|Button", module) + .addDecorator(RoutingDecorator) .add("Colors", () => (
{colors.map(color => ( @@ -70,6 +73,7 @@ storiesOf("Buttons|Button", module) const buttonStory = (name: string, storyFn: () => ReactElement) => { return storiesOf("Buttons|" + name, module) + .addDecorator(RoutingDecorator) .addDecorator(SpacingDecorator) .add("Default", storyFn); }; diff --git a/scm-ui/ui-components/src/forms/Checkbox.stories.tsx b/scm-ui/ui-components/src/forms/Checkbox.stories.tsx index 32112d97a1..9f714e5af0 100644 --- a/scm-ui/ui-components/src/forms/Checkbox.stories.tsx +++ b/scm-ui/ui-components/src/forms/Checkbox.stories.tsx @@ -42,4 +42,3 @@ storiesOf("Forms|Checkbox", module) )); - diff --git a/scm-ui/ui-components/src/forms/Checkbox.tsx b/scm-ui/ui-components/src/forms/Checkbox.tsx index 00f6285423..70888cf483 100644 --- a/scm-ui/ui-components/src/forms/Checkbox.tsx +++ b/scm-ui/ui-components/src/forms/Checkbox.tsx @@ -54,7 +54,7 @@ export default class Checkbox extends React.Component { if (title) { return ; } - } + }; render() { const { label, checked, disabled } = this.props; @@ -68,13 +68,7 @@ export default class Checkbox extends React.Component { but bulma does. // @ts-ignore */}
diff --git a/scm-ui/ui-components/src/forms/Radio.tsx b/scm-ui/ui-components/src/forms/Radio.tsx index d05e40a19a..fe6a35f4a6 100644 --- a/scm-ui/ui-components/src/forms/Radio.tsx +++ b/scm-ui/ui-components/src/forms/Radio.tsx @@ -25,7 +25,6 @@ import React, { ChangeEvent } from "react"; import { Help } from "../index"; import styled from "styled-components"; - const StyledRadio = styled.label` margin-right: 0.5em; `; diff --git a/scm-ui/ui-components/src/forms/Textarea.stories.tsx b/scm-ui/ui-components/src/forms/Textarea.stories.tsx index 2698195519..bcb638cbaa 100644 --- a/scm-ui/ui-components/src/forms/Textarea.stories.tsx +++ b/scm-ui/ui-components/src/forms/Textarea.stories.tsx @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, {useState} from "react"; +import React, { useState } from "react"; import { storiesOf } from "@storybook/react"; import styled from "styled-components"; import Textarea from "./Textarea"; diff --git a/scm-ui/ui-components/src/layout/Footer.stories.tsx b/scm-ui/ui-components/src/layout/Footer.stories.tsx index 02738f68c0..c25b618a87 100644 --- a/scm-ui/ui-components/src/layout/Footer.stories.tsx +++ b/scm-ui/ui-components/src/layout/Footer.stories.tsx @@ -33,6 +33,7 @@ import hitchhiker from "../__resources__/hitchhiker.png"; import marvin from "../__resources__/marvin.jpg"; import NavLink from "../navigation/NavLink"; import ExternalLink from "../navigation/ExternalLink"; +import { MemoryRouter } from "react-router-dom"; const trillian: Me = { name: "trillian", @@ -64,6 +65,7 @@ const withBinder = (binder: Binder) => { }; storiesOf("Layout|Footer", module) + .addDecorator(story => {story()}) .add("Default", () => { return
; }) diff --git a/scm-ui/ui-components/src/layout/Page.tsx b/scm-ui/ui-components/src/layout/Page.tsx index 5d14d1af46..0f4b4e7b4f 100644 --- a/scm-ui/ui-components/src/layout/Page.tsx +++ b/scm-ui/ui-components/src/layout/Page.tsx @@ -52,7 +52,7 @@ const PageActionContainer = styled.div` `; const MarginLeft = styled.div` -margin-left: 0.5rem; + margin-left: 0.5rem; `; const FlexContainer = styled.div` diff --git a/scm-ui/ui-components/src/layout/PrimaryContentColumn.tsx b/scm-ui/ui-components/src/layout/PrimaryContentColumn.tsx index 76427dc4e0..73ddadd0a4 100644 --- a/scm-ui/ui-components/src/layout/PrimaryContentColumn.tsx +++ b/scm-ui/ui-components/src/layout/PrimaryContentColumn.tsx @@ -21,13 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { ReactNode } from "react"; +import React, { FC } from "react"; import styled from "styled-components"; - -type Props = { - children?: ReactNode; - collapsed: boolean; -}; +import useMenuContext from "../navigation/MenuContext"; const PrimaryColumn = styled.div<{ collapsed: boolean }>` /* This is the counterpart to the specific column in SecondaryNavigationColumn. */ @@ -39,18 +35,13 @@ const PrimaryColumn = styled.div<{ collapsed: boolean }>` } `; -export default class PrimaryContentColumn extends React.Component { - static defaultProps = { - collapsed: false - }; +const PrimaryContentColumn: FC = ({ children }) => { + const context = useMenuContext(); + return ( + + {children} + + ); +}; - render() { - const { children, collapsed } = this.props; - - return ( - - {children} - - ); - } -} +export default PrimaryContentColumn; diff --git a/scm-ui/ui-components/src/layout/SecondaryNavigationColumn.tsx b/scm-ui/ui-components/src/layout/SecondaryNavigationColumn.tsx index aae722bbe3..a974c8d438 100644 --- a/scm-ui/ui-components/src/layout/SecondaryNavigationColumn.tsx +++ b/scm-ui/ui-components/src/layout/SecondaryNavigationColumn.tsx @@ -21,13 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { ReactNode } from "react"; +import React, { FC } from "react"; import styled from "styled-components"; - -type Props = { - children?: ReactNode; - collapsed: boolean; -}; +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. @@ -42,18 +38,13 @@ const SecondaryColumn = styled.div<{ collapsed: boolean }>` } `; -export default class SecondaryNavigationColumn extends React.Component { - static defaultProps = { - collapsed: false - }; +const SecondaryNavigationColumn: FC = ({ children }) => { + const context = useMenuContext(); + return ( + + {children} + + ); +}; - render() { - const { children, collapsed } = this.props; - - return ( - - {children} - - ); - } -} +export default SecondaryNavigationColumn; diff --git a/scm-ui/ui-components/src/layout/Title.tsx b/scm-ui/ui-components/src/layout/Title.tsx index cd040be936..0e1024ea06 100644 --- a/scm-ui/ui-components/src/layout/Title.tsx +++ b/scm-ui/ui-components/src/layout/Title.tsx @@ -40,7 +40,7 @@ const Title: FC = ({ title, preventRefreshingPageTitle, customPageTitle, document.title = title; } } - },[title, preventRefreshingPageTitle, customPageTitle]); + }, [title, preventRefreshingPageTitle, customPageTitle]); if (title) { return

{title}

; diff --git a/scm-ui/ui-components/src/layout/index.ts b/scm-ui/ui-components/src/layout/index.ts index 2c9f7fba72..f1876d4ae1 100644 --- a/scm-ui/ui-components/src/layout/index.ts +++ b/scm-ui/ui-components/src/layout/index.ts @@ -34,4 +34,3 @@ export { default as Title } from "./Title"; export { default as CustomQueryFlexWrappedColumns } from "./CustomQueryFlexWrappedColumns"; export { default as PrimaryContentColumn } from "./PrimaryContentColumn"; export { default as SecondaryNavigationColumn } from "./SecondaryNavigationColumn"; - diff --git a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx index b20f971cc2..e8671a77cb 100644 --- a/scm-ui/ui-components/src/modals/ConfirmAlert.tsx +++ b/scm-ui/ui-components/src/modals/ConfirmAlert.tsx @@ -62,7 +62,11 @@ class ConfirmAlert extends React.Component {
{buttons.map((button, i) => (

- this.handleClickButton(button)}> + this.handleClickButton(button)} + > {button.label}

diff --git a/scm-ui/ui-components/src/navigation/ExternalLink.tsx b/scm-ui/ui-components/src/navigation/ExternalLink.tsx index a87b118ee3..1565f6fa79 100644 --- a/scm-ui/ui-components/src/navigation/ExternalLink.tsx +++ b/scm-ui/ui-components/src/navigation/ExternalLink.tsx @@ -30,6 +30,8 @@ type Props = { label: string; }; +// TODO is it used in the menu? should it use MenuContext for collapse state? + const ExternalLink: FC = ({ to, icon, label }) => { let showIcon; if (icon) { diff --git a/scm-ui/ui-components/src/navigation/MenuContext.tsx b/scm-ui/ui-components/src/navigation/MenuContext.tsx new file mode 100644 index 0000000000..f02e5ab8d2 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/MenuContext.tsx @@ -0,0 +1,56 @@ +/* + * 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, { FC, useContext, useState } from "react"; + +export type MenuContext = { + isCollapsed: () => boolean; + setCollapsed: (collapsed: boolean) => void; +}; + +export const MenuContext = React.createContext({ + isCollapsed() { + return false; + }, + setCollapsed() {} +}); + +export const StateMenuContextProvider: FC = ({ children }) => { + const [collapsed, setCollapsed] = useState(false); + + const context = { + isCollapsed() { + return collapsed; + }, + setCollapsed + }; + + return {children}; +}; + +const useMenuContext = () => { + return useContext(MenuContext); +}; + +export default useMenuContext; diff --git a/scm-ui/ui-components/src/navigation/NavAction.tsx b/scm-ui/ui-components/src/navigation/NavAction.tsx index dd350d1c71..394b30d961 100644 --- a/scm-ui/ui-components/src/navigation/NavAction.tsx +++ b/scm-ui/ui-components/src/navigation/NavAction.tsx @@ -29,6 +29,8 @@ type Props = { action: () => void; }; +// TODO is it used in the menu? should it use MenuContext for collapse state? + class NavAction extends React.Component { render() { const { label, icon, action } = this.props; diff --git a/scm-ui/ui-components/src/navigation/NavLink.tsx b/scm-ui/ui-components/src/navigation/NavLink.tsx index 7a527e5ab5..d9cb3c4e7e 100644 --- a/scm-ui/ui-components/src/navigation/NavLink.tsx +++ b/scm-ui/ui-components/src/navigation/NavLink.tsx @@ -23,60 +23,47 @@ */ import * as React from "react"; import classNames from "classnames"; -import { Link, Route } from "react-router-dom"; +import { Link, useRouteMatch } from "react-router-dom"; +import { RoutingProps } from "./RoutingProps"; +import { FC } from "react"; +import useMenuContext from "./MenuContext"; -// TODO mostly copy of PrimaryNavigationLink - -type Props = { - to: string; - icon?: string; +type Props = RoutingProps & { label: string; - activeOnlyWhenExact?: boolean; - activeWhenMatch?: (route: any) => boolean; - collapsed?: boolean; title?: string; + icon?: string; }; -class NavLink extends React.Component { - static defaultProps = { - activeOnlyWhenExact: true - }; +const NavLink: FC = ({ to, activeOnlyWhenExact, icon, label, title }) => { + const match = useRouteMatch({ + path: to, + exact: activeOnlyWhenExact + }); - isActive(route: any) { - const { activeWhenMatch } = this.props; - return route.match || (activeWhenMatch && activeWhenMatch(route)); - } + const context = useMenuContext(); + const collapsed = context.isCollapsed(); - renderLink = (route: any) => { - const { to, icon, label, collapsed, title } = this.props; - - let showIcon = null; - if (icon) { - showIcon = ( - <> - {" "} - - ); - } - - return ( -
  • - - {showIcon} - {collapsed ? null : label} - -
  • + let showIcon = null; + if (icon) { + showIcon = ( + <> + {" "} + ); - }; - - render() { - const { to, activeOnlyWhenExact } = this.props; - - return ; } -} + + return ( +
  • + + {showIcon} + {collapsed ? null : label} + +
  • + ); +}; + +NavLink.defaultProps = { + activeOnlyWhenExact: true +}; export default NavLink; diff --git a/scm-ui/ui-components/src/navigation/Navigation.tsx b/scm-ui/ui-components/src/navigation/Navigation.tsx index dc848b4d24..0ccd94e7f7 100644 --- a/scm-ui/ui-components/src/navigation/Navigation.tsx +++ b/scm-ui/ui-components/src/navigation/Navigation.tsx @@ -27,6 +27,8 @@ type Props = { children?: ReactNode; }; +// TODO it is used? + class Navigation extends React.Component { render() { return ; diff --git a/scm-ui/ui-components/src/navigation/MenuContext.ts b/scm-ui/ui-components/src/navigation/RoutingProps.ts similarity index 73% rename from scm-ui/ui-components/src/navigation/MenuContext.ts rename to scm-ui/ui-components/src/navigation/RoutingProps.ts index e2760da025..20f9d8d900 100644 --- a/scm-ui/ui-components/src/navigation/MenuContext.ts +++ b/scm-ui/ui-components/src/navigation/RoutingProps.ts @@ -22,18 +22,8 @@ * SOFTWARE. */ -import React from "react"; - -const MENU_COLLAPSED = "secondary-menu-collapsed"; - -export const MenuContext = React.createContext({ - menuCollapsed: isMenuCollapsed(), - setMenuCollapsed: (collapsed: boolean) => {} -}); - -export function isMenuCollapsed() { - return localStorage.getItem(MENU_COLLAPSED) === "true"; -} -export function storeMenuCollapsed(status: boolean) { - localStorage.setItem(MENU_COLLAPSED, String(status)); -} +export type RoutingProps = { + to: string; + activeOnlyWhenExact?: boolean; + activeWhenMatch?: (route: any) => boolean; +}; diff --git a/scm-ui/ui-components/src/navigation/SecondaryNavigation.stories.tsx b/scm-ui/ui-components/src/navigation/SecondaryNavigation.stories.tsx new file mode 100644 index 0000000000..73531dcb18 --- /dev/null +++ b/scm-ui/ui-components/src/navigation/SecondaryNavigation.stories.tsx @@ -0,0 +1,89 @@ +/* + * 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 { storiesOf } from "@storybook/react"; +import React, { ReactElement } from "react"; +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; +`; + +const starships = ( + + + + +); + +const withRoute = (route: string) => { + return (story: ReactElement) => {story}; +}; + +storiesOf("Navigation|Secondary", module) + .addDecorator(story => {story()}) + .addDecorator(story => ( + +
    {story()}
    +
    + )) + .add("Default", () => + withRoute("/")( + + + + + ) + ) + .add("Sub Navigation", () => + withRoute("/")( + + + {starships} + + ) + ) + .add("Extension Point", () => { + const binder = new Binder("menu"); + binder.bind("subnav.sample", starships); + return withRoute("/hitchhiker/starships/titanic")( + + + + + + + ); + }); diff --git a/scm-ui/ui-components/src/navigation/SecondaryNavigation.tsx b/scm-ui/ui-components/src/navigation/SecondaryNavigation.tsx index fd8e1dd3eb..59e3593ef2 100644 --- a/scm-ui/ui-components/src/navigation/SecondaryNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SecondaryNavigation.tsx @@ -21,17 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC, ReactElement, ReactNode, useContext, useEffect } from "react"; + +import React, { FC } from "react"; import styled from "styled-components"; -import SubNavigation from "./SubNavigation"; -import { matchPath, useLocation } from "react-router-dom"; -import { isMenuCollapsed, MenuContext } from "./MenuContext"; +import useMenuContext from "./MenuContext"; type Props = { label: string; - children: ReactElement[]; - collapsed: boolean; - onCollapse?: (newStatus: boolean) => void; }; type CollapsedProps = { @@ -60,73 +56,37 @@ const MenuLabel = styled.p` cursor: pointer; `; -const SecondaryNavigation: FC = ({ label, children, collapsed, onCollapse }) => { - const location = useLocation(); - const menuContext = useContext(MenuContext); +const SecondaryNavigation: FC = ({ label, children }) => { + const menuContext = useMenuContext(); + const isCollapsed = menuContext.isCollapsed(); - const subNavActive = isSubNavigationActive(children, location.pathname); - const isCollapsed = collapsed && !subNavActive; + const toggleCollapseState = () => { + menuContext.setCollapsed(!isCollapsed); + }; - useEffect(() => { - if (isMenuCollapsed()) { - menuContext.setMenuCollapsed(!subNavActive); + const uncollapseMenu = () => { + if (isCollapsed) { + menuContext.setCollapsed(false); } - }, [subNavActive]); + }; - const childrenWithProps = React.Children.map(children, (child: ReactElement) => - React.cloneElement(child, { collapsed: isCollapsed }) - ); const arrowIcon = isCollapsed ? : ; return (
    - onCollapse(!isCollapsed) : undefined} - > - {onCollapse && !subNavActive && ( - - {arrowIcon} - - )} + + + {arrowIcon} + {isCollapsed ? "" : label} -
      {childrenWithProps}
    +
      + {children} +
    ); }; -const createParentPath = (to: string) => { - const parents = to.split("/"); - parents.splice(-1, 1); - return parents.join("/"); -}; - -const isSubNavigationActive = (children: ReactNode, url: string): boolean => { - const childArray = React.Children.toArray(children); - const match = childArray - .filter(child => { - // what about extension points? - // @ts-ignore - return child.type.name === SubNavigation.name; - }) - .map(child => { - // @ts-ignore - return child.props; - }) - .find(props => { - const path = createParentPath(props.to); - const matches = matchPath(url, { - path, - exact: props.activeOnlyWhenExact as boolean - }); - return matches != null; - }); - - return match != null; -}; - export default SecondaryNavigation; diff --git a/scm-ui/ui-components/src/navigation/SecondaryNavigationItem.tsx b/scm-ui/ui-components/src/navigation/SecondaryNavigationItem.tsx index e35acdb386..698eafe34a 100644 --- a/scm-ui/ui-components/src/navigation/SecondaryNavigationItem.tsx +++ b/scm-ui/ui-components/src/navigation/SecondaryNavigationItem.tsx @@ -21,48 +21,23 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { ReactElement, ReactNode } from "react"; -import { MenuContext } from "./MenuContext"; +import React, { FC } from "react"; import SubNavigation from "./SubNavigation"; import NavLink from "./NavLink"; +import { RoutingProps } from "./RoutingProps"; -type Props = { - to: string; - icon: string; +type Props = RoutingProps & { label: string; - title: string; - activeWhenMatch?: (route: any) => boolean; - activeOnlyWhenExact?: boolean; - children?: ReactElement[]; + title?: string; + icon?: string; }; -export default class SecondaryNavigationItem extends React.Component { - render() { - const { to, icon, label, title, activeWhenMatch, activeOnlyWhenExact, children } = this.props; - if (children) { - return ( - - {({ menuCollapsed }) => ( - - {children} - - )} - - ); - } else { - return ( - - {({ menuCollapsed }) => } - - ); - } +const SecondaryNavigationItem: FC = ({ children, ...props }) => { + if (children) { + return {children}; + } else { + return ; } -} +}; + +export default SecondaryNavigationItem; diff --git a/scm-ui/ui-components/src/navigation/SubNavigation.tsx b/scm-ui/ui-components/src/navigation/SubNavigation.tsx index 165f10588b..56a6c438d3 100644 --- a/scm-ui/ui-components/src/navigation/SubNavigation.tsx +++ b/scm-ui/ui-components/src/navigation/SubNavigation.tsx @@ -21,22 +21,19 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React, { FC, ReactElement, useContext, useEffect } from "react"; +import React, { FC, useContext } from "react"; import { Link, useRouteMatch } from "react-router-dom"; import classNames from "classnames"; +import useMenuContext, { MenuContext } from "./MenuContext"; +import { RoutingProps } from "./RoutingProps"; -type Props = { - to: string; - icon?: string; +type Props = RoutingProps & { label: string; - activeOnlyWhenExact?: boolean; - activeWhenMatch?: (route: any) => boolean; - children?: ReactElement[]; - collapsed?: boolean; title?: string; + icon?: string; }; -const SubNavigation: FC = ({ to, activeOnlyWhenExact, icon, collapsed, title, label, children }) => { +const SubNavigation: FC = ({ to, activeOnlyWhenExact, icon, title, label, children }) => { const parents = to.split("/"); parents.splice(-1, 1); const parent = parents.join("/"); @@ -46,6 +43,9 @@ const SubNavigation: FC = ({ to, activeOnlyWhenExact, icon, collapsed, ti exact: activeOnlyWhenExact }); + const context = useMenuContext(); + const collapsed = context.isCollapsed(); + let defaultIcon = "fas fa-cog"; if (icon) { defaultIcon = icon; diff --git a/scm-ui/ui-components/src/navigation/index.ts b/scm-ui/ui-components/src/navigation/index.ts index b7326f6810..4dcce4321b 100644 --- a/scm-ui/ui-components/src/navigation/index.ts +++ b/scm-ui/ui-components/src/navigation/index.ts @@ -31,5 +31,5 @@ export { default as SubNavigation } from "./SubNavigation"; export { default as PrimaryNavigation } from "./PrimaryNavigation"; export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink"; export { default as SecondaryNavigation } from "./SecondaryNavigation"; -export { MenuContext, storeMenuCollapsed, isMenuCollapsed } from "./MenuContext"; +export { MenuContext, StateMenuContextProvider } from "./MenuContext"; export { default as SecondaryNavigationItem } from "./SecondaryNavigationItem"; diff --git a/scm-ui/ui-components/src/repos/DiffFile.tsx b/scm-ui/ui-components/src/repos/DiffFile.tsx index 5d6381a971..4b042f0c4d 100644 --- a/scm-ui/ui-components/src/repos/DiffFile.tsx +++ b/scm-ui/ui-components/src/repos/DiffFile.tsx @@ -34,7 +34,6 @@ import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./ import TokenizedDiffView from "./TokenizedDiffView"; import DiffButton from "./DiffButton"; import { MenuContext } from "@scm-manager/ui-components"; -import { storeMenuCollapsed } from "../navigation"; const EMPTY_ANNOTATION_FACTORY = {}; @@ -132,7 +131,6 @@ class DiffFile extends React.Component { }), () => callback() ); - storeMenuCollapsed(true); }; setCollapse = (collapsed: boolean) => { @@ -289,11 +287,17 @@ class DiffFile extends React.Component { - {({ setMenuCollapsed }) => ( + {({ setCollapsed }) => ( this.toggleSideBySide(() => setMenuCollapsed(true))} + onClick={() => + this.toggleSideBySide(() => { + if (this.state.sideBySide) { + setCollapsed(true); + } + }) + } /> )} diff --git a/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx b/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx index 22979c631b..4954d765ed 100644 --- a/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntry.stories.tsx @@ -32,6 +32,7 @@ import { Binder, BinderContext } from "@scm-manager/ui-extensions"; import { Repository } from "@scm-manager/ui-types"; import Image from "../Image"; import Icon from "../Icon"; +import { MemoryRouter } from "react-router-dom"; const baseDate = "2020-03-26T12:13:42+02:00"; @@ -74,6 +75,7 @@ const QuickLink = ( ); storiesOf("RepositoryEntry", module) + .addDecorator(story => {story()}) .addDecorator(storyFn => {storyFn()}) .add("Default", () => { return ; diff --git a/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx b/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx index 3078c4c4d9..0f627374bb 100644 --- a/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx +++ b/scm-ui/ui-extensions/src/ExtensionPoint.test.tsx @@ -168,4 +168,46 @@ describe("ExtensionPoint test", () => { const text = rendered.text(); expect(text).toBe(""); }); + + it("should render an instance", () => { + const Label = () => { + return ; + }; + + mockedBinder.hasExtension.mockReturnValue(true); + mockedBinder.getExtension.mockReturnValue(
    -
    - {this.createRestartSectionContent()} -
    +
    {this.createRestartSectionContent()}
    {this.renderNotifications()} diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx index d28b51f927..c9ee5462d4 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx @@ -27,7 +27,6 @@ import { PendingPlugins } from "@scm-manager/ui-types"; import { useTranslation } from "react-i18next"; import PendingPluginsQueue from "./PendingPluginsQueue"; - type ModalBodyProps = { pendingPlugins: PendingPlugins; }; diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index 221e8974c7..8f14bed1e7 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -30,20 +30,18 @@ import { WithTranslation, withTranslation } from "react-i18next"; import { Me } from "@scm-manager/ui-types"; import { ErrorPage, - isMenuCollapsed, - MenuContext, NavLink, Page, CustomQueryFlexWrappedColumns, PrimaryContentColumn, SecondaryNavigationColumn, SecondaryNavigation, - SubNavigation + SubNavigation, + StateMenuContextProvider } from "@scm-manager/ui-components"; import ChangeUserPassword from "./ChangeUserPassword"; import ProfileInfo from "./ProfileInfo"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; type Props = RouteComponentProps & WithTranslation & { @@ -53,23 +51,7 @@ type Props = RouteComponentProps & match: any; }; -type State = { - menuCollapsed: boolean; -}; - -class Profile extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - menuCollapsed: isMenuCollapsed() - }; - } - - onCollapseProfileMenu = (collapsed: boolean) => { - this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); - }; - +class Profile extends React.Component { stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -85,7 +67,6 @@ class Profile extends React.Component { const url = this.matchedUrl(); const { me, t } = this.props; - const { menuCollapsed } = this.state; if (!me) { return ( @@ -106,22 +87,16 @@ class Profile extends React.Component { }; return ( - this.setState({ menuCollapsed: collapsed }) }} - > + - + } /> } /> - - this.onCollapseProfileMenu(!menuCollapsed)} - collapsed={menuCollapsed} - > + + { - + ); } } diff --git a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx index c7c658c059..5b5555b80c 100644 --- a/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/SingleGroup.tsx @@ -29,16 +29,15 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Group } from "@scm-manager/ui-types"; import { ErrorPage, - isMenuCollapsed, Loading, - MenuContext, NavLink, Page, CustomQueryFlexWrappedColumns, PrimaryContentColumn, SecondaryNavigationColumn, SecondaryNavigation, - SubNavigation + SubNavigation, + StateMenuContextProvider } from "@scm-manager/ui-components"; import { getGroupsLink } from "../../modules/indexResource"; import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups"; @@ -46,7 +45,6 @@ import { Details } from "./../components/table"; import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks"; import EditGroup from "./EditGroup"; import SetPermissions from "../../permissions/components/SetPermissions"; -import { storeMenuCollapsed } from "@scm-manager/ui-components/src"; type Props = RouteComponentProps & WithTranslation & { @@ -60,27 +58,11 @@ type Props = RouteComponentProps & fetchGroupByName: (p1: string, p2: string) => void; }; -type State = { - menuCollapsed: boolean; -}; - -class SingleGroup extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - menuCollapsed: isMenuCollapsed() - }; - } - +class SingleGroup extends React.Component { componentDidMount() { this.props.fetchGroupByName(this.props.groupLink, this.props.name); } - onCollapseGroupMenu = (collapsed: boolean) => { - this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); - }; - stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 2); @@ -94,7 +76,6 @@ class SingleGroup extends React.Component { render() { const { t, loading, error, group } = this.props; - const { menuCollapsed } = this.state; if (error) { return ; @@ -112,12 +93,10 @@ class SingleGroup extends React.Component { }; return ( - this.setState({ menuCollapsed: collapsed }) }} - > + - +
    } /> } /> { /> - - this.onCollapseGroupMenu(!menuCollapsed)} - collapsed={menuCollapsed} - > + + { - + ); } } diff --git a/scm-ui/ui-webapp/src/repos/containers/Create.tsx b/scm-ui/ui-webapp/src/repos/containers/Create.tsx index 1c8ff8a824..0ef6121afc 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Create.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Create.tsx @@ -54,7 +54,12 @@ type Props = WithTranslation & { // dispatch functions fetchNamespaceStrategiesIfNeeded: () => void; fetchRepositoryTypesIfNeeded: () => void; - createRepo: (link: string, repository: Repository, initRepository: boolean, callback: (repo: Repository) => void) => void; + createRepo: ( + link: string, + repository: Repository, + initRepository: boolean, + callback: (repo: Repository) => void + ) => void; resetForm: () => void; // context props diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 9651cd0a0b..38b0570cb5 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -37,9 +37,7 @@ import { SecondaryNavigationColumn, SecondaryNavigation, SubNavigation, - MenuContext, - storeMenuCollapsed, - isMenuCollapsed + StateMenuContextProvider } from "@scm-manager/ui-components"; import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos"; import RepositoryDetails from "../components/RepositoryDetails"; @@ -70,19 +68,7 @@ type Props = RouteComponentProps & fetchRepoByName: (link: string, namespace: string, name: string) => void; }; -type State = { - menuCollapsed: boolean; -}; - -class RepositoryRoot extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - menuCollapsed: isMenuCollapsed() - }; - } - +class RepositoryRoot extends React.Component { componentDidMount() { const { fetchRepoByName, namespace, name, repoLink } = this.props; fetchRepoByName(repoLink, namespace, name); @@ -131,13 +117,8 @@ class RepositoryRoot extends React.Component { return `${url}/changesets`; }; - onCollapseRepositoryMenu = (collapsed: boolean) => { - this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); - }; - render() { const { loading, error, indexLinks, repository, t } = this.props; - const { menuCollapsed } = this.state; if (error) { return ( @@ -166,18 +147,13 @@ class RepositoryRoot extends React.Component { } return ( - this.setState({ menuCollapsed: collapsed }) - }} - > + } > - + @@ -227,12 +203,8 @@ class RepositoryRoot extends React.Component { - - this.onCollapseRepositoryMenu(!menuCollapsed)} - collapsed={menuCollapsed} - > + + { - + ); } } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index a7ffac791a..b69cadd017 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -176,7 +176,7 @@ export default function reducer( pending: false } }; - } else if (action.itemId && (action.type === FETCH_UPDATES_SUCCESS)) { + } else if (action.itemId && action.type === FETCH_UPDATES_SUCCESS) { return { ...state, [action.itemId + action.payload.hunk]: { diff --git a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx index 5050b3ea05..ff2c57f0e6 100644 --- a/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/SingleUser.tsx @@ -28,9 +28,7 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { User } from "@scm-manager/ui-types"; import { ErrorPage, - isMenuCollapsed, Loading, - MenuContext, NavLink, Page, CustomQueryFlexWrappedColumns, @@ -38,7 +36,7 @@ import { SecondaryNavigationColumn, SecondaryNavigation, SubNavigation, - storeMenuCollapsed + StateMenuContextProvider } from "@scm-manager/ui-components"; import { Details } from "./../components/table"; import EditUser from "./EditUser"; @@ -61,19 +59,7 @@ type Props = RouteComponentProps & fetchUserByName: (p1: string, p2: string) => void; }; -type State = { - menuCollapsed: boolean; -}; - -class SingleUser extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - menuCollapsed: isMenuCollapsed() - }; - } - +class SingleUser extends React.Component { componentDidMount() { this.props.fetchUserByName(this.props.usersLink, this.props.name); } @@ -85,17 +71,12 @@ class SingleUser extends React.Component { return url; }; - onCollapseUserMenu = (collapsed: boolean) => { - this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed)); - }; - matchedUrl = () => { return this.stripEndingSlash(this.props.match.url); }; render() { const { t, loading, error, user } = this.props; - const { menuCollapsed } = this.state; if (error) { return ; @@ -113,12 +94,10 @@ class SingleUser extends React.Component { }; return ( - this.setState({ menuCollapsed: collapsed }) }} - > + - +
    } /> } /> } /> @@ -128,12 +107,8 @@ class SingleUser extends React.Component { /> - - this.onCollapseUserMenu(!menuCollapsed)} - collapsed={menuCollapsed} - > + + { - + ); } } diff --git a/yarn.lock b/yarn.lock index 7ebd28adce..1f60f6bbed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10020,6 +10020,16 @@ mini-css-extract-plugin@^0.8.0: schema-utils "^1.0.0" webpack-sources "^1.1.0" +mini-css-extract-plugin@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" + integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -12247,10 +12257,10 @@ react-redux@^5.0.7: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react-refresh@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.7.2.tgz#f30978d21eb8cac6e2f2fde056a7d04f6844dd50" - integrity sha512-u5l7fhAJXecWUJzVxzMRU2Zvw8m4QmDNHlTrT5uo3KBlYBhmChd7syAakBoay1yIiVhx/8Fi7a6v6kQZfsw81Q== +react-refresh@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.1.tgz#5500506ad6fc891fdd057d0bf3581f9310abc6a2" + integrity sha512-xZIKi49RtLUUSAZ4a4ut2xr+zr4+glOD5v0L413B55MPvlg4EQ6Ctx8PD4CmjlPGoAWmSCTmmkY59TErizNsow== react-router-dom@^5.1.2: version "5.1.2"