@scm-manager/ui-components 4.0.0-REACT18-20250701-125025 → 4.0.0-REACT19

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.
Files changed (124) hide show
  1. package/package.json +46 -51
  2. package/src/BranchSelector.stories.tsx +60 -11
  3. package/src/Breadcrumb.stories.tsx +131 -57
  4. package/src/Breadcrumb.tsx +3 -3
  5. package/src/CardColumn.stories.tsx +94 -27
  6. package/src/CardColumnSmall.stories.tsx +102 -27
  7. package/src/CommaSeparatedList.tsx +2 -2
  8. package/src/Date.stories.tsx +64 -17
  9. package/src/Duration.stories.tsx +92 -45
  10. package/src/ErrorBoundary.tsx +38 -58
  11. package/src/ErrorNotification.tsx +1 -1
  12. package/src/Help.stories.tsx +61 -17
  13. package/src/Help.tsx +5 -11
  14. package/src/Icon.stories.tsx +93 -13
  15. package/src/LinkPaginator.tsx +9 -6
  16. package/src/Loading.stories.tsx +26 -6
  17. package/src/Logo.stories.tsx +44 -13
  18. package/src/Notification.stories.tsx +111 -23
  19. package/src/OverviewPageActions.tsx +5 -5
  20. package/src/Paginator.test.tsx +115 -94
  21. package/src/PdfViewer.stories.tsx +83 -5
  22. package/src/PreformattedCodeBlock.stories.tsx +97 -26
  23. package/src/ProtectedRoute.tsx +14 -39
  24. package/src/SmallLoadingSpinner.stories.tsx +26 -6
  25. package/src/SyntaxHighlighter.stories.tsx +122 -40
  26. package/src/SyntaxHighlighterRenderer.tsx +7 -7
  27. package/src/Tag.stories.tsx +135 -18
  28. package/src/Tag.tsx +2 -1
  29. package/src/Tooltip.stories.tsx +147 -34
  30. package/src/__snapshots__/storyshots.test.ts.snap +6 -112
  31. package/src/avatar/Avatar.ts +1 -1
  32. package/src/avatar/AvatarImage.tsx +1 -1
  33. package/src/avatar/AvatarWrapper.tsx +2 -2
  34. package/src/buttons/Button.stories.tsx +159 -0
  35. package/src/buttons/Button.tsx +5 -5
  36. package/src/config/ConfigurationBinder.tsx +39 -39
  37. package/src/config/ConfigurationForm.tsx +2 -1
  38. package/src/config/TitledSettings.tsx +4 -1
  39. package/src/forms/AddKeyValueEntryToTableField.stories.tsx +60 -25
  40. package/src/forms/Checkbox.stories.tsx +165 -27
  41. package/src/forms/Checkbox.tsx +1 -0
  42. package/src/forms/DropDown.stories.tsx +81 -35
  43. package/src/forms/FileInput.stories.tsx +85 -10
  44. package/src/forms/InputField.stories.tsx +210 -37
  45. package/src/forms/InputField.tsx +1 -0
  46. package/src/forms/Radio.stories.tsx +181 -34
  47. package/src/forms/Radio.tsx +1 -0
  48. package/src/forms/Select.stories.tsx +266 -46
  49. package/src/forms/Select.tsx +1 -0
  50. package/src/forms/Textarea.stories.tsx +99 -53
  51. package/src/forms/Textarea.tsx +1 -0
  52. package/src/forms/index.ts +3 -1
  53. package/src/index.ts +5 -5
  54. package/src/jest-dom.d.ts +17 -0
  55. package/src/layout/Footer.stories.tsx +114 -22
  56. package/src/layout/FooterSection.tsx +1 -0
  57. package/src/layout/Header.tsx +2 -1
  58. package/src/layout/Page.tsx +1 -3
  59. package/src/layout/PrimaryContentColumn.tsx +2 -2
  60. package/src/layout/SecondaryNavigationColumn.tsx +3 -3
  61. package/src/layout/SubSubtitle.tsx +2 -1
  62. package/src/layout/Subtitle.tsx +2 -1
  63. package/src/layout/Title.tsx +2 -5
  64. package/src/markdown/LazyMarkdownView.tsx +16 -5
  65. package/src/markdown/MarkdownHeadingRenderer.test.ts +9 -1
  66. package/src/markdown/MarkdownHeadingRenderer.tsx +3 -3
  67. package/src/markdown/MarkdownImageRenderer.test.ts +8 -1
  68. package/src/markdown/MarkdownImageRenderer.tsx +2 -1
  69. package/src/markdown/MarkdownLinkRenderer.test.tsx +9 -0
  70. package/src/markdown/MarkdownLinkRenderer.tsx +6 -4
  71. package/src/markdown/MarkdownView.stories.tsx +224 -72
  72. package/src/markdown/markdownExtensions.ts +6 -4
  73. package/src/modals/ConfirmAlert.stories.tsx +133 -44
  74. package/src/modals/ConfirmAlert.tsx +5 -4
  75. package/src/modals/Modal.stories.tsx +461 -252
  76. package/src/modals/Modal.tsx +12 -11
  77. package/src/modals/useRegisterModal.test.tsx +5 -4
  78. package/src/navigation/MenuContext.tsx +2 -2
  79. package/src/navigation/NavLink.tsx +4 -7
  80. package/src/navigation/PrimaryNavigation.tsx +2 -2
  81. package/src/navigation/PrimaryNavigationLink.tsx +6 -5
  82. package/src/navigation/RoutingProps.ts +1 -3
  83. package/src/navigation/SecondaryNavigation.stories.tsx +157 -45
  84. package/src/navigation/SecondaryNavigation.tsx +17 -16
  85. package/src/navigation/SecondaryNavigationItem.tsx +2 -1
  86. package/src/navigation/SubNavigation.tsx +4 -8
  87. package/src/navigation/useActiveMatch.ts +20 -22
  88. package/src/navigation/useNavigationLock.ts +1 -0
  89. package/src/popover/Popover.stories.tsx +111 -41
  90. package/src/popover/Popover.tsx +2 -5
  91. package/src/repos/Diff.stories.tsx +418 -223
  92. package/src/repos/Diff.tsx +1 -5
  93. package/src/repos/HunkExpandDivider.tsx +2 -2
  94. package/src/repos/LoadingDiff.tsx +11 -6
  95. package/src/repos/RepositoryEntry.stories.tsx +217 -53
  96. package/src/repos/RepositoryFlag.tsx +2 -1
  97. package/src/repos/TokenizedDiffView.tsx +4 -2
  98. package/src/repos/annotate/Annotate.stories.tsx +225 -111
  99. package/src/repos/annotate/AnnotateLine.tsx +2 -1
  100. package/src/repos/changesets/ChangesetAuthor.tsx +2 -2
  101. package/src/repos/changesets/ChangesetButtonGroup.test.tsx +9 -5
  102. package/src/repos/changesets/ChangesetDiff.test.ts +10 -2
  103. package/src/repos/changesets/Changesets.stories.tsx +388 -197
  104. package/src/repos/changesets/ContributorRow.tsx +2 -2
  105. package/src/repos/changesets/SignatureIcon.tsx +1 -0
  106. package/src/repos/diff/DiffFileTree.tsx +1 -1
  107. package/src/repos/diff/styledElements.tsx +4 -3
  108. package/src/repos/index.ts +15 -15
  109. package/src/search/Hit.tsx +3 -2
  110. package/src/search/TextHitField.stories.tsx +131 -43
  111. package/src/search/TextHitField.tsx +3 -2
  112. package/src/search/index.ts +1 -1
  113. package/src/storyshots.test.ts +66 -60
  114. package/src/table/Table.stories.tsx +146 -48
  115. package/src/table/Table.tsx +7 -8
  116. package/src/table/TextColumn.tsx +0 -9
  117. package/src/toast/Toast.tsx +2 -1
  118. package/src/toast/ToastArea.tsx +2 -2
  119. package/src/toast/ToastButton.tsx +2 -1
  120. package/src/toast/ToastButtons.tsx +4 -2
  121. package/src/toast/ToastNotification.tsx +2 -1
  122. package/src/toast/index.stories.tsx +144 -39
  123. package/src/toast/index.ts +1 -1
  124. package/src/buttons/index.stories.tsx +0 -85
@@ -14,12 +14,57 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
+ // import React from "react";
18
+ // import { MemoryRouter } from "react-router-dom";
19
+ // import { storiesOf } from "@storybook/react";
20
+ // import CardColumnSmall from "./CardColumnSmall";
21
+ // import Icon from "./Icon";
22
+ // import styled from "styled-components";
23
+ //
24
+ // const Wrapper = styled.div`
25
+ // margin: 2rem;
26
+ // max-width: 400px;
27
+ // `;
28
+ //
29
+ // const link = "/foo/bar";
30
+ // const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
31
+ // const contentLeft = <strong className="m-0">main content</strong>;
32
+ // const contentRight = <small>more text</small>;
33
+ //
34
+ // storiesOf("CardColumnSmall", module)
35
+ // .addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
36
+ // .addDecorator((storyFn) => <Wrapper>{storyFn()}</Wrapper>)
37
+ // .add("Default", () => (
38
+ // <CardColumnSmall link={link} avatar={avatar} contentLeft={contentLeft} contentRight={contentRight} />
39
+ // ))
40
+ // .add("Minimal", () => <CardColumnSmall link={link} contentLeft={contentLeft} contentRight={contentRight} />)
41
+ // .add("Task", () => (
42
+ // <CardColumnSmall
43
+ // link={link}
44
+ // avatar={<Icon name="exchange-alt" className="fa-fw fa-lg" color="inherit" alt="avatar" />}
45
+ // contentLeft={<strong>Repository created</strong>}
46
+ // contentRight={<small>over 42 years ago</small>}
47
+ // footer="New: scmadmin/spaceship"
48
+ // />
49
+ // ))
50
+ // .add("Linkless", () => (
51
+ // <CardColumnSmall
52
+ // avatar={<Icon name="eraser" className="fa-fw fa-lg" color="inherit" alt="avatar" />}
53
+ // contentLeft={<strong>Repository deleted</strong>}
54
+ // contentRight={<small>over 1337 minutes ago</small>}
55
+ // footer="Deleted: scmadmin/spaceship"
56
+ // />
57
+ // ));
58
+
17
59
  import React from "react";
18
- import { MemoryRouter } from "react-router-dom";
19
- import { storiesOf } from "@storybook/react";
60
+ import styled from "styled-components";
61
+ import type { Meta, StoryObj } from "@storybook/react";
62
+
20
63
  import CardColumnSmall from "./CardColumnSmall";
21
64
  import Icon from "./Icon";
22
- import styled from "styled-components";
65
+ import { MemoryRouter } from "react-router-dom";
66
+
67
+ // --- Mock Data and Helpers (preserved from your original story) ---
23
68
 
24
69
  const Wrapper = styled.div`
25
70
  margin: 2rem;
@@ -31,27 +76,57 @@ const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
31
76
  const contentLeft = <strong className="m-0">main content</strong>;
32
77
  const contentRight = <small>more text</small>;
33
78
 
34
- storiesOf("CardColumnSmall", module)
35
- .addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
36
- .addDecorator((storyFn) => <Wrapper>{storyFn()}</Wrapper>)
37
- .add("Default", () => (
38
- <CardColumnSmall link={link} avatar={avatar} contentLeft={contentLeft} contentRight={contentRight} />
39
- ))
40
- .add("Minimal", () => <CardColumnSmall link={link} contentLeft={contentLeft} contentRight={contentRight} />)
41
- .add("Task", () => (
42
- <CardColumnSmall
43
- link={link}
44
- avatar={<Icon name="exchange-alt" className="fa-fw fa-lg" color="inherit" alt="avatar" />}
45
- contentLeft={<strong>Repository created</strong>}
46
- contentRight={<small>over 42 years ago</small>}
47
- footer="New: scmadmin/spaceship"
48
- />
49
- ))
50
- .add("Linkless", () => (
51
- <CardColumnSmall
52
- avatar={<Icon name="eraser" className="fa-fw fa-lg" color="inherit" alt="avatar" />}
53
- contentLeft={<strong>Repository deleted</strong>}
54
- contentRight={<small>over 1337 minutes ago</small>}
55
- footer="Deleted: scmadmin/spaceship"
56
- />
57
- ));
79
+ // --- Storybook Metadata ---
80
+
81
+ const meta: Meta<typeof CardColumnSmall> = {
82
+ title: "Components/CardColumnSmall",
83
+ component: CardColumnSmall,
84
+ // The Wrapper is now a decorator in the meta object
85
+ decorators: [
86
+ (Story) => <MemoryRouter initialEntries={["/"]}>{Story()}</MemoryRouter>,
87
+ (Story) => <Wrapper>{Story()}</Wrapper>],
88
+ tags: ["autodocs"], // Enables automatic documentation generation
89
+ };
90
+
91
+ export default meta;
92
+
93
+ // --- Story Definitions ---
94
+
95
+ type Story = StoryObj<typeof meta>;
96
+
97
+ export const Default: Story = {
98
+ args: {
99
+ // Props for the component go into the `args` object
100
+ link: link,
101
+ avatar: avatar,
102
+ contentLeft: contentLeft,
103
+ contentRight: contentRight,
104
+ },
105
+ };
106
+
107
+ export const Minimal: Story = {
108
+ args: {
109
+ link: link,
110
+ contentLeft: contentLeft,
111
+ contentRight: contentRight,
112
+ },
113
+ };
114
+
115
+ export const Task: Story = {
116
+ args: {
117
+ link: link,
118
+ avatar: <Icon name="exchange-alt" className="fa-fw fa-lg" color="inherit" alt="avatar" />,
119
+ contentLeft: <strong>Repository created</strong>,
120
+ contentRight: <small>over 42 years ago</small>,
121
+ footer: "New: scmadmin/spaceship",
122
+ },
123
+ };
124
+
125
+ export const Linkless: Story = {
126
+ args: {
127
+ avatar: <Icon name="eraser" className="fa-fw fa-lg" color="inherit" alt="avatar" />,
128
+ contentLeft: <strong>Repository deleted</strong>,
129
+ contentRight: <small>over 1337 minutes ago</small>,
130
+ footer: "Deleted: scmadmin/spaceship",
131
+ },
132
+ };
@@ -14,10 +14,10 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { FC } from "react";
17
+ import React, { FC, ReactNode } from "react";
18
18
  import { useTranslation } from "react-i18next";
19
19
 
20
- const CommaSeparatedList: FC = ({ children }) => {
20
+ const CommaSeparatedList: FC<{ children: ReactNode }> = ({ children }) => {
21
21
  const [t] = useTranslation("commons");
22
22
  const childArray = React.Children.toArray(children);
23
23
  return (
@@ -14,10 +14,47 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
+ // import React from "react";
18
+ // import { storiesOf } from "@storybook/react";
19
+ // import DateFromNow from "./DateFromNow";
20
+ // import DateShort from "./DateShort";
21
+ //
22
+ // const baseProps = {
23
+ // timeZone: "Europe/Berlin",
24
+ // baseDate: "2019-10-12T13:56:42+02:00",
25
+ // };
26
+ //
27
+ // const dates = [
28
+ // "2009-06-30T18:30:00+02:00",
29
+ // "2019-06-30T18:30:00+02:00",
30
+ // "2019-10-12T13:56:40+02:00",
31
+ // "2019-10-11T13:56:40+02:00",
32
+ // ];
33
+ //
34
+ // storiesOf("Date", module)
35
+ // .add("Date from now", () => (
36
+ // <div className="p-5">
37
+ // {dates.map((d) => (
38
+ // <p>
39
+ // <DateFromNow date={d} {...baseProps} />
40
+ // </p>
41
+ // ))}
42
+ // </div>
43
+ // ))
44
+ // .add("Short", () => (
45
+ // <div className="p-5">
46
+ // {dates.map((d) => (
47
+ // <p>
48
+ // <DateShort date={d} {...baseProps} />
49
+ // </p>
50
+ // ))}
51
+ // </div>
52
+ // ));
53
+
17
54
  import React from "react";
18
- import { storiesOf } from "@storybook/react";
55
+ import type { Meta, StoryObj } from "@storybook/react";
56
+
19
57
  import DateFromNow from "./DateFromNow";
20
- import DateShort from "./DateShort";
21
58
 
22
59
  const baseProps = {
23
60
  timeZone: "Europe/Berlin",
@@ -31,22 +68,32 @@ const dates = [
31
68
  "2019-10-11T13:56:40+02:00",
32
69
  ];
33
70
 
34
- storiesOf("Date", module)
35
- .add("Date from now", () => (
36
- <div className="p-5">
37
- {dates.map((d) => (
38
- <p>
71
+ const meta: Meta<typeof DateFromNow> = {
72
+ title: "Components/DateFromNow",
73
+ component: DateFromNow,
74
+ decorators: [(Story) => <div className="p-5">{Story()}</div>],
75
+ tags: ["autodocs"],
76
+ };
77
+
78
+ export default meta;
79
+
80
+ type Story = StoryObj<typeof meta>;
81
+
82
+ export const Primary: Story = {
83
+ args: {
84
+ date: dates[3],
85
+ ...baseProps,
86
+ },
87
+ };
88
+
89
+ export const AllExamples: Story = {
90
+ render: () => (
91
+ <div>
92
+ {dates.map((d, i) => (
93
+ <p key={i}>
39
94
  <DateFromNow date={d} {...baseProps} />
40
95
  </p>
41
96
  ))}
42
97
  </div>
43
- ))
44
- .add("Short", () => (
45
- <div className="p-5">
46
- {dates.map((d) => (
47
- <p>
48
- <DateShort date={d} {...baseProps} />
49
- </p>
50
- ))}
51
- </div>
52
- ));
98
+ ),
99
+ };
@@ -14,50 +14,97 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import { storiesOf } from "@storybook/react";
18
- import Duration from "./Duration";
17
+ // import { storiesOf } from "@storybook/react";
18
+ // import Duration from "./Duration";
19
+ // import React from "react";
20
+ //
21
+ // storiesOf("Duration", module).add("Duration", () => (
22
+ // <div className="m-5 p-5">
23
+ // <p>
24
+ // <Duration duration={1} />
25
+ // </p>
26
+ // <p>
27
+ // <Duration duration={500} />
28
+ // </p>
29
+ // <p>
30
+ // <Duration duration={1000 + 1} />
31
+ // </p>
32
+ // <p>
33
+ // <Duration duration={2000} />
34
+ // </p>
35
+ // <p>
36
+ // <Duration duration={1000 * 60 + 1} />
37
+ // </p>
38
+ // <p>
39
+ // <Duration duration={42 * 1000 * 60} />
40
+ // </p>
41
+ // <p>
42
+ // <Duration duration={1000 * 60 * 60 + 1} />
43
+ // </p>
44
+ // <p>
45
+ // <Duration duration={21 * 1000 * 60 * 60} />
46
+ // </p>
47
+ // <p>
48
+ // <Duration duration={1000 * 60 * 60 * 24 + 1} />
49
+ // </p>
50
+ // <p>
51
+ // <Duration duration={5 * 1000 * 60 * 60 * 24} />
52
+ // </p>
53
+ // <p>
54
+ // <Duration duration={1000 * 60 * 60 * 24 * 7 + 1} />
55
+ // </p>
56
+ // <p>
57
+ // <Duration duration={3 * 1000 * 60 * 60 * 24 * 7} />
58
+ // </p>
59
+ // <p>
60
+ // <Duration duration={12 * 1000 * 60 * 60 * 24} />
61
+ // </p>
62
+ // </div>
63
+ // ));
64
+
19
65
  import React from "react";
66
+ import type { Meta, StoryObj } from "@storybook/react";
67
+
68
+ import DateShort from "./DateShort";
69
+
70
+ const baseProps = {
71
+ timeZone: "Europe/Berlin",
72
+ baseDate: "2019-10-12T13:56:42+02:00",
73
+ };
74
+
75
+ const dates = [
76
+ "2009-06-30T18:30:00+02:00",
77
+ "2019-06-30T18:30:00+02:00",
78
+ "2019-10-12T13:56:40+02:00",
79
+ "2019-10-11T13:56:40+02:00",
80
+ ];
81
+
82
+ const meta: Meta<typeof DateShort> = {
83
+ title: "Components/DateShort",
84
+ component: DateShort,
85
+ decorators: [(Story) => <div className="p-5">{Story()}</div>],
86
+ tags: ["autodocs"],
87
+ };
88
+
89
+ export default meta;
90
+
91
+ type Story = StoryObj<typeof meta>;
92
+
93
+ export const Primary: Story = {
94
+ args: {
95
+ date: dates[3],
96
+ ...baseProps,
97
+ },
98
+ };
20
99
 
21
- storiesOf("Duration", module).add("Duration", () => (
22
- <div className="m-5 p-5">
23
- <p>
24
- <Duration duration={1} />
25
- </p>
26
- <p>
27
- <Duration duration={500} />
28
- </p>
29
- <p>
30
- <Duration duration={1000 + 1} />
31
- </p>
32
- <p>
33
- <Duration duration={2000} />
34
- </p>
35
- <p>
36
- <Duration duration={1000 * 60 + 1} />
37
- </p>
38
- <p>
39
- <Duration duration={42 * 1000 * 60} />
40
- </p>
41
- <p>
42
- <Duration duration={1000 * 60 * 60 + 1} />
43
- </p>
44
- <p>
45
- <Duration duration={21 * 1000 * 60 * 60} />
46
- </p>
47
- <p>
48
- <Duration duration={1000 * 60 * 60 * 24 + 1} />
49
- </p>
50
- <p>
51
- <Duration duration={5 * 1000 * 60 * 60 * 24} />
52
- </p>
53
- <p>
54
- <Duration duration={1000 * 60 * 60 * 24 * 7 + 1} />
55
- </p>
56
- <p>
57
- <Duration duration={3 * 1000 * 60 * 60 * 24 * 7} />
58
- </p>
59
- <p>
60
- <Duration duration={12 * 1000 * 60 * 60 * 24} />
61
- </p>
62
- </div>
63
- ));
100
+ export const AllExamples: Story = {
101
+ render: () => (
102
+ <div>
103
+ {dates.map((d, i) => (
104
+ <p key={i}>
105
+ <DateShort date={d} {...baseProps} />
106
+ </p>
107
+ ))}
108
+ </div>
109
+ ),
110
+ };
@@ -14,8 +14,9 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { FC, ReactNode, useEffect } from "react";
18
- import { RouteComponentProps, useLocation, withRouter } from "react-router-dom";
17
+ import React, { ErrorInfo, FC, ReactNode, useEffect } from "react";
18
+ import { useLocation } from "react-router-dom";
19
+ import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary";
19
20
  import { useTranslation } from "react-i18next";
20
21
  import classNames from "classnames";
21
22
  import styled from "styled-components";
@@ -26,37 +27,31 @@ import ErrorPage from "./ErrorPage";
26
27
  import { Subtitle, Title } from "./layout";
27
28
  import Icon from "./Icon";
28
29
 
29
- type State = {
30
- error?: Error;
31
- errorInfo?: ErrorInfo;
30
+ type ErrorState = {
31
+ error: Error;
32
32
  };
33
33
 
34
34
  type ExportedProps = {
35
- fallback?: React.ComponentType<State>;
35
+ fallback?: React.ComponentType<ErrorState>;
36
36
  children: ReactNode;
37
37
  };
38
38
 
39
- type Props = RouteComponentProps & ExportedProps;
40
-
41
- type ErrorInfo = {
42
- componentStack: string;
43
- };
44
-
45
39
  type ErrorDisplayProps = {
46
- fallback?: React.ComponentType<State>;
47
40
  error: Error;
48
- errorInfo: ErrorInfo;
41
+ // react-error-boundary does not pass errorInfo to the fallback component by default
42
+ // for performance reasons, but it can be logged via the `onError` prop.
43
+ // We keep it in the signature for compatibility with the custom fallback prop.
44
+ errorInfo?: { componentStack: string };
45
+ fallback?: React.ComponentType<ErrorState>;
49
46
  };
50
47
 
51
48
  const RedirectIconContainer = styled.div`
52
49
  min-height: 256px;
53
50
  `;
54
51
 
55
- const RedirectPage = () => {
52
+ const RedirectPage: FC = () => {
56
53
  const [t] = useTranslation("commons");
57
54
  useDocumentTitle(t("errorNotification.prefix"));
58
- // we use an icon instead of loading spinner,
59
- // because a redirect is synchron and a spinner does not spin on a synchron action
60
55
  return (
61
56
  <section className="section">
62
57
  <div className="container">
@@ -82,6 +77,7 @@ const ErrorDisplay: FC<ErrorDisplayProps> = ({ error, errorInfo, fallback: Fallb
82
77
  const [t] = useTranslation("commons");
83
78
  const location = useLocation();
84
79
  const isMissingLink = error instanceof MissingLinkError;
80
+
85
81
  useEffect(() => {
86
82
  if (isMissingLink && loginLink) {
87
83
  window.location.assign(urls.withContextPath("/login?from=" + location.pathname));
@@ -90,20 +86,11 @@ const ErrorDisplay: FC<ErrorDisplayProps> = ({ error, errorInfo, fallback: Fallb
90
86
 
91
87
  if (isMissingLink) {
92
88
  if (loginLink) {
93
- // we can render a loading screen,
94
- // because the effect hook above should redirect
95
89
  return <RedirectPage />;
96
- } else {
97
- // missing link error without login link means we have no permissions
98
- // and we should render an error
99
- return (
100
- <ErrorPage error={error} title={t("errorNotification.prefix")} subtitle={t("errorNotification.forbidden")} />
101
- );
102
90
  }
103
- }
104
-
105
- if (!FallbackComponent) {
106
- return <ErrorNotification error={error} />;
91
+ return (
92
+ <ErrorPage error={error} title={t("errorNotification.prefix")} subtitle={t("errorNotification.forbidden")} />
93
+ );
107
94
  }
108
95
 
109
96
  const fallbackProps = {
@@ -111,37 +98,30 @@ const ErrorDisplay: FC<ErrorDisplayProps> = ({ error, errorInfo, fallback: Fallb
111
98
  errorInfo,
112
99
  };
113
100
 
114
- return <FallbackComponent {...fallbackProps} />;
115
- };
116
-
117
- class ErrorBoundary extends React.Component<Props, State> {
118
- constructor(props: Props) {
119
- super(props);
120
- this.state = {};
101
+ if (FallbackComponent) {
102
+ return <FallbackComponent {...fallbackProps} />;
121
103
  }
104
+ return <ErrorNotification error={error} />;
105
+ };
122
106
 
123
- componentDidUpdate(prevProps: Readonly<Props>) {
124
- // we must reset the error if the url has changed
125
- if (this.state.error && prevProps.location !== this.props.location) {
126
- this.setState({ error: undefined, errorInfo: undefined });
127
- }
128
- }
107
+ const ErrorBoundary: FC<ExportedProps> = ({ children, fallback }) => {
108
+ const location = useLocation();
129
109
 
130
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
131
- this.setState({
132
- error,
133
- errorInfo,
134
- });
135
- }
110
+ const ErrorBoundaryFallback = ({ error }: FallbackProps) => (
111
+ <ErrorDisplay
112
+ error={error}
113
+ // For compatibility with potential custom fallbacks that expect errorInfo.
114
+ // The actual component stack is available in the `onError` callback below.
115
+ errorInfo={{ componentStack: "" }}
116
+ fallback={fallback}
117
+ />
118
+ );
136
119
 
137
- render() {
138
- const { fallback } = this.props;
139
- const { error, errorInfo } = this.state;
140
- if (error && errorInfo) {
141
- return <ErrorDisplay error={error} errorInfo={errorInfo} fallback={fallback} />;
142
- }
143
- return this.props.children;
144
- }
145
- }
120
+ return (
121
+ <ReactErrorBoundary resetKeys={[location.pathname]} FallbackComponent={ErrorBoundaryFallback}>
122
+ {children}
123
+ </ReactErrorBoundary>
124
+ );
125
+ };
146
126
 
147
- export default withRouter(ErrorBoundary);
127
+ export default ErrorBoundary;
@@ -19,7 +19,7 @@ import { useTranslation } from "react-i18next";
19
19
  import { BackendError, ForbiddenError, UnauthorizedError, urls } from "@scm-manager/ui-api";
20
20
  import Notification from "./Notification";
21
21
  import BackendErrorNotification from "./BackendErrorNotification";
22
- import { useLocation } from "react-router-dom";
22
+ import { useLocation } from "react-router";
23
23
 
24
24
  type Props = ComponentProps<typeof BasicErrorMessage> & {
25
25
  error?: Error | null;
@@ -14,8 +14,35 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import * as React from "react";
18
- import { storiesOf } from "@storybook/react";
17
+ // import * as React from "react";
18
+ // import { storiesOf } from "@storybook/react";
19
+ // import Help from "./Help";
20
+ //
21
+ // const longContent =
22
+ // "Cleverness nuclear genuine static irresponsibility invited President Zaphod\n" +
23
+ // "Beeblebrox hyperspace ship. Another custard through computer-generated universe\n" +
24
+ // "shapes field strong disaster parties Russell’s ancestors infinite colour\n" +
25
+ // "imaginative generator sweep.";
26
+ //
27
+ // storiesOf("Help", module)
28
+ // .addDecorator((storyFn) => <div className="m-6">{storyFn()}</div>)
29
+ // .add("Default", () => <Help message="This is a help message" />)
30
+ // .add("Multiline", () => (
31
+ // <>
32
+ // <div className="mt-4">
33
+ // <label>With multiline (default):</label>
34
+ // <Help message={longContent} />
35
+ // </div>
36
+ // <div className="mt-4">
37
+ // <label>Without multiline:</label>
38
+ // <Help message={longContent} multiline={false} />
39
+ // </div>
40
+ // </>
41
+ // ));
42
+
43
+ import React from "react";
44
+ import type { Meta, StoryObj } from "@storybook/react";
45
+
19
46
  import Help from "./Help";
20
47
 
21
48
  const longContent =
@@ -24,18 +51,35 @@ const longContent =
24
51
  "shapes field strong disaster parties Russell’s ancestors infinite colour\n" +
25
52
  "imaginative generator sweep.";
26
53
 
27
- storiesOf("Help", module)
28
- .addDecorator((storyFn) => <div className="m-6">{storyFn()}</div>)
29
- .add("Default", () => <Help message="This is a help message" />)
30
- .add("Multiline", () => (
31
- <>
32
- <div className="mt-4">
33
- <label>With multiline (default):</label>
34
- <Help message={longContent} />
35
- </div>
36
- <div className="mt-4">
37
- <label>Without multiline:</label>
38
- <Help message={longContent} multiline={false} />
39
- </div>
40
- </>
41
- ));
54
+ const meta: Meta<typeof Help> = {
55
+ title: "Components/Help",
56
+ component: Help,
57
+ decorators: [(Story) => <div className="m-6">{Story()}</div>],
58
+ parameters: {
59
+ layout: "centered",
60
+ },
61
+ tags: ["autodocs"],
62
+ };
63
+
64
+ export default meta;
65
+
66
+ type Story = StoryObj<typeof meta>;
67
+
68
+ export const Default: Story = {
69
+ args: {
70
+ message: "This is a help message",
71
+ },
72
+ };
73
+
74
+ export const Multiline: Story = {
75
+ args: {
76
+ message: longContent,
77
+ },
78
+ };
79
+
80
+ export const SingleLine: Story = {
81
+ args: {
82
+ message: longContent,
83
+ multiline: false,
84
+ },
85
+ };
package/src/Help.tsx CHANGED
@@ -26,19 +26,13 @@ type Props = {
26
26
  id?: string;
27
27
  };
28
28
 
29
- const Help: FC<Props> = ({ message, multiline, className, id }) => (
30
- <Tooltip
31
- className={classNames("is-inline-block", "pl-1", className)}
32
- message={message}
33
- id={id}
34
- multiline={multiline}
35
- >
29
+ /**
30
+ * @deprecated Please import the identical module from "@scm-manager/ui-core"
31
+ */
32
+ const Help: FC<Props> = ({ message, multiline = true, className, id }) => (
33
+ <Tooltip className={classNames("is-inline-block", "pl-1", className)} message={message} id={id} multiline={multiline}>
36
34
  <HelpIcon />
37
35
  </Tooltip>
38
36
  );
39
37
 
40
- Help.defaultProps = {
41
- multiline: true
42
- };
43
-
44
38
  export default Help;