@scm-manager/ui-components 3.7.4 → 3.7.5-20250212-173204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-manager/ui-components",
3
- "version": "3.7.4",
3
+ "version": "3.7.5-20250212-173204",
4
4
  "description": "UI Components for SCM-Manager and its plugins",
5
5
  "main": "src/index.ts",
6
6
  "files": [
@@ -32,14 +32,14 @@
32
32
  "react-query": "^3.39.2"
33
33
  },
34
34
  "devDependencies": {
35
- "@scm-manager/ui-tests": "3.7.4",
36
- "@scm-manager/ui-types": "3.7.4",
35
+ "@scm-manager/ui-tests": "3.7.5-20250212-173204",
36
+ "@scm-manager/ui-types": "3.7.5-20250212-173204",
37
37
  "@types/fetch-mock": "^7.3.1",
38
38
  "@types/react-select": "^2.0.19",
39
39
  "@types/unist": "^2.0.3",
40
40
  "gitdiff-parser": "^0.2.2",
41
41
  "i18next-fetch-backend": "4",
42
- "webpack": "^5.72.0",
42
+ "webpack": "^5.76.0",
43
43
  "@storybook/addon-actions": "^6.5.10",
44
44
  "@storybook/addon-essentials": "^6.5.10",
45
45
  "@storybook/addon-interactions": "^6.5.10",
@@ -47,6 +47,7 @@
47
47
  "@storybook/builder-webpack5": "^6.5.10",
48
48
  "@storybook/manager-webpack5": "^6.5.10",
49
49
  "@storybook/react": "^6.5.10",
50
+ "@testing-library/react": "^12.1.5",
50
51
  "storybook-addon-i18next": "^1.3.0",
51
52
  "storybook-addon-themes": "^6.1.0",
52
53
  "@types/classnames": "^2.3.1",
@@ -67,17 +68,17 @@
67
68
  "@scm-manager/jest-preset": "^2.14.1",
68
69
  "@scm-manager/prettier-config": "^2.12.0",
69
70
  "@scm-manager/tsconfig": "^2.13.0",
70
- "@scm-manager/ui-syntaxhighlighting": "3.7.4",
71
- "@scm-manager/ui-shortcuts": "3.7.4",
72
- "@scm-manager/ui-text": "3.7.4"
71
+ "@scm-manager/ui-syntaxhighlighting": "3.7.5-20250212-173204",
72
+ "@scm-manager/ui-shortcuts": "3.7.5-20250212-173204",
73
+ "@scm-manager/ui-text": "3.7.5-20250212-173204"
73
74
  },
74
75
  "dependencies": {
75
- "@scm-manager/ui-core": "3.7.4",
76
- "@scm-manager/ui-overlays": "3.7.4",
77
- "@scm-manager/ui-layout": "3.7.4",
78
- "@scm-manager/ui-buttons": "3.7.4",
79
- "@scm-manager/ui-api": "3.7.4",
80
- "@scm-manager/ui-extensions": "3.7.4",
76
+ "@scm-manager/ui-core": "3.7.5-20250212-173204",
77
+ "@scm-manager/ui-overlays": "3.7.5-20250212-173204",
78
+ "@scm-manager/ui-layout": "3.7.5-20250212-173204",
79
+ "@scm-manager/ui-buttons": "3.7.5-20250212-173204",
80
+ "@scm-manager/ui-api": "3.7.5-20250212-173204",
81
+ "@scm-manager/ui-extensions": "3.7.5-20250212-173204",
81
82
  "deepmerge": "^4.2.2",
82
83
  "hast-util-sanitize": "^3.0.2",
83
84
  "react-diff-view": "^2.4.10",
@@ -20,21 +20,39 @@ import React from "react";
20
20
 
21
21
  storiesOf("Duration", module).add("Duration", () => (
22
22
  <div className="m-5 p-5">
23
+ <p>
24
+ <Duration duration={1} />
25
+ </p>
23
26
  <p>
24
27
  <Duration duration={500} />
25
28
  </p>
29
+ <p>
30
+ <Duration duration={1000 + 1} />
31
+ </p>
26
32
  <p>
27
33
  <Duration duration={2000} />
28
34
  </p>
35
+ <p>
36
+ <Duration duration={1000 * 60 + 1} />
37
+ </p>
29
38
  <p>
30
39
  <Duration duration={42 * 1000 * 60} />
31
40
  </p>
41
+ <p>
42
+ <Duration duration={1000 * 60 * 60 + 1} />
43
+ </p>
32
44
  <p>
33
45
  <Duration duration={21 * 1000 * 60 * 60} />
34
46
  </p>
47
+ <p>
48
+ <Duration duration={1000 * 60 * 60 * 24 + 1} />
49
+ </p>
35
50
  <p>
36
51
  <Duration duration={5 * 1000 * 60 * 60 * 24} />
37
52
  </p>
53
+ <p>
54
+ <Duration duration={1000 * 60 * 60 * 24 * 7 + 1} />
55
+ </p>
38
56
  <p>
39
57
  <Duration duration={3 * 1000 * 60 * 60 * 24 * 7} />
40
58
  </p>
@@ -1521,6 +1521,13 @@ exports[`Storyshots Duration Duration 1`] = `
1521
1521
  <div
1522
1522
  className="m-5 p-5"
1523
1523
  >
1524
+ <p>
1525
+ <time
1526
+ dateTime="1ms"
1527
+ >
1528
+ duration.ms
1529
+ </time>
1530
+ </p>
1524
1531
  <p>
1525
1532
  <time
1526
1533
  dateTime="500ms"
@@ -1528,6 +1535,13 @@ exports[`Storyshots Duration Duration 1`] = `
1528
1535
  duration.ms
1529
1536
  </time>
1530
1537
  </p>
1538
+ <p>
1539
+ <time
1540
+ dateTime="1s"
1541
+ >
1542
+ duration.s
1543
+ </time>
1544
+ </p>
1531
1545
  <p>
1532
1546
  <time
1533
1547
  dateTime="2s"
@@ -1535,6 +1549,13 @@ exports[`Storyshots Duration Duration 1`] = `
1535
1549
  duration.s
1536
1550
  </time>
1537
1551
  </p>
1552
+ <p>
1553
+ <time
1554
+ dateTime="1m"
1555
+ >
1556
+ duration.m
1557
+ </time>
1558
+ </p>
1538
1559
  <p>
1539
1560
  <time
1540
1561
  dateTime="42m"
@@ -1542,6 +1563,13 @@ exports[`Storyshots Duration Duration 1`] = `
1542
1563
  duration.m
1543
1564
  </time>
1544
1565
  </p>
1566
+ <p>
1567
+ <time
1568
+ dateTime="1h"
1569
+ >
1570
+ duration.h
1571
+ </time>
1572
+ </p>
1545
1573
  <p>
1546
1574
  <time
1547
1575
  dateTime="21h"
@@ -1549,6 +1577,13 @@ exports[`Storyshots Duration Duration 1`] = `
1549
1577
  duration.h
1550
1578
  </time>
1551
1579
  </p>
1580
+ <p>
1581
+ <time
1582
+ dateTime="1d"
1583
+ >
1584
+ duration.d
1585
+ </time>
1586
+ </p>
1552
1587
  <p>
1553
1588
  <time
1554
1589
  dateTime="5d"
@@ -1556,6 +1591,13 @@ exports[`Storyshots Duration Duration 1`] = `
1556
1591
  duration.d
1557
1592
  </time>
1558
1593
  </p>
1594
+ <p>
1595
+ <time
1596
+ dateTime="1w"
1597
+ >
1598
+ duration.w
1599
+ </time>
1600
+ </p>
1559
1601
  <p>
1560
1602
  <time
1561
1603
  dateTime="3w"
@@ -78158,11 +78200,18 @@ exports[`Storyshots Secondary Navigation Active when match 1`] = `
78158
78200
  <div>
78159
78201
  <button
78160
78202
  aria-label="secondaryNavigation.hideContent"
78161
- className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label is-clickable"
78203
+ className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label"
78162
78204
  collapsed={false}
78163
78205
  onClick={[Function]}
78164
78206
  type="button"
78165
78207
  >
78208
+ <i
78209
+ className="SecondaryNavigation__Icon-sc-8p1rgi-1 gqxbcY is-medium"
78210
+ >
78211
+ <i
78212
+ className="fas fa-caret-down"
78213
+ />
78214
+ </i>
78166
78215
  Hitchhiker
78167
78216
  </button>
78168
78217
  <ul
@@ -78215,7 +78264,7 @@ exports[`Storyshots Secondary Navigation Default 1`] = `
78215
78264
  <div>
78216
78265
  <button
78217
78266
  aria-label="secondaryNavigation.hideContent"
78218
- className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label is-clickable"
78267
+ className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label"
78219
78268
  collapsed={false}
78220
78269
  onClick={[Function]}
78221
78270
  type="button"
@@ -78278,7 +78327,7 @@ exports[`Storyshots Secondary Navigation Extension Point 1`] = `
78278
78327
  <div>
78279
78328
  <button
78280
78329
  aria-label="secondaryNavigation.hideContent"
78281
- className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label is-clickable"
78330
+ className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label"
78282
78331
  collapsed={false}
78283
78332
  onClick={[Function]}
78284
78333
  type="button"
@@ -78369,7 +78418,7 @@ exports[`Storyshots Secondary Navigation Sub Navigation 1`] = `
78369
78418
  <div>
78370
78419
  <button
78371
78420
  aria-label="secondaryNavigation.hideContent"
78372
- className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label is-clickable"
78421
+ className="button SecondaryNavigation__MenuButton-sc-8p1rgi-2 fkeiWf menu-label"
78373
78422
  collapsed={false}
78374
78423
  onClick={[Function]}
78375
78424
  type="button"
@@ -41,7 +41,7 @@ type Props = ButtonProps & {
41
41
  };
42
42
 
43
43
  /**
44
- * @deprecated Use {@link ui-buttons/src/Button.tsx} instead
44
+ * @deprecated Use {@link ui-core/src/base/buttons/Button} instead
45
45
  */
46
46
  const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
47
47
  (
@@ -21,6 +21,7 @@ import { createAttributesForTesting } from "../devBuild";
21
21
  import useAutofocus from "./useAutofocus";
22
22
  import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
23
23
  import { createA11yId } from "../createA11yId";
24
+ import { FieldMessage } from "@scm-manager/ui-core";
24
25
 
25
26
  type BaseProps = {
26
27
  label?: string;
@@ -40,6 +41,7 @@ type BaseProps = {
40
41
  defaultValue?: string | number;
41
42
  readOnly?: boolean;
42
43
  required?: boolean;
44
+ warning?: string;
43
45
  };
44
46
 
45
47
  export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>> = ({
@@ -60,6 +62,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
60
62
  defaultValue,
61
63
  readOnly,
62
64
  required,
65
+ warning,
63
66
  ...props
64
67
  }) => {
65
68
  const field = useAutofocus<HTMLInputElement>(autofocus, props.innerRef);
@@ -123,6 +126,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
123
126
  {...createAttributesForTesting(testId)}
124
127
  />
125
128
  </div>
129
+ {warning ? <FieldMessage variant="warning">{warning}</FieldMessage> : null}
126
130
  {helper}
127
131
  </fieldset>
128
132
  );
@@ -17,6 +17,7 @@
17
17
  import React, { FC } from "react";
18
18
  import styled from "styled-components";
19
19
  import { useSecondaryNavigation } from "../useSecondaryNavigation";
20
+ import { SecondaryNavigationProvider } from "../navigation/SecondaryNavigationContext";
20
21
 
21
22
  const SecondaryColumn = styled.div<{ collapsed: boolean }>`
22
23
  flex: 0 0 auto;
@@ -28,7 +29,7 @@ const SecondaryColumn = styled.div<{ collapsed: boolean }>`
28
29
  }
29
30
  `;
30
31
 
31
- const SecondaryNavigationColumn: FC = ({ children }) => {
32
+ const SecondaryNavigationColumnIntern: FC = ({ children }) => {
32
33
  const { collapsed } = useSecondaryNavigation();
33
34
 
34
35
  return (
@@ -38,4 +39,12 @@ const SecondaryNavigationColumn: FC = ({ children }) => {
38
39
  );
39
40
  };
40
41
 
42
+ const SecondaryNavigationColumn: FC = ({ children }) => {
43
+ return (
44
+ <SecondaryNavigationProvider>
45
+ <SecondaryNavigationColumnIntern>{children}</SecondaryNavigationColumnIntern>
46
+ </SecondaryNavigationProvider>
47
+ );
48
+ };
49
+
41
50
  export default SecondaryNavigationColumn;
@@ -14,11 +14,10 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { FC, useContext } from "react";
17
+ import React, { FC } from "react";
18
18
  import classNames from "classnames";
19
19
  import { useSecondaryNavigation } from "../useSecondaryNavigation";
20
20
  import ExternalLink from "./ExternalLink";
21
- import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
22
21
 
23
22
  type Props = {
24
23
  to: string;
@@ -28,7 +27,6 @@ type Props = {
28
27
 
29
28
  const ExternalNavLink: FC<Props> = ({ to, icon, label }) => {
30
29
  const { collapsed } = useSecondaryNavigation();
31
- const isSecondaryNavigation = useContext(SecondaryNavigationContext);
32
30
 
33
31
  let showIcon;
34
32
  if (icon) {
@@ -43,7 +41,7 @@ const ExternalNavLink: FC<Props> = ({ to, icon, label }) => {
43
41
  <li title={collapsed ? label : undefined}>
44
42
  <ExternalLink to={to} className={collapsed ? "has-text-centered" : ""} aria-label={collapsed ? label : undefined}>
45
43
  {showIcon}
46
- {isSecondaryNavigation && collapsed ? null : label}
44
+ {collapsed ? null : label}
47
45
  </ExternalLink>
48
46
  </li>
49
47
  );
@@ -17,12 +17,11 @@
17
17
  import React, { FC, useContext, useEffect } from "react";
18
18
  import classNames from "classnames";
19
19
  import { Link } from "react-router-dom";
20
+ import { createAttributesForTesting } from "@scm-manager/ui-core";
20
21
  import { useSecondaryNavigation } from "../useSecondaryNavigation";
21
22
  import { RoutingProps } from "./RoutingProps";
22
- import useActiveMatch from "./useActiveMatch";
23
- import { createAttributesForTesting } from "@scm-manager/ui-core";
24
- import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
25
23
  import { SubNavigationContext } from "./SubNavigationContext";
24
+ import useActiveMatch from "./useActiveMatch";
26
25
 
27
26
  type Props = RoutingProps & {
28
27
  label: string;
@@ -51,14 +50,13 @@ const NavLinkContent: FC<NavLinkContentProp> = ({ label, icon, collapsed }) => (
51
50
  const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, title, testId, children, ...contentProps }) => {
52
51
  const active = useActiveMatch({ to, activeWhenMatch, activeOnlyWhenExact });
53
52
  const { collapsed, setCollapsible } = useSecondaryNavigation();
54
- const isSecondaryNavigation = useContext(SecondaryNavigationContext);
55
53
  const isSubNavigation = useContext(SubNavigationContext);
56
54
 
57
55
  useEffect(() => {
58
- if (isSecondaryNavigation && active) {
56
+ if (active) {
59
57
  setCollapsible(!isSubNavigation);
60
58
  }
61
- }, [active, isSecondaryNavigation, isSubNavigation, setCollapsible]);
59
+ }, [active, isSubNavigation, setCollapsible]);
62
60
 
63
61
  return (
64
62
  <li title={collapsed ? title : undefined}>
@@ -69,11 +67,7 @@ const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, title, t
69
67
  aria-label={collapsed ? title : undefined}
70
68
  {...(active ? { "aria-current": "page" } : {})}
71
69
  >
72
- {children ? (
73
- children
74
- ) : (
75
- <NavLinkContent {...contentProps} collapsed={(isSecondaryNavigation && collapsed) ?? false} />
76
- )}
70
+ {children || <NavLinkContent {...contentProps} collapsed={collapsed} />}
77
71
  </Link>
78
72
  </li>
79
73
  );
@@ -16,12 +16,13 @@
16
16
 
17
17
  import { storiesOf } from "@storybook/react";
18
18
  import React, { ReactElement } from "react";
19
+ import { MemoryRouter } from "react-router-dom";
20
+ import styled from "styled-components";
21
+ import { Binder, ExtensionPoint, BinderContext } from "@scm-manager/ui-extensions";
22
+ import { SecondaryNavigationProvider } from "./SecondaryNavigationContext";
19
23
  import SecondaryNavigation from "./SecondaryNavigation";
20
24
  import SecondaryNavigationItem from "./SecondaryNavigationItem";
21
- import styled from "styled-components";
22
25
  import SubNavigation from "./SubNavigation";
23
- import { Binder, ExtensionPoint, BinderContext } from "@scm-manager/ui-extensions";
24
- import { MemoryRouter } from "react-router-dom";
25
26
 
26
27
  const Columns = styled.div`
27
28
  margin: 2rem;
@@ -46,7 +47,9 @@ const withRoute = (route: string) => {
46
47
  storiesOf("Secondary Navigation", module)
47
48
  .addDecorator((story) => (
48
49
  <Columns className="columns">
49
- <div className="column is-3">{story()}</div>
50
+ <div className="column is-3">
51
+ <SecondaryNavigationProvider>{story()}</SecondaryNavigationProvider>
52
+ </div>
50
53
  </Columns>
51
54
  ))
52
55
  .add("Default", () =>
@@ -16,10 +16,8 @@
16
16
 
17
17
  import React, { FC } from "react";
18
18
  import styled from "styled-components";
19
- import classNames from "classnames";
20
19
  import { useTranslation } from "react-i18next";
21
20
  import { useSecondaryNavigation } from "../useSecondaryNavigation";
22
- import { SecondaryNavigationContext } from "./SecondaryNavigationContext";
23
21
  import { Button } from "@scm-manager/ui-buttons";
24
22
 
25
23
  type Props = {
@@ -73,14 +71,9 @@ const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true })
73
71
  const menuAriaLabel = collapsed ? t("secondaryNavigation.showContent") : t("secondaryNavigation.hideContent");
74
72
 
75
73
  return (
76
- <SectionContainer className="menu" collapsed={collapsed ?? false}>
74
+ <SectionContainer className="menu" collapsed={collapsed}>
77
75
  <div>
78
- <MenuButton
79
- className={classNames("menu-label", { "is-clickable": true })}
80
- collapsed={collapsed}
81
- onClick={toggleCollapse}
82
- aria-label={menuAriaLabel}
83
- >
76
+ <MenuButton className="menu-label" collapsed={collapsed} onClick={toggleCollapse} aria-label={menuAriaLabel}>
84
77
  {isCollapsible ? (
85
78
  <Icon className="is-medium" collapsed={collapsed}>
86
79
  {arrowIcon}
@@ -88,9 +81,7 @@ const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true })
88
81
  ) : null}
89
82
  {collapsed ? "" : label}
90
83
  </MenuButton>
91
- <ul className="menu-list">
92
- <SecondaryNavigationContext.Provider value={true}>{children}</SecondaryNavigationContext.Provider>
93
- </ul>
84
+ <ul className="menu-list">{children}</ul>
94
85
  </div>
95
86
  </SectionContainer>
96
87
  );
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright (c) 2020 - present Cloudogu GmbH
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify it under
5
+ * the terms of the GNU Affero General Public License as published by the Free
6
+ * Software Foundation, version 3.
7
+ *
8
+ * This program is distributed in the hope that it will be useful, but WITHOUT
9
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
+ * details.
12
+ *
13
+ * You should have received a copy of the GNU Affero General Public License
14
+ * along with this program. If not, see https://www.gnu.org/licenses/.
15
+ */
16
+
17
+ import React, { ReactNode, useMemo, useState } from "react";
18
+
19
+ type SecondaryNavigationContextState = {
20
+ collapsible: boolean;
21
+ setCollapsible: (collapsed: boolean) => void;
22
+ };
23
+
24
+ const dummy: SecondaryNavigationContextState = {
25
+ collapsible: false,
26
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
27
+ setCollapsible: () => {},
28
+ };
29
+
30
+ export const SecondaryNavigationContext = React.createContext<SecondaryNavigationContextState>(dummy);
31
+
32
+ export const SecondaryNavigationProvider = ({ children }: { children: ReactNode }) => {
33
+ const [collapsible, setCollapsible] = useState(true);
34
+ const contextValue = useMemo(() => ({ collapsible, setCollapsible }), [collapsible, setCollapsible]);
35
+
36
+ return <SecondaryNavigationContext.Provider value={contextValue}>{children}</SecondaryNavigationContext.Provider>;
37
+ };
@@ -26,7 +26,7 @@ import { DiffObjectProps } from "./DiffTypes";
26
26
  import DiffStatistics from "./DiffStatistics";
27
27
  import { DiffDropDown } from "../index";
28
28
  import DiffFileTree from "./diff/DiffFileTree";
29
- import { DiffContent, Divider, FileTreeContent } from "./diff/styledElements";
29
+ import { DiffContent, Divider, FileTreeContent, StickyFileDiffContainer } from "./diff/styledElements";
30
30
  import { useHistory, useLocation } from "react-router-dom";
31
31
  import { getFileNameFromHash } from "./diffs";
32
32
  import LayoutRadioButtons from "./LayoutRadioButtons";
@@ -111,7 +111,7 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
111
111
  </div>
112
112
  <LayoutRadioButtons layout={layout} setLayout={setLayout} />
113
113
  <div className="is-flex mb-4 mt-1 columns is-multiline">
114
- <div
114
+ <StickyFileDiffContainer
115
115
  className={
116
116
  (layout === "Both" ? "column pl-3 is-one-quarter" : "column pl-3 is-full") +
117
117
  (layout !== "Diff" ? "" : " is-hidden")
@@ -125,10 +125,11 @@ const LoadingDiff: FC<Props> = ({ url, limit, refetchOnWindowFocus, ...props })
125
125
  tree={data.tree}
126
126
  currentFile={decodeURIComponent(getFileNameFromHash(location.hash) ?? "")}
127
127
  setCurrentFile={setFilePath}
128
+ gap={12}
128
129
  />
129
130
  )}
130
131
  </FileTreeContent>
131
- </div>
132
+ </StickyFileDiffContainer>
132
133
  <DiffContent id={diffContentId} className={layout !== "Tree" ? "column" : "is-hidden"}>
133
134
  <Diff
134
135
  defaultCollapse={collapsed}
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Copyright (c) 2020 - present Cloudogu GmbH
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify it under
5
+ * the terms of the GNU Affero General Public License as published by the Free
6
+ * Software Foundation, version 3.
7
+ *
8
+ * This program is distributed in the hope that it will be useful, but WITHOUT
9
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
+ * details.
12
+ *
13
+ * You should have received a copy of the GNU Affero General Public License
14
+ * along with this program. If not, see https://www.gnu.org/licenses/.
15
+ */
16
+
17
+ import * as changesets from "./changesets";
18
+ import { render } from "@testing-library/react";
19
+ import { Branch, Changeset, Repository } from "@scm-manager/ui-types";
20
+ import ChangesetButtonGroup from "./ChangesetButtonGroup";
21
+ import React from "react";
22
+ import { BrowserRouter } from "react-router-dom";
23
+ import { stubI18Next } from "@scm-manager/ui-tests";
24
+
25
+ const createChangesetLink = jest.spyOn(changesets, "createChangesetLink");
26
+ const createChangesetLinkByBranch = jest.spyOn(changesets, "createChangesetLinkByBranch");
27
+
28
+ afterEach(() => {
29
+ jest.resetAllMocks();
30
+ });
31
+
32
+ describe("ChangesetButtonGroup", () => {
33
+ test("shouldCallCreateChangesetLinkWithoutBranch", async () => {
34
+ stubI18Next();
35
+ const { repository, changeset } = createTestData();
36
+ render(
37
+ <BrowserRouter>
38
+ <ChangesetButtonGroup repository={repository} changeset={changeset}></ChangesetButtonGroup>
39
+ </BrowserRouter>
40
+ );
41
+ expect(createChangesetLink).toHaveBeenCalled();
42
+ expect(createChangesetLinkByBranch).toHaveBeenCalledTimes(0);
43
+ });
44
+
45
+ test("shouldCallCreateChangesetLinkByBranchWithBranch", async () => {
46
+ stubI18Next();
47
+ const { repository, changeset, branch } = createTestData();
48
+ render(
49
+ <BrowserRouter>
50
+ <ChangesetButtonGroup repository={repository} changeset={changeset} branch={branch}></ChangesetButtonGroup>
51
+ </BrowserRouter>
52
+ );
53
+ expect(createChangesetLinkByBranch).toHaveBeenCalled();
54
+ expect(createChangesetLink).toHaveBeenCalledTimes(0);
55
+ });
56
+ });
57
+
58
+ // TODO centralized test data
59
+ function createTestData() {
60
+ const repository: Repository = { _links: {}, name: "", namespace: "", type: "" };
61
+ const changeset: Changeset = {
62
+ _links: {},
63
+ author: {
64
+ name: "",
65
+ },
66
+ date: new Date(),
67
+ description: "",
68
+ id: "",
69
+ };
70
+ const branch: Branch = { _links: {}, name: "", revision: "" };
71
+ return { repository, changeset, branch };
72
+ }
@@ -15,21 +15,24 @@
15
15
  */
16
16
 
17
17
  import React from "react";
18
- import { Changeset, File, Repository } from "@scm-manager/ui-types";
19
- import { ButtonAddons, Button } from "../../buttons";
20
- import { createChangesetLink, createSourcesLink } from "./changesets";
18
+ import { Branch, Changeset, File, Repository } from "@scm-manager/ui-types";
19
+ import { Button, ButtonAddons } from "../../buttons";
20
+ import { createChangesetLink, createChangesetLinkByBranch, createSourcesLink } from "./changesets";
21
21
  import { useTranslation } from "react-i18next";
22
22
 
23
23
  type Props = {
24
24
  repository: Repository;
25
25
  changeset: Changeset;
26
26
  file?: File;
27
+ branch?: Branch;
27
28
  };
28
29
 
29
30
  const ChangesetButtonGroup = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
30
- ({ repository, changeset, file }, ref) => {
31
+ ({ repository, changeset, file, branch }, ref) => {
31
32
  const [t] = useTranslation("repos");
32
- const changesetLink = createChangesetLink(repository, changeset);
33
+ const changesetLink = branch
34
+ ? createChangesetLinkByBranch(repository, changeset, branch)
35
+ : createChangesetLink(repository, changeset);
33
36
  const sourcesLink = createSourcesLink(repository, changeset, file);
34
37
  return (
35
38
  <ButtonAddons className="m-0">
@@ -16,20 +16,23 @@
16
16
 
17
17
  import ChangesetRow from "./ChangesetRow";
18
18
  import React, { FC } from "react";
19
- import { Changeset, File, Repository } from "@scm-manager/ui-types";
19
+ import { Branch, Changeset, File, Repository } from "@scm-manager/ui-types";
20
20
  import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
21
21
 
22
22
  type Props = {
23
23
  repository: Repository;
24
24
  changesets: Changeset[];
25
25
  file?: File;
26
+ branch?: Branch;
26
27
  };
27
28
 
28
- const ChangesetList: FC<Props> = ({ repository, changesets, file }) => {
29
+ const ChangesetList: FC<Props> = ({ repository, changesets, file, branch }) => {
29
30
  return (
30
31
  <KeyboardIterator>
31
32
  {changesets.map((changeset) => {
32
- return <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} file={file} />;
33
+ return (
34
+ <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} file={file} branch={branch} />
35
+ );
33
36
  })}
34
37
  </KeyboardIterator>
35
38
  );
@@ -18,7 +18,7 @@ import React, { FC } from "react";
18
18
  import classNames from "classnames";
19
19
  import styled from "styled-components";
20
20
  import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
21
- import { Changeset, File, Repository } from "@scm-manager/ui-types";
21
+ import { Branch, Changeset, File, Repository } from "@scm-manager/ui-types";
22
22
  import ChangesetButtonGroup from "./ChangesetButtonGroup";
23
23
  import SingleChangeset from "./SingleChangeset";
24
24
  import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
@@ -27,11 +27,13 @@ type Props = {
27
27
  repository: Repository;
28
28
  changeset: Changeset;
29
29
  file?: File;
30
+ branch?: Branch;
30
31
  };
31
32
 
32
33
  const Wrapper = styled.div`
33
34
  // & references parent rule
34
35
  // have a look at https://cssinjs.org/jss-plugin-nested?v=v10.0.0-alpha.9
36
+
35
37
  & + & {
36
38
  margin-top: 1rem;
37
39
  padding-top: 1rem;
@@ -39,7 +41,7 @@ const Wrapper = styled.div`
39
41
  }
40
42
  `;
41
43
 
42
- const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
44
+ const ChangesetRow: FC<Props> = ({ repository, changeset, file, branch }) => {
43
45
  const ref = useKeyboardIteratorTarget();
44
46
  return (
45
47
  <Wrapper>
@@ -48,7 +50,7 @@ const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
48
50
  <SingleChangeset repository={repository} changeset={changeset} />
49
51
  </div>
50
52
  <div className={classNames("column", "is-flex", "is-justify-content-flex-end", "is-align-items-center")}>
51
- <ChangesetButtonGroup ref={ref} repository={repository} changeset={changeset} file={file} />
53
+ <ChangesetButtonGroup ref={ref} repository={repository} changeset={changeset} file={file} branch={branch} />
52
54
  <ExtensionPoint<extensionPoints.ChangesetRight>
53
55
  name="changeset.right"
54
56
  props={{
@@ -14,9 +14,38 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import { parseDescription } from "./changesets";
17
+ import { createChangesetLink, createChangesetLinkByBranch, parseDescription } from "./changesets";
18
+ import { Branch, Changeset, Repository } from "@scm-manager/ui-types";
18
19
 
19
- describe("parseDescription tests", () => {
20
+ describe("createChangesetLink", () => {
21
+ it("should return a changeset link", () => {
22
+ const { repository, changeset } = createTestData();
23
+ const link = createChangesetLink(repository, changeset);
24
+ expect(link).toBe("/repo/sandbox/anarchy/code/changeset/4f153aa670d4b27c");
25
+ });
26
+ });
27
+
28
+ describe("createChangesetLinkByBranch", () => {
29
+ it("should return a changeset link with a branch query with given branch", () => {
30
+ const { repository, changeset, branch } = createTestData();
31
+ const link = createChangesetLinkByBranch(repository, changeset, branch);
32
+ expect(link).toBe("/repo/sandbox/anarchy/code/changeset/4f153aa670d4b27c?branch=resonanceCascade");
33
+ });
34
+ it("should return no branch query parameter with empty string", () => {
35
+ const { repository, changeset, branch } = createTestData();
36
+ branch.name = "";
37
+ const link = createChangesetLinkByBranch(repository, changeset, branch);
38
+ expect(link).toBe("/repo/sandbox/anarchy/code/changeset/4f153aa670d4b27c");
39
+ });
40
+ it("should escape a branch with a slash inside", () => {
41
+ const { repository, changeset, branch } = createTestData();
42
+ branch.name = "feature/rescueWorld";
43
+ const link = createChangesetLinkByBranch(repository, changeset, branch);
44
+ expect(link).toBe("/repo/sandbox/anarchy/code/changeset/4f153aa670d4b27c?branch=feature%2FrescueWorld");
45
+ });
46
+ });
47
+
48
+ describe("parseDescription", () => {
20
49
  it("should return a description with title and message", () => {
21
50
  const desc = parseDescription("Hello\nTrillian");
22
51
  expect(desc.title).toBe("Hello");
@@ -34,3 +63,34 @@ describe("parseDescription tests", () => {
34
63
  expect(desc.message).toBe("");
35
64
  });
36
65
  });
66
+
67
+ function createTestData() {
68
+ const repository: Repository = {
69
+ name: "anarchy",
70
+ namespace: "sandbox",
71
+ type: "git",
72
+ _links: {},
73
+ };
74
+
75
+ const changeset: Changeset = {
76
+ author: {
77
+ name: "Gordon Freeman",
78
+ },
79
+ date: new Date(),
80
+ description: "Some repository.",
81
+ id: "4f153aa670d4b27c",
82
+ _links: {},
83
+ };
84
+
85
+ const branch: Branch = {
86
+ name: "resonanceCascade",
87
+ revision: "4f153aa670d4b27c",
88
+ _links: {},
89
+ };
90
+
91
+ return {
92
+ repository,
93
+ changeset,
94
+ branch,
95
+ };
96
+ }
@@ -14,7 +14,7 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import { Changeset, File, Repository } from "@scm-manager/ui-types";
17
+ import { Branch, Changeset, File, Repository } from "@scm-manager/ui-types";
18
18
 
19
19
  export type Description = {
20
20
  title: string;
@@ -25,6 +25,16 @@ export function createChangesetLink(repository: Repository, changeset: Changeset
25
25
  return `/repo/${repository.namespace}/${repository.name}/code/changeset/${changeset.id}`;
26
26
  }
27
27
 
28
+ export function createChangesetLinkByBranch(repository: Repository, changeset: Changeset, branch: Branch) {
29
+ if (!branch.name) {
30
+ return `/repo/${repository.namespace}/${repository.name}/code/changeset/${changeset.id}`;
31
+ } else {
32
+ return `/repo/${repository.namespace}/${repository.name}/code/changeset/${changeset.id}?branch=${encodeURIComponent(
33
+ branch.name
34
+ )}`;
35
+ }
36
+ }
37
+
28
38
  export function createSourcesLink(repository: Repository, changeset: Changeset, file?: File) {
29
39
  let url = `/repo/${repository.namespace}/${repository.name}/code/sources/${changeset.id}`;
30
40
 
@@ -50,6 +60,6 @@ export function parseDescription(description?: string): Description {
50
60
 
51
61
  return {
52
62
  title,
53
- message
63
+ message,
54
64
  };
55
65
  }
@@ -21,16 +21,16 @@ import { Icon } from "@scm-manager/ui-core";
21
21
  import { useTranslation } from "react-i18next";
22
22
  import styled from "styled-components";
23
23
 
24
- type Props = { tree: FileTree; currentFile: string; setCurrentFile: (path: string) => void };
24
+ type Props = { tree: FileTree; currentFile: string; setCurrentFile: (path: string) => void; gap?: number };
25
25
 
26
26
  const StyledIcon = styled(Icon)`
27
27
  min-width: 1.5rem;
28
28
  `;
29
29
 
30
- const DiffFileTree: FC<Props> = ({ tree, currentFile, setCurrentFile }) => {
30
+ const DiffFileTree: FC<Props> = ({ tree, currentFile, setCurrentFile, gap = 15 }) => {
31
31
  return (
32
32
  <FileDiffContainer className={"mt-4 py-3 pr-2"}>
33
- <FileDiffContent>
33
+ <FileDiffContent gap={gap}>
34
34
  {Object.keys(tree.children).map((key) => (
35
35
  <TreeNode
36
36
  key={key}
@@ -96,17 +96,28 @@ export const DiffContent = styled.div`
96
96
  width: 100%;
97
97
  `;
98
98
 
99
- export const FileDiffContainer = styled.div`
99
+ export const StickyFileDiffContainer = styled.div`
100
+ top: 3rem;
100
101
  position: sticky;
102
+ height: 100%;
103
+ `;
104
+
105
+ export const FileDiffContainer = styled.div`
101
106
  top: 5rem;
102
107
  `;
103
108
 
104
- export const FileDiffContent = styled.ul`
109
+ export const FileDiffContent = styled.ul<{ gap?: number }>`
105
110
  overflow: auto;
106
- @supports (-moz-appearance: none) {
107
- max-height: calc(100vh - 11rem);
111
+ ${(props) => {
112
+ if (props.gap) {
113
+ return `
114
+ @supports (-moz-appearance: none) {
115
+ max-height: calc(100vh - ${props.gap}rem);
108
116
  }
109
- max-height: calc(100svh - 11rem);
117
+ max-height: calc(100svh - ${props.gap}rem);
118
+ `;
119
+ }
120
+ }};
110
121
  `;
111
122
 
112
123
  export const Divider = styled.div`
@@ -15,17 +15,14 @@
15
15
  */
16
16
 
17
17
  import { useLocalStorage } from "@scm-manager/ui-api";
18
- import { useCallback, useMemo } from "react";
18
+ import { useCallback, useContext } from "react";
19
+ import { SecondaryNavigationContext } from "./navigation/SecondaryNavigationContext";
19
20
 
20
21
  export const useSecondaryNavigation = (isNavigationCollapsible = true) => {
21
- const [isCollapsed, setCollapsed] = useLocalStorage<boolean>("secondaryNavigation.collapsed", false);
22
- const [isRouteCollapsible, setRouteCollapsible] = useLocalStorage<boolean>("secondaryNavigation.collapsible", true);
22
+ const { collapsible, setCollapsible } = useContext(SecondaryNavigationContext);
23
+ const [isCollapsed, setCollapsed] = useLocalStorage("secondaryNavigation.collapsed", false);
23
24
 
24
- const collapsible = useMemo(
25
- () => isRouteCollapsible && isNavigationCollapsible,
26
- [isNavigationCollapsible, isRouteCollapsible]
27
- );
28
- const collapsed = useMemo(() => collapsible && isCollapsed, [collapsible, isCollapsed]);
25
+ const collapsed = collapsible && isCollapsed;
29
26
 
30
27
  const toggleCollapse = useCallback(() => {
31
28
  if (collapsible) {
@@ -33,13 +30,10 @@ export const useSecondaryNavigation = (isNavigationCollapsible = true) => {
33
30
  }
34
31
  }, [collapsible, setCollapsed]);
35
32
 
36
- return useMemo(
37
- () => ({
38
- collapsed,
39
- collapsible,
40
- setCollapsible: setRouteCollapsible,
41
- toggleCollapse,
42
- }),
43
- [collapsed, collapsible, setRouteCollapsible, toggleCollapse]
44
- );
33
+ return {
34
+ collapsed,
35
+ collapsible,
36
+ setCollapsible,
37
+ toggleCollapse,
38
+ };
45
39
  };
@@ -1,19 +0,0 @@
1
- /*
2
- * Copyright (c) 2020 - present Cloudogu GmbH
3
- *
4
- * This program is free software: you can redistribute it and/or modify it under
5
- * the terms of the GNU Affero General Public License as published by the Free
6
- * Software Foundation, version 3.
7
- *
8
- * This program is distributed in the hope that it will be useful, but WITHOUT
9
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
- * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
- * details.
12
- *
13
- * You should have received a copy of the GNU Affero General Public License
14
- * along with this program. If not, see https://www.gnu.org/licenses/.
15
- */
16
-
17
- import React from "react";
18
-
19
- export const SecondaryNavigationContext = React.createContext(false);