@scm-manager/ui-components 4.0.0-REACT19 → 4.0.0-REACT19-20250910-113025

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": "4.0.0-REACT19",
3
+ "version": "4.0.0-REACT19-20250910-113025",
4
4
  "description": "UI Components for SCM-Manager and its plugins",
5
5
  "main": "src/index.ts",
6
6
  "files": [
@@ -33,14 +33,14 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@scm-manager/babel-preset": "^2.13.1",
36
- "@scm-manager/eslint-config": "^2.17.0",
36
+ "@scm-manager/eslint-config": "^2.18.2",
37
37
  "@scm-manager/jest-preset": "^2.15.0-alpha1",
38
38
  "@scm-manager/prettier-config": "^2.12.0",
39
39
  "@scm-manager/tsconfig": "^2.13.0",
40
- "@scm-manager/ui-shortcuts": "4.0.0-REACT19",
41
- "@scm-manager/ui-syntaxhighlighting": "4.0.0-REACT19",
42
- "@scm-manager/ui-text": "4.0.0-REACT19",
43
- "@scm-manager/ui-types": "4.0.0-REACT19",
40
+ "@scm-manager/ui-shortcuts": "4.0.0-REACT19-20250910-113025",
41
+ "@scm-manager/ui-syntaxhighlighting": "4.0.0-REACT19-20250910-113025",
42
+ "@scm-manager/ui-text": "4.0.0-REACT19-20250910-113025",
43
+ "@scm-manager/ui-types": "4.0.0-REACT19-20250910-113025",
44
44
  "@storybook/addon-actions": "^9.0.8",
45
45
  "@storybook/addon-docs": "^9.1.5",
46
46
  "@storybook/addon-essentials": "^9.0.0-alpha.12",
@@ -67,12 +67,12 @@
67
67
  "storybook-addon-themes": "^6.1.0"
68
68
  },
69
69
  "dependencies": {
70
- "@scm-manager/ui-api": "4.0.0-REACT19",
71
- "@scm-manager/ui-buttons": "4.0.0-REACT19",
72
- "@scm-manager/ui-core": "4.0.0-REACT19",
73
- "@scm-manager/ui-extensions": "4.0.0-REACT19",
74
- "@scm-manager/ui-layout": "4.0.0-REACT19",
75
- "@scm-manager/ui-overlays": "4.0.0-REACT19",
70
+ "@scm-manager/ui-api": "4.0.0-REACT19-20250910-113025",
71
+ "@scm-manager/ui-buttons": "4.0.0-REACT19-20250910-113025",
72
+ "@scm-manager/ui-core": "4.0.0-REACT19-20250910-113025",
73
+ "@scm-manager/ui-extensions": "4.0.0-REACT19-20250910-113025",
74
+ "@scm-manager/ui-layout": "4.0.0-REACT19-20250910-113025",
75
+ "@scm-manager/ui-overlays": "4.0.0-REACT19-20250910-113025",
76
76
  "deepmerge": "^4.2.2",
77
77
  "hast-util-sanitize": "^3.0.2",
78
78
  "react-diff-view": "^2.4.10",
@@ -104,4 +104,4 @@
104
104
  "publishConfig": {
105
105
  "access": "public"
106
106
  }
107
- }
107
+ }
@@ -16,7 +16,7 @@
16
16
 
17
17
  import React, { FC, useState } from "react";
18
18
  import { useTranslation } from "react-i18next";
19
- import { Link, useNavigate, useLocation } from "react-router-dom";
19
+ import { Link, useLocation, useNavigate } from "react-router-dom";
20
20
  import classNames from "classnames";
21
21
  import styled from "styled-components";
22
22
  import { urls } from "@scm-manager/ui-api";
@@ -145,9 +145,8 @@ const Breadcrumb: FC<Props> = ({
145
145
 
146
146
  const pathSection = () => {
147
147
  if (path) {
148
- const paths = path.split("/");
148
+ const paths = path.split("/").filter((pathFragment) => pathFragment !== "");
149
149
  return paths.map((pathFragment, index) => {
150
- const decodedPathFragment = decodeURIComponent(pathFragment);
151
150
  let currPath = paths
152
151
  .slice(0, index + 1)
153
152
  .map(encodeURIComponent)
@@ -158,7 +157,7 @@ const Breadcrumb: FC<Props> = ({
158
157
  if (paths.length - 1 === index) {
159
158
  return (
160
159
  <li className="is-active" key={index}>
161
- <BreadcrumbNode clickable={clickable} text={decodedPathFragment} url="#" current={true} />
160
+ <BreadcrumbNode clickable={clickable} text={pathFragment} url="#" current={true} />
162
161
  </li>
163
162
  );
164
163
  }
@@ -166,7 +165,7 @@ const Breadcrumb: FC<Props> = ({
166
165
  <li key={index}>
167
166
  <BreadcrumbNode
168
167
  clickable={clickable}
169
- text={decodedPathFragment}
168
+ text={pathFragment}
170
169
  url={baseUrl + "/" + encodeURIComponent(revision) + "/" + currPath}
171
170
  />
172
171
  </li>
@@ -180,7 +179,7 @@ const Breadcrumb: FC<Props> = ({
180
179
  navigate(location.pathname);
181
180
  setCopying(true);
182
181
  copyToClipboard(
183
- window.location.protocol + "//" + window.location.host + urls.withContextPath(permalink || location.pathname)
182
+ window.location.protocol + "//" + window.location.host + urls.withContextPath(permalink || location.pathname),
184
183
  ).finally(() => setCopying(false));
185
184
  };
186
185
 
@@ -206,7 +205,7 @@ const Breadcrumb: FC<Props> = ({
206
205
  "my-4",
207
206
  "is-flex",
208
207
  "is-align-items-center",
209
- "is-full-width"
208
+ "is-full-width",
210
209
  )}
211
210
  aria-label="breadcrumbs"
212
211
  >
@@ -303,7 +302,7 @@ const Breadcrumb: FC<Props> = ({
303
302
  "is-justify-content-flex-start",
304
303
  "is-align-self-center",
305
304
  "m-2",
306
- "is-flex-wrap-wrap"
305
+ "is-flex-wrap-wrap",
307
306
  )}
308
307
  >
309
308
  {renderExtensionPoints()}
@@ -14,7 +14,7 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { ReactNode, useCallback } from "react";
17
+ import React, { FC, ReactNode, Ref, useCallback } from "react";
18
18
  import classNames from "classnames";
19
19
  import styled from "styled-components";
20
20
  import { Link } from "react-router-dom";
@@ -29,6 +29,7 @@ type Props = {
29
29
  footerRight: ReactNode;
30
30
  action?: () => void;
31
31
  className?: string;
32
+ ref?: Ref<HTMLElement>;
32
33
  };
33
34
 
34
35
  const NoEventWrapper = styled.article`
@@ -48,72 +49,81 @@ const InvisibleButton = styled.button`
48
49
  cursor: pointer;
49
50
  `;
50
51
 
51
- const CardColumn = React.forwardRef<HTMLElement, Props>(
52
- ({ link, avatar, title, description, contentRight, footerLeft, footerRight, action, className }, ref) => {
53
- const renderAvatar = avatar ? <figure className="media-left mt-3 ml-4">{avatar}</figure> : null;
54
- const renderDescription = description ? <p className="shorten-text">{description}</p> : null;
55
- const renderContentRight = contentRight ? <div className="ml-auto">{contentRight}</div> : null;
56
- const executeRef = useCallback(
57
- (el: HTMLButtonElement | HTMLAnchorElement | null) => {
58
- if (typeof ref === "function") {
59
- ref(el);
60
- } else if (ref) {
61
- ref.current = el;
62
- }
63
- },
64
- [ref]
65
- );
52
+ const CardColumn: FC<Props> = ({
53
+ link,
54
+ avatar,
55
+ title,
56
+ description,
57
+ contentRight,
58
+ footerLeft,
59
+ footerRight,
60
+ action,
61
+ className,
62
+ ref,
63
+ }) => {
64
+ const renderAvatar = avatar ? <figure className="media-left mt-3 ml-4">{avatar}</figure> : null;
65
+ const renderDescription = description ? <p className="shorten-text">{description}</p> : null;
66
+ const renderContentRight = contentRight ? <div className="ml-auto">{contentRight}</div> : null;
67
+ const executeRef = useCallback(
68
+ (el: HTMLButtonElement | HTMLAnchorElement | null) => {
69
+ if (typeof ref === "function") {
70
+ ref(el);
71
+ } else if (ref) {
72
+ ref.current = el;
73
+ }
74
+ },
75
+ [ref],
76
+ );
66
77
 
67
- let createLink = null;
68
- if (link) {
69
- createLink = <Link ref={executeRef} className="overlay-column" to={link} />;
70
- } else if (action) {
71
- createLink = (
72
- <InvisibleButton
73
- ref={executeRef}
74
- className="overlay-column"
75
- onClick={(e) => {
76
- e.preventDefault();
77
- action();
78
- }}
79
- tabIndex={0}
80
- />
81
- );
82
- }
78
+ let createLink = null;
79
+ if (link) {
80
+ createLink = <Link ref={executeRef} className="overlay-column" to={link} />;
81
+ } else if (action) {
82
+ createLink = (
83
+ <InvisibleButton
84
+ ref={executeRef}
85
+ className="overlay-column"
86
+ onClick={(e) => {
87
+ e.preventDefault();
88
+ action();
89
+ }}
90
+ tabIndex={0}
91
+ />
92
+ );
93
+ }
83
94
 
84
- return (
85
- <>
86
- {createLink}
87
- <NoEventWrapper className={classNames("media", className)}>
88
- {renderAvatar}
89
- <div
90
- className={classNames(
91
- "media-content",
92
- "text-box",
93
- "is-flex",
94
- "is-flex-direction-column",
95
- "is-justify-content-space-around",
96
- "is-align-self-stretch"
97
- )}
98
- >
99
- <div className="is-flex">
100
- <div className="is-clipped mb-0">
101
- <p className="shorten-text m-0">{title}</p>
102
- {renderDescription}
103
- </div>
104
- {renderContentRight}
105
- </div>
106
- <div className={classNames("level", "is-flex", "pb-4")}>
107
- <div className={classNames("level-left", "is-hidden-mobile", "mr-2")}>{footerLeft}</div>
108
- <InheritFlexShrinkDiv className="level-right is-block is-mobile m-0 shorten-text">
109
- {footerRight}
110
- </InheritFlexShrinkDiv>
95
+ return (
96
+ <>
97
+ {createLink}
98
+ <NoEventWrapper className={classNames("media", className)}>
99
+ {renderAvatar}
100
+ <div
101
+ className={classNames(
102
+ "media-content",
103
+ "text-box",
104
+ "is-flex",
105
+ "is-flex-direction-column",
106
+ "is-justify-content-space-around",
107
+ "is-align-self-stretch",
108
+ )}
109
+ >
110
+ <div className="is-flex">
111
+ <div className="is-clipped mb-0">
112
+ <p className="shorten-text m-0">{title}</p>
113
+ {renderDescription}
111
114
  </div>
115
+ {renderContentRight}
112
116
  </div>
113
- </NoEventWrapper>
114
- </>
115
- );
116
- }
117
- );
117
+ <div className={classNames("level", "is-flex", "pb-4")}>
118
+ <div className={classNames("level-left", "is-hidden-mobile", "mr-2")}>{footerLeft}</div>
119
+ <InheritFlexShrinkDiv className="level-right is-block is-mobile m-0 shorten-text">
120
+ {footerRight}
121
+ </InheritFlexShrinkDiv>
122
+ </div>
123
+ </div>
124
+ </NoEventWrapper>
125
+ </>
126
+ );
127
+ };
118
128
 
119
129
  export default CardColumn;
@@ -118,6 +118,7 @@ export default meta;
118
118
 
119
119
  // --- Story-Definitionen ---
120
120
 
121
+ // @ts-ignore This type is too complex, no clue what to do about it
121
122
  type Story = StoryObj<typeof meta>;
122
123
 
123
124
  export const Loading: Story = {
@@ -14,7 +14,7 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { KeyboardEvent, MouseEvent, ReactNode, useCallback } from "react";
17
+ import React, { ButtonHTMLAttributes, FC, KeyboardEvent, MouseEvent, ReactNode, Ref, useCallback } from "react";
18
18
  import classNames from "classnames";
19
19
  import { Link } from "react-router";
20
20
  import Icon from "../Icon";
@@ -35,103 +35,101 @@ export type ButtonProps = {
35
35
  testId?: string;
36
36
  };
37
37
 
38
- type Props = ButtonProps & {
39
- type?: "button" | "submit" | "reset";
40
- color?: string;
41
- };
38
+ type Props = ButtonProps &
39
+ ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> & {
40
+ type?: "button" | "submit" | "reset";
41
+ color?: string;
42
+ ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;
43
+ };
42
44
 
43
45
  /**
44
46
  * @deprecated Use {@link ui-core/src/base/buttons/Button} instead
45
47
  */
46
- const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
47
- (
48
- {
49
- link,
50
- className,
51
- icon,
52
- fullWidth,
53
- reducedMobile,
54
- testId,
55
- children,
56
- label,
57
- type = "button",
58
- title,
59
- loading,
60
- disabled,
61
- action,
62
- color = "default",
48
+ const Button: FC<Props> = ({
49
+ link,
50
+ className,
51
+ icon,
52
+ fullWidth,
53
+ reducedMobile,
54
+ testId,
55
+ children,
56
+ label,
57
+ type = "button",
58
+ title,
59
+ loading,
60
+ disabled,
61
+ action,
62
+ color = "default",
63
+ ref,
64
+ }) => {
65
+ const executeRef = useCallback(
66
+ (el: HTMLButtonElement | HTMLAnchorElement | null) => {
67
+ if (typeof ref === "function") {
68
+ ref(el);
69
+ } else if (ref) {
70
+ ref.current = el;
71
+ }
63
72
  },
64
- ref,
65
- ) => {
66
- const executeRef = useCallback(
67
- (el: HTMLButtonElement | HTMLAnchorElement | null) => {
68
- if (typeof ref === "function") {
69
- ref(el);
70
- } else if (ref) {
71
- ref.current = el;
72
- }
73
- },
74
- [ref],
73
+ [ref],
74
+ );
75
+ const renderIcon = () => {
76
+ return (
77
+ <>
78
+ {icon ? (
79
+ <Icon name={icon} color="inherit" className={classNames("is-medium", { "pr-5": label || children })} />
80
+ ) : null}
81
+ </>
75
82
  );
76
- const renderIcon = () => {
77
- return (
78
- <>
79
- {icon ? (
80
- <Icon name={icon} color="inherit" className={classNames("is-medium", { "pr-5": label || children })} />
81
- ) : null}
82
- </>
83
- );
84
- };
83
+ };
85
84
 
86
- const classes = classNames(
87
- "button",
88
- "is-" + color,
89
- { "is-loading": loading },
90
- { "is-fullwidth": fullWidth },
91
- { "is-reduced-mobile": reducedMobile },
92
- className,
93
- );
85
+ const classes = classNames(
86
+ "button",
87
+ "is-" + color,
88
+ { "is-loading": loading },
89
+ { "is-fullwidth": fullWidth },
90
+ { "is-reduced-mobile": reducedMobile },
91
+ className,
92
+ );
94
93
 
95
- const content = (
96
- <span>
97
- {renderIcon()}{" "}
98
- {(label || children) && (
99
- <>
100
- {label} {children}
101
- </>
102
- )}
103
- </span>
104
- );
94
+ const content = (
95
+ <span>
96
+ {renderIcon()}{" "}
97
+ {(label || children) && (
98
+ <>
99
+ {label} {children}
100
+ </>
101
+ )}
102
+ </span>
103
+ );
105
104
 
106
- if (link && !disabled) {
107
- if (link.includes("://")) {
108
- return (
109
- <a ref={executeRef} className={classes} href={link} aria-label={label}>
110
- {content}
111
- </a>
112
- );
113
- }
105
+ if (link && !disabled) {
106
+ if (link.includes("://")) {
114
107
  return (
115
- <Link ref={executeRef} className={classes} to={link} aria-label={label}>
108
+ <a ref={executeRef} className={classes} href={link} aria-label={label}>
116
109
  {content}
117
- </Link>
110
+ </a>
118
111
  );
119
112
  }
120
-
121
113
  return (
122
- <button
123
- type={type}
124
- title={title}
125
- disabled={disabled}
126
- onClick={(event) => action && action(event)}
127
- className={classes}
128
- ref={executeRef}
129
- {...createAttributesForTesting(testId)}
130
- >
114
+ <Link ref={executeRef} className={classes} to={link} aria-label={label}>
131
115
  {content}
132
- </button>
116
+ </Link>
133
117
  );
134
- },
135
- );
118
+ }
119
+
120
+ return (
121
+ <button
122
+ type={type}
123
+ title={title}
124
+ disabled={disabled}
125
+ onClick={(event) => action && action(event)}
126
+ className={classes}
127
+ ref={executeRef}
128
+ {...createAttributesForTesting(testId)}
129
+ >
130
+ {content}
131
+ </button>
132
+ );
133
+ };
136
134
 
137
135
  export default Button;
@@ -20,22 +20,18 @@ import { NavLink } from "../navigation";
20
20
  import { Route } from "react-router-dom";
21
21
  import { useTranslation, WithTranslation, withTranslation } from "react-i18next";
22
22
  import { Link, Links, Namespace, Repository } from "@scm-manager/ui-types";
23
- import { urls } from "@scm-manager/ui-api";
24
23
  import {
25
24
  TitledGlobalSettingComponent,
26
25
  TitledNamespaceSettingComponent,
27
26
  TitledRepositorySettingComponent,
28
27
  } from "./TitledSettings";
28
+ import { NAMESPACE_SUB_ROUTES, useNamespaceFromParams } from "@scm-manager/ui-core";
29
29
 
30
30
  type GlobalRouteProps = {
31
31
  url: string;
32
32
  links: Links;
33
33
  };
34
34
 
35
- type NamespaceRouteProps = {
36
- namespace: Namespace;
37
- };
38
-
39
35
  type RepositoryRouteProps = {
40
36
  url: string;
41
37
  repository: Repository;
@@ -58,7 +54,7 @@ class ConfigurationBinder {
58
54
  }
59
55
 
60
56
  route(path: string, Component: any) {
61
- return <Route path={urls.escapeUrlForRoute(path)} element={Component} />;
57
+ return <Route path={path} element={Component} />;
62
58
  }
63
59
 
64
60
  bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any, sortKey?: string) {
@@ -82,7 +78,7 @@ class ConfigurationBinder {
82
78
 
83
79
  if (link) {
84
80
  return this.route(
85
- "/settings" + to,
81
+ "settings" + to,
86
82
  <TitledGlobalSettingComponent i18nNamespace={this.i18nNamespace} label={labelI18nKey}>
87
83
  <ConfigurationComponent link={(link as Link).href} {...additionalProps} />
88
84
  </TitledGlobalSettingComponent>,
@@ -105,7 +101,7 @@ class ConfigurationBinder {
105
101
 
106
102
  const AdminNavLink = withTranslation(this.i18nNamespace)(
107
103
  ({ t }: WithTranslation & extensionPoints.AdminNavigation["props"]) =>
108
- this.navLink(to, labelI18nKey, t, { icon }),
104
+ this.navLink("/admin/" + to, labelI18nKey, t, { icon }),
109
105
  );
110
106
  binder.bind("admin.navigation", AdminNavLink, predicate);
111
107
 
@@ -171,7 +167,7 @@ class ConfigurationBinder {
171
167
 
172
168
  if (link) {
173
169
  return this.route(
174
- "/settings" + to,
170
+ "settings" + to,
175
171
  <TitledRepositorySettingComponent
176
172
  i18nNamespace={this.i18nNamespace}
177
173
  label={labelI18nKey}
@@ -193,28 +189,25 @@ class ConfigurationBinder {
193
189
  linkName: string,
194
190
  ConfigurationComponent: ComponentType<{ link: string; namespace: Namespace }>,
195
191
  ) {
196
- const namespacePredicate = (props: (extensionPoints.NamespaceSetting | extensionPoints.NamespaceRoute)["props"]) =>
192
+ const namespacePredicate = (props: extensionPoints.NamespaceSetting["props"]) =>
197
193
  props.namespace && props.namespace._links && props.namespace._links[linkName];
198
194
 
199
- const NamespaceNavLink: FC<extensionPoints.NamespaceRoute["props"]> = () => {
195
+ const NamespaceNavLink: FC<extensionPoints.NamespaceSetting["props"]> = () => {
200
196
  const [t] = useTranslation(this.i18nNamespace);
201
- return this.navLink("/settings" + to, labelI18nKey, t, { activeOnlyWhenExact: false });
197
+ return this.navLink("/" + NAMESPACE_SUB_ROUTES.SETTINGS + to, labelI18nKey, t, { activeOnlyWhenExact: false });
202
198
  };
203
199
 
204
200
  binder.bind<extensionPoints.NamespaceSetting>("namespace.setting", NamespaceNavLink, namespacePredicate);
205
201
 
206
- const NamespaceRoute = ({ namespace, ...additionalProps }: NamespaceRouteProps) => {
202
+ const NamespaceRoute = ({ ...additionalProps }: NamespaceNavProps) => {
203
+ const namespace = useNamespaceFromParams();
207
204
  if (namespace) {
208
205
  const link = namespace._links[linkName];
209
206
 
210
207
  if (link) {
211
208
  return this.route(
212
- "/settings" + to,
213
- <TitledNamespaceSettingComponent
214
- i18nNamespace={this.i18nNamespace}
215
- label={labelI18nKey}
216
- namespace={namespace}
217
- >
209
+ NAMESPACE_SUB_ROUTES.SETTINGS + to,
210
+ <TitledNamespaceSettingComponent i18nNamespace={this.i18nNamespace} label={labelI18nKey}>
218
211
  <ConfigurationComponent namespace={namespace} link={(link as Link).href} {...additionalProps} />
219
212
  </TitledNamespaceSettingComponent>,
220
213
  );
@@ -223,7 +216,7 @@ class ConfigurationBinder {
223
216
  return null;
224
217
  };
225
218
 
226
- binder.bind("namespace.route", NamespaceRoute, namespacePredicate);
219
+ binder.bind("namespace.route", NamespaceRoute);
227
220
  }
228
221
  }
229
222
 
@@ -16,8 +16,8 @@
16
16
 
17
17
  import React, { FC, ReactNode } from "react";
18
18
  import { useTranslation } from "react-i18next";
19
- import { useDocumentTitle, useDocumentTitleForRepository } from "@scm-manager/ui-core";
20
- import { Namespace, Repository } from "@scm-manager/ui-types";
19
+ import { useDocumentTitle, useDocumentTitleForRepository, useNamespaceFromParams } from "@scm-manager/ui-core";
20
+ import { Repository } from "@scm-manager/ui-types";
21
21
 
22
22
  type GlobalProps = {
23
23
  i18nNamespace: string;
@@ -54,12 +54,12 @@ export const TitledRepositorySettingComponent: FC<RepositoryProps> = ({
54
54
  type NamespaceProps = {
55
55
  i18nNamespace: string;
56
56
  label: string;
57
- namespace: Namespace;
58
57
  children?: ReactNode;
59
58
  };
60
59
 
61
- export const TitledNamespaceSettingComponent: FC<NamespaceProps> = ({ children, i18nNamespace, label, namespace }) => {
60
+ export const TitledNamespaceSettingComponent: FC<NamespaceProps> = ({ children, i18nNamespace, label }) => {
62
61
  const [t] = useTranslation(i18nNamespace);
62
+ const namespace = useNamespaceFromParams();
63
63
  const [commonTranslation] = useTranslation("commons");
64
64
  useDocumentTitle(t(label), commonTranslation("documentTitle.namespaceConfiguration"), namespace.namespace);
65
65
  return <>{children}</>;
@@ -14,7 +14,7 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React, { ChangeEvent, FocusEvent, useState } from "react";
17
+ import React, { ChangeEvent, FC, FocusEvent, useState } from "react";
18
18
  import { useTranslation } from "react-i18next";
19
19
  import classNames from "classnames";
20
20
  import { createAttributesForTesting } from "../devBuild";
@@ -32,69 +32,80 @@ type Props = {
32
32
  testId?: string;
33
33
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
34
34
  onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
35
+ ref?: React.Ref<HTMLInputElement>;
35
36
  };
36
37
 
37
38
  /**
38
39
  * @deprecated
39
40
  */
40
- const FileInput = React.forwardRef<HTMLInputElement, Props>(
41
- ({ name, filenamePlaceholder, testId, helpText, placeholder, disabled, label, className, onBlur, onChange }, ref) => {
42
- const [t] = useTranslation("commons");
43
- const [file, setFile] = useState<File | null>(null);
41
+ const FileInput: FC<Props> = ({
42
+ name,
43
+ filenamePlaceholder,
44
+ testId,
45
+ helpText,
46
+ placeholder,
47
+ disabled,
48
+ label,
49
+ className,
50
+ onBlur,
51
+ onChange,
52
+ ref,
53
+ }) => {
54
+ const [t] = useTranslation("commons");
55
+ const [file, setFile] = useState<File | null>(null);
44
56
 
45
- const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
46
- const uploadedFile = event?.target?.files![0];
47
- // @ts-ignore the uploaded file doesn't match our types
48
- setFile(uploadedFile);
57
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
58
+ const uploadedFile = event?.target?.files![0];
59
+ // @ts-ignore the uploaded file doesn't match our types
60
+ setFile(uploadedFile);
49
61
 
50
- if (onChange && event.target.files) {
51
- onChange(event);
52
- }
53
- };
62
+ if (onChange && event.target.files) {
63
+ onChange(event);
64
+ }
65
+ };
54
66
 
55
- const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
56
- if (onBlur && event.target.files) {
57
- onBlur(event);
58
- }
59
- };
67
+ const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
68
+ if (onBlur && event.target.files) {
69
+ onBlur(event);
70
+ }
71
+ };
60
72
 
61
- const id = createA11yId("file-input");
73
+ const id = createA11yId("file-input");
62
74
 
63
- return (
64
- <div className={classNames("field", className)}>
65
- <LabelWithHelpIcon label={label} helpText={helpText} id={id} />
66
- <div className="file is-info has-name is-fullwidth">
67
- <label className="file-label">
68
- <input
69
- ref={ref}
70
- name={name}
71
- className="file-input"
72
- type="file"
73
- placeholder={placeholder}
74
- disabled={disabled}
75
- onChange={handleChange}
76
- onBlur={handleBlur}
77
- aria-describedby={id}
78
- {...createAttributesForTesting(testId)}
79
- />
80
- <span className="file-cta">
81
- <span className="file-icon">
82
- <i className="fas fa-arrow-circle-up" />
83
- </span>
84
- <span className="file-label has-text-weight-bold">{t("fileInput.label")}</span>
75
+ return (
76
+ <div className={classNames("field", className)}>
77
+ <LabelWithHelpIcon label={label} helpText={helpText} id={id} />
78
+ <div className="file is-info has-name is-fullwidth">
79
+ <label className="file-label">
80
+ <input
81
+ ref={ref}
82
+ name={name}
83
+ className="file-input"
84
+ type="file"
85
+ placeholder={placeholder}
86
+ disabled={disabled}
87
+ onChange={handleChange}
88
+ onBlur={handleBlur}
89
+ aria-describedby={id}
90
+ {...createAttributesForTesting(testId)}
91
+ />
92
+ <span className="file-cta">
93
+ <span className="file-icon">
94
+ <i className="fas fa-arrow-circle-up" />
85
95
  </span>
86
- {file?.name ? (
87
- <span className="file-name">{file?.name}</span>
88
- ) : (
89
- <span className="file-name has-text-weight-light has-text-secondary">
90
- {filenamePlaceholder || t("fileInput.noFileChosen")}
91
- </span>
92
- )}
93
- </label>
94
- </div>
96
+ <span className="file-label has-text-weight-bold">{t("fileInput.label")}</span>
97
+ </span>
98
+ {file?.name ? (
99
+ <span className="file-name">{file?.name}</span>
100
+ ) : (
101
+ <span className="file-name has-text-weight-light has-text-secondary">
102
+ {filenamePlaceholder || t("fileInput.noFileChosen")}
103
+ </span>
104
+ )}
105
+ </label>
95
106
  </div>
96
- );
97
- }
98
- );
107
+ </div>
108
+ );
109
+ };
99
110
 
100
111
  export default FileInput;
@@ -54,7 +54,7 @@ export type InnerRefProps<Base extends MinimumBaseProps, ElementType extends HTM
54
54
  * @deprecated
55
55
  */
56
56
  export const isUsingRef = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
57
- props: Partial<FieldProps<Base, ElementType, ValueType>>
57
+ props: Partial<FieldProps<Base, ElementType, ValueType>>,
58
58
  ): props is InnerRefProps<Base, ElementType> => {
59
59
  return (props as Partial<InnerRefProps<Base, ElementType>>).innerRef !== undefined;
60
60
  };
@@ -63,7 +63,7 @@ export const isUsingRef = <Base extends MinimumBaseProps, ElementType extends HT
63
63
  * @deprecated
64
64
  */
65
65
  export const isLegacy = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
66
- props: FieldProps<Base, ElementType, ValueType>
66
+ props: FieldProps<Base, ElementType, ValueType>,
67
67
  ): props is LegacyProps<Base, ValueType> => {
68
68
  return (props as Partial<InnerRefProps<Base, ElementType>>).innerRef === undefined;
69
69
  };
@@ -93,12 +93,14 @@ export type FieldType<Base extends MinimumBaseProps, ElementType extends HTMLEle
93
93
  /**
94
94
  * @deprecated
95
95
  */
96
- export const createFormFieldWrapper = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
97
- InnerType: FC<FieldProps<Base, ElementType, ValueType>>
98
- ) =>
99
- React.forwardRef<ElementType, LegacyProps<Base, ValueType> | OuterProps<Base, ElementType>>((props, ref) => {
100
- if (ref) {
101
- return <InnerType innerRef={ref} {...(props as RefProps<Base, ElementType>)} />;
96
+ export const createFormFieldWrapper =
97
+ <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
98
+ InnerType: FC<FieldProps<Base, ElementType, ValueType>>,
99
+ ) =>
100
+ (props: LegacyProps<Base, ValueType> | OuterProps<Base, ElementType>) => {
101
+ if ("ref" in props) {
102
+ const { ref, ...rest } = props as OuterProps<Base, ElementType>;
103
+ return <InnerType innerRef={ref} {...(rest as RefProps<Base, ElementType>)} />;
102
104
  }
103
105
  return <InnerType {...(props as LegacyProps<Base, ValueType>)} />;
104
- });
106
+ };
@@ -22,13 +22,10 @@ type BaseProps = {
22
22
  passwordChanged: (p1: string, p2: boolean) => void;
23
23
  passwordValidator?: (p: string) => boolean;
24
24
  onReturnPressed?: () => void;
25
+ ref?: React.Ref<HTMLInputElement>;
25
26
  };
26
27
 
27
- type InnerProps = BaseProps & {
28
- innerRef: React.Ref<HTMLInputElement>;
29
- };
30
-
31
- const PasswordConfirmation: FC<InnerProps> = ({ passwordChanged, passwordValidator, onReturnPressed, innerRef }) => {
28
+ const PasswordConfirmation: FC<BaseProps> = ({ passwordChanged, passwordValidator, onReturnPressed, ref }) => {
32
29
  const [t] = useTranslation("commons");
33
30
  const [password, setPassword] = useState("");
34
31
  const [confirmedPassword, setConfirmedPassword] = useState("");
@@ -51,13 +48,7 @@ const PasswordConfirmation: FC<InnerProps> = ({ passwordChanged, passwordValidat
51
48
  setPasswordConfirmationFailed(password !== newConfirmedPassword);
52
49
  };
53
50
 
54
- const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement> | string) => {
55
- let newPassword;
56
- if (typeof event === "string") {
57
- newPassword = event;
58
- } else {
59
- newPassword = event.target.value;
60
- }
51
+ const handlePasswordChange = (newPassword: string) => {
61
52
  setPasswordConfirmationFailed(newPassword !== confirmedPassword);
62
53
  setPassword(newPassword);
63
54
  setPasswordValid(validatePassword(newPassword));
@@ -69,11 +60,12 @@ const PasswordConfirmation: FC<InnerProps> = ({ passwordChanged, passwordValidat
69
60
  <InputField
70
61
  label={t("password.newPassword")}
71
62
  type="password"
72
- onChange={(event) => handlePasswordChange(event)}
63
+ onChange={handlePasswordChange}
73
64
  value={password}
74
65
  validationError={!passwordValid}
75
66
  errorMessage={t("password.passwordInvalid")}
76
- ref={innerRef}
67
+ // @ts-ignore TODO check this
68
+ ref={ref}
77
69
  onReturnPressed={onReturnPressed}
78
70
  testId="input-password"
79
71
  />
@@ -94,9 +86,4 @@ const PasswordConfirmation: FC<InnerProps> = ({ passwordChanged, passwordValidat
94
86
  );
95
87
  };
96
88
 
97
- /**
98
- * @deprecated
99
- */
100
- export default React.forwardRef<HTMLInputElement, BaseProps>((props, ref) => (
101
- <PasswordConfirmation {...props} innerRef={ref} />
102
- ));
89
+ export default PasswordConfirmation;
@@ -24,13 +24,13 @@ type Props = {
24
24
  links: Links;
25
25
  };
26
26
 
27
- type Appender = (to: string, match: string, label: string, linkName: string) => void;
27
+ type Appender = (to: string, match: string | string[], label: string, linkName: string) => void;
28
28
 
29
29
  const PrimaryNavigation: FC<Props> = ({ links }) => {
30
30
  const [t] = useTranslation("commons");
31
31
 
32
32
  const createNavigationAppender = (navItems: ReactNode[]): Appender => {
33
- return (to: string, match: string, label: string, linkName: string) => {
33
+ return (to: string, match: string | string[], label: string, linkName: string) => {
34
34
  const link = links[linkName];
35
35
  if (link) {
36
36
  const navigationItem = (
@@ -65,12 +65,12 @@ const PrimaryNavigation: FC<Props> = ({ links }) => {
65
65
  key="primary-navigation.first-menu"
66
66
  name="primary-navigation.first-menu"
67
67
  props={extensionProps}
68
- />
68
+ />,
69
69
  );
70
70
  }
71
- append("/repos/", "/(repo|repos)", "primary-navigation.repositories", "repositories");
72
- append("/users/", "/(user|users)", "primary-navigation.users", "users");
73
- append("/groups/", "/(group|groups)", "primary-navigation.groups", "groups");
71
+ append("/repos/", ["/repo", "/repos"], "primary-navigation.repositories", "repositories");
72
+ append("/users/", ["/user", "/users"], "primary-navigation.users", "users");
73
+ append("/groups/", ["/group", "/groups"], "primary-navigation.groups", "groups");
74
74
  append("/admin", "/admin", "primary-navigation.admin", "config");
75
75
 
76
76
  navItems.push(
@@ -81,7 +81,7 @@ const PrimaryNavigation: FC<Props> = ({ links }) => {
81
81
  props={{
82
82
  links,
83
83
  }}
84
- />
84
+ />,
85
85
  );
86
86
 
87
87
  return navItems;
@@ -15,26 +15,31 @@
15
15
  */
16
16
 
17
17
  import React, { FC } from "react";
18
- import { useLocation, NavLink } from "react-router-dom";
18
+ import { matchPath, useLocation, NavLink } from "react-router-dom";
19
19
  import { createAttributesForTesting } from "../devBuild";
20
20
  import classNames from "classnames";
21
21
 
22
22
  type Props = {
23
23
  to: string;
24
- match: string;
24
+ match: string | string[];
25
25
  label: string;
26
26
  testId?: string;
27
27
  className?: string;
28
28
  };
29
29
 
30
30
  const PrimaryNavigationLink: FC<Props> = ({ to, match, testId, label, className }) => {
31
- const location = useLocation();
32
- const isActive = new RegExp(match).test(location.pathname);
31
+ const { pathname } = useLocation();
32
+ let isMatch: boolean;
33
+ if (Array.isArray(match)) {
34
+ isMatch = match.some((m) => !!matchPath(m + "/*", pathname));
35
+ } else {
36
+ isMatch = !!matchPath((match as string) + "/*", pathname);
37
+ }
33
38
 
34
39
  return (
35
40
  <NavLink
36
41
  to={to}
37
- className={() => classNames(className, "navbar-item", { "is-active": isActive })}
42
+ className={() => classNames(className, "navbar-item", { "is-active": isMatch })}
38
43
  {...createAttributesForTesting(testId)}
39
44
  >
40
45
  {label}
@@ -14,7 +14,7 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import React from "react";
17
+ import React, { FC, Ref } from "react";
18
18
  import { Branch, Changeset, File, Repository } from "@scm-manager/ui-types";
19
19
  import { createChangesetLink, createChangesetLinkByBranch, createSourcesLink } from "./changesets";
20
20
  import { useTranslation } from "react-i18next";
@@ -26,28 +26,27 @@ type Props = {
26
26
  changeset: Changeset;
27
27
  file?: File;
28
28
  branch?: Branch;
29
+ ref?: Ref<HTMLAnchorElement>;
29
30
  };
30
31
 
31
- const ChangesetButtonGroup = React.forwardRef<HTMLAnchorElement, Props>(
32
- ({ repository, changeset, file, branch }, ref) => {
33
- const [t] = useTranslation("repos");
34
- const changesetLink = branch
35
- ? createChangesetLinkByBranch(repository, changeset, branch)
36
- : createChangesetLink(repository, changeset);
37
- const sourcesLink = createSourcesLink(repository, changeset, file);
38
- return (
39
- <ButtonAddons>
40
- <LinkButton className="px-3 pl-5" ref={ref} to={changesetLink}>
41
- <Icon>list-ul</Icon>
42
- <span>{t("changeset.buttons.details")}</span>
43
- </LinkButton>
44
- <LinkButton className="px-3 pl-5" to={sourcesLink}>
45
- <Icon>code</Icon>
46
- <span>{t("changeset.buttons.sources")}</span>
47
- </LinkButton>
48
- </ButtonAddons>
49
- );
50
- }
51
- );
32
+ const ChangesetButtonGroup: FC<Props> = ({ repository, changeset, file, branch, ref }) => {
33
+ const [t] = useTranslation("repos");
34
+ const changesetLink = branch
35
+ ? createChangesetLinkByBranch(repository, changeset, branch)
36
+ : createChangesetLink(repository, changeset);
37
+ const sourcesLink = createSourcesLink(repository, changeset, file);
38
+ return (
39
+ <ButtonAddons>
40
+ <LinkButton className="px-3 pl-5" ref={ref} to={changesetLink}>
41
+ <Icon>list-ul</Icon>
42
+ <span>{t("changeset.buttons.details")}</span>
43
+ </LinkButton>
44
+ <LinkButton className="px-3 pl-5" to={sourcesLink}>
45
+ <Icon>code</Icon>
46
+ <span>{t("changeset.buttons.sources")}</span>
47
+ </LinkButton>
48
+ </ButtonAddons>
49
+ );
50
+ };
52
51
 
53
52
  export default ChangesetButtonGroup;