@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 +13 -13
- package/src/Breadcrumb.tsx +7 -8
- package/src/CardColumn.tsx +74 -64
- package/src/buttons/Button.stories.tsx +1 -0
- package/src/buttons/Button.tsx +81 -83
- package/src/config/ConfigurationBinder.tsx +13 -20
- package/src/config/TitledSettings.tsx +4 -4
- package/src/forms/FileInput.tsx +64 -53
- package/src/forms/FormFieldTypes.tsx +11 -9
- package/src/forms/PasswordConfirmation.tsx +7 -20
- package/src/navigation/PrimaryNavigation.tsx +7 -7
- package/src/navigation/PrimaryNavigationLink.tsx +10 -5
- package/src/repos/changesets/ChangesetButtonGroup.tsx +21 -22
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.
|
|
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
|
+
}
|
package/src/Breadcrumb.tsx
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import React, { FC, useState } from "react";
|
|
18
18
|
import { useTranslation } from "react-i18next";
|
|
19
|
-
import { Link,
|
|
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={
|
|
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={
|
|
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()}
|
package/src/CardColumn.tsx
CHANGED
|
@@ -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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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;
|
package/src/buttons/Button.tsx
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
const content = (
|
|
95
|
+
<span>
|
|
96
|
+
{renderIcon()}{" "}
|
|
97
|
+
{(label || children) && (
|
|
98
|
+
<>
|
|
99
|
+
{label} {children}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</span>
|
|
103
|
+
);
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
<
|
|
108
|
+
<a ref={executeRef} className={classes} href={link} aria-label={label}>
|
|
116
109
|
{content}
|
|
117
|
-
</
|
|
110
|
+
</a>
|
|
118
111
|
);
|
|
119
112
|
}
|
|
120
|
-
|
|
121
113
|
return (
|
|
122
|
-
<
|
|
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
|
-
</
|
|
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={
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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:
|
|
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.
|
|
195
|
+
const NamespaceNavLink: FC<extensionPoints.NamespaceSetting["props"]> = () => {
|
|
200
196
|
const [t] = useTranslation(this.i18nNamespace);
|
|
201
|
-
return this.navLink("/
|
|
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 = ({
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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}</>;
|
package/src/forms/FileInput.tsx
CHANGED
|
@@ -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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
if (onChange && event.target.files) {
|
|
63
|
+
onChange(event);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
|
68
|
+
if (onBlur && event.target.files) {
|
|
69
|
+
onBlur(event);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
const id = createA11yId("file-input");
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 = (
|
|
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={
|
|
63
|
+
onChange={handlePasswordChange}
|
|
73
64
|
value={password}
|
|
74
65
|
validationError={!passwordValid}
|
|
75
66
|
errorMessage={t("password.passwordInvalid")}
|
|
76
|
-
|
|
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/", "/
|
|
72
|
-
append("/users/", "/
|
|
73
|
-
append("/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
|
|
32
|
-
|
|
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":
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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;
|