mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
Introduce ui-forms framework
Adding a new ui framework to make creating forms as easy and consistent as possible. It wraps a lot of boilerplate code and enforces good practices for make the forms in the "SCM-Manager way". Co-authored-by: Florian Scholdei <florian.scholdei@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com> Reviewed-by: Rene Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
committed by
SCM-Manager
parent
f2f2f29791
commit
72dfe80843
@@ -23,7 +23,8 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
"classnames": "^2.2.6"
|
"classnames": "^2.2.6",
|
||||||
|
"@scm-manager/ui-components": "2.40.2-SNAPSHOT"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/prettier-config": "^2.11.1",
|
"@scm-manager/prettier-config": "^2.11.1",
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"react-query": "^3.25.1",
|
"react-query": "^3.25.1",
|
||||||
"i18next": "^19.9.2",
|
"i18next": "^19.9.2",
|
||||||
"react-i18next": "^10.13.2",
|
"react-i18next": "11",
|
||||||
"i18next-fetch-backend": "^2.3.1",
|
"i18next-fetch-backend": "^2.3.1",
|
||||||
"depcheck": "^1.4.3"
|
"depcheck": "^1.4.3"
|
||||||
},
|
},
|
||||||
@@ -80,4 +81,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "restricted"
|
"access": "restricted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
ButtonVariants,
|
ButtonVariants,
|
||||||
ExternalLinkButton as ExternalLinkButtonComponent,
|
ExternalLinkButton as ExternalLinkButtonComponent,
|
||||||
LinkButton as LinkButtonComponent,
|
LinkButton as LinkButtonComponent,
|
||||||
} from "./button";
|
} from "./Button";
|
||||||
import StoryRouter from "storybook-react-router";
|
import StoryRouter from "storybook-react-router";
|
||||||
import { StoryFn } from "@storybook/react";
|
import { StoryFn } from "@storybook/react";
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Meta, Story } from "@storybook/addon-docs";
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
import { Button, ButtonVariantList } from "./button";
|
import { Button, ButtonVariantList } from "./Button";
|
||||||
|
|
||||||
<Meta title="Tests"/>
|
<Meta title="Tests"/>
|
||||||
|
|
||||||
@@ -18,9 +18,10 @@ import { Button, ButtonVariantList } from "./button";
|
|||||||
<th>STATE</th>
|
<th>STATE</th>
|
||||||
{ButtonVariantList.map(variant => <th>{variant.toUpperCase()}</th>)}
|
{ButtonVariantList.map(variant => <th>{variant.toUpperCase()}</th>)}
|
||||||
</tr>
|
</tr>
|
||||||
{["Normal", "Hover", "Active", "Focus", "Disabled"].map(state => <tr>
|
{["Normal", "Hover", "Active", "Focus", "Disabled", "Loading"].map(state => <tr>
|
||||||
<td>{state}</td>
|
<td>{state}</td>
|
||||||
{ButtonVariantList.map(variant => <td><Button id={`${variant}-${state}`} disabled={state === "Disabled"}
|
{ButtonVariantList.map(variant => <td><Button id={`${variant}-${state}`} disabled={state === "Disabled"}
|
||||||
|
isLoading={state === "Loading"}
|
||||||
variant={variant}>Button</Button></td>)}
|
variant={variant}>Button</Button></td>)}
|
||||||
</tr>)}
|
</tr>)}
|
||||||
</table>
|
</table>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
|
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
|
||||||
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
|
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
export const ButtonVariants = {
|
export const ButtonVariants = {
|
||||||
PRIMARY: "primary",
|
PRIMARY: "primary",
|
||||||
@@ -37,16 +38,19 @@ export const ButtonVariantList = Object.values(ButtonVariants);
|
|||||||
|
|
||||||
type ButtonVariant = typeof ButtonVariants[keyof typeof ButtonVariants];
|
type ButtonVariant = typeof ButtonVariants[keyof typeof ButtonVariants];
|
||||||
|
|
||||||
const createButtonClasses = (variant?: ButtonVariant) =>
|
const createButtonClasses = (variant?: ButtonVariant, isLoading?: boolean) =>
|
||||||
classNames("button", {
|
classNames("button", {
|
||||||
"is-primary": variant === "primary",
|
"is-primary": variant === "primary",
|
||||||
"is-primary is-outlined": variant === "secondary",
|
"is-primary is-outlined": variant === "secondary",
|
||||||
"is-primary is-inverted": variant === "tertiary",
|
"is-primary is-inverted": variant === "tertiary",
|
||||||
"is-warning": variant === "signal",
|
"is-warning": variant === "signal",
|
||||||
|
"is-loading": isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
type BaseButtonProps = {
|
type BaseButtonProps = {
|
||||||
variant: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
|
isLoading?: boolean;
|
||||||
|
testId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
@@ -55,8 +59,13 @@ type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
|||||||
* Styled html button
|
* Styled html button
|
||||||
*/
|
*/
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, children, ...props }, ref) => (
|
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||||
<button {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
|
<button
|
||||||
|
{...props}
|
||||||
|
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||||
|
ref={ref}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -68,8 +77,13 @@ type LinkButtonProps = BaseButtonProps & ReactRouterLinkProps;
|
|||||||
* Styled react router link
|
* Styled react router link
|
||||||
*/
|
*/
|
||||||
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
||||||
({ className, variant, children, ...props }, ref) => (
|
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||||
<ReactRouterLink {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
|
<ReactRouterLink
|
||||||
|
{...props}
|
||||||
|
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||||
|
ref={ref}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ReactRouterLink>
|
</ReactRouterLink>
|
||||||
)
|
)
|
||||||
@@ -81,8 +95,13 @@ type ExternalLinkButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchor
|
|||||||
* Styled html anchor
|
* Styled html anchor
|
||||||
*/
|
*/
|
||||||
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
||||||
({ className, variant, children, ...props }, ref) => (
|
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||||
<a {...props} className={classNames(createButtonClasses(variant), className)} ref={ref}>
|
<a
|
||||||
|
{...props}
|
||||||
|
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||||
|
ref={ref}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Button, LinkButton, ExternalLinkButton, ButtonVariants } from "./button";
|
export { Button, LinkButton, ExternalLinkButton, ButtonVariants } from "./Button";
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
"react-diff-view": "^2.4.10",
|
"react-diff-view": "^2.4.10",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-hook-form": "^7.5.1",
|
"react-hook-form": "^7.5.1",
|
||||||
"react-i18next": "^10.13.1",
|
"react-i18next": "11",
|
||||||
"react-router": "^5.3.1",
|
"react-router": "^5.3.1",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
"react-select": "^2.1.2",
|
"react-select": "^2.1.2",
|
||||||
@@ -108,4 +108,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20156,10 +20156,14 @@ exports[`Storyshots Repositories/Changesets Co-Authors with avatar 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20324,10 +20328,14 @@ exports[`Storyshots Repositories/Changesets Commiter and Co-Authors with avatar
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20468,10 +20476,14 @@ exports[`Storyshots Repositories/Changesets Default 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20583,10 +20595,14 @@ exports[`Storyshots Repositories/Changesets List with navigation 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20712,10 +20728,14 @@ exports[`Storyshots Repositories/Changesets List with navigation 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20832,10 +20852,14 @@ exports[`Storyshots Repositories/Changesets List with navigation 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -20957,10 +20981,14 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21072,10 +21100,14 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21199,10 +21231,14 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21348,10 +21384,14 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21463,10 +21503,14 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21587,10 +21631,14 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21711,10 +21759,14 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21839,10 +21891,14 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -21963,10 +22019,14 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -22087,10 +22147,14 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -22211,10 +22275,14 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -22335,10 +22403,14 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
@@ -22459,10 +22531,14 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-touch"
|
className="is-hidden-touch"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.summary
|
||||||
|
</p>
|
||||||
<p
|
<p
|
||||||
className="is-hidden-desktop"
|
className="is-hidden-desktop"
|
||||||
/>
|
>
|
||||||
|
repos:changeset.shortSummary
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="is-flex"
|
className="is-flex"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -241,4 +241,4 @@ class LazyMarkdownView extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(withTranslation("repos")(LazyMarkdownView));
|
export default withTranslation("repos")(withRouter(LazyMarkdownView));
|
||||||
|
|||||||
57
scm-ui/ui-forms/.storybook/RemoveThemesPlugin.js
Normal file
57
scm-ui/ui-forms/.storybook/RemoveThemesPlugin.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
|
class RemoveThemesPlugin {
|
||||||
|
apply (compiler) {
|
||||||
|
compiler.hooks.compilation.tap('RemoveThemesPlugin', (compilation) => {
|
||||||
|
|
||||||
|
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
|
||||||
|
'RemoveThemesPlugin',
|
||||||
|
(data, cb) => {
|
||||||
|
|
||||||
|
// remove generated style-loader bundles from the page
|
||||||
|
// there should be a better way, which does not generate the bundles at all
|
||||||
|
// but for now it works
|
||||||
|
if (data.assets.js) {
|
||||||
|
data.assets.js = data.assets.js.filter(bundle => !bundle.startsWith("ui-theme-"))
|
||||||
|
.filter(bundle => !bundle.startsWith("runtime~ui-theme-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove css links to avoid conflicts with the themes
|
||||||
|
// so we remove all and add our own via preview-head.html
|
||||||
|
if (data.assets.css) {
|
||||||
|
data.assets.css = data.assets.css.filter(css => !css.startsWith("ui-theme-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell webpack to move on
|
||||||
|
cb(null, data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoveThemesPlugin
|
||||||
92
scm-ui/ui-forms/.storybook/main.js
Normal file
92
scm-ui/ui-forms/.storybook/main.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
const RemoveThemesPlugin = require("./RemoveThemesPlugin");
|
||||||
|
const ReactDOM = require("react-dom");
|
||||||
|
|
||||||
|
const root = path.resolve("..");
|
||||||
|
|
||||||
|
const themedir = path.join(root, "ui-styles", "src");
|
||||||
|
|
||||||
|
ReactDOM.createPortal = (node) => node;
|
||||||
|
|
||||||
|
const themes = fs
|
||||||
|
.readdirSync(themedir)
|
||||||
|
.map((filename) => path.parse(filename))
|
||||||
|
.filter((p) => p.ext === ".scss")
|
||||||
|
.reduce((entries, current) => ({ ...entries, [`ui-theme-${current.name}`]: path.join(themedir, current.base) }), {});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
typescript: { reactDocgen: false },
|
||||||
|
core: {
|
||||||
|
builder: "webpack5",
|
||||||
|
},
|
||||||
|
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
|
addons: [
|
||||||
|
"storybook-addon-i18next",
|
||||||
|
"storybook-addon-themes",
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
|
"@storybook/addon-a11y",
|
||||||
|
"storybook-addon-pseudo-states",
|
||||||
|
"storybook-addon-mock",
|
||||||
|
],
|
||||||
|
framework: "@storybook/react",
|
||||||
|
webpackFinal: async (config) => {
|
||||||
|
// add our themes to webpack entry points
|
||||||
|
config.entry = {
|
||||||
|
main: config.entry,
|
||||||
|
...themes,
|
||||||
|
};
|
||||||
|
|
||||||
|
// create separate css files for our themes
|
||||||
|
config.plugins.push(
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "[name].css",
|
||||||
|
ignoreOrder: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// the html-webpack-plugin adds the generated css and js files to the iframe,
|
||||||
|
// which overrides our manually loaded css files.
|
||||||
|
// So we use a custom plugin which uses a hook of html-webpack-plugin
|
||||||
|
// to filter our themes from the output.
|
||||||
|
config.plugins.push(new RemoveThemesPlugin());
|
||||||
|
|
||||||
|
// force cjs instead of esm
|
||||||
|
// https://github.com/tannerlinsley/react-query/issues/3513
|
||||||
|
config.resolve.alias["react-query/devtools"] = require.resolve("react-query/devtools");
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
26
scm-ui/ui-forms/.storybook/preview-head.html
Normal file
26
scm-ui/ui-forms/.storybook/preview-head.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!--
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<link id="ui-theme" data-theme="light" rel="stylesheet" type="text/css" href="/ui-theme-light.css">
|
||||||
|
|
||||||
72
scm-ui/ui-forms/.storybook/preview.js
Normal file
72
scm-ui/ui-forms/.storybook/preview.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
whitelist: ["en", "de"],
|
||||||
|
lng: "en",
|
||||||
|
fallbackLng: "en",
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
useSuspense: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const Decorator = ({ children, themeName }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const link = document.querySelector("#ui-theme");
|
||||||
|
if (link && link["data-theme"] !== themeName) {
|
||||||
|
link.href = `ui-theme-${themeName}.css`;
|
||||||
|
link["data-theme"] = themeName;
|
||||||
|
}
|
||||||
|
}, [themeName]);
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<Story />
|
||||||
|
</I18nextProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
themes: {
|
||||||
|
Decorator,
|
||||||
|
clearable: false,
|
||||||
|
default: "light",
|
||||||
|
list: [
|
||||||
|
{ name: "light", color: "#fff" },
|
||||||
|
{ name: "highcontrast", color: "#050514" },
|
||||||
|
{ name: "dark", color: "#121212" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
49
scm-ui/ui-forms/package.json
Normal file
49
scm-ui/ui-forms/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@scm-manager/ui-forms",
|
||||||
|
"private": true,
|
||||||
|
"version": "2.40.2-SNAPSHOT",
|
||||||
|
"main": "build/index.js",
|
||||||
|
"types": "build/index.d.ts",
|
||||||
|
"module": "build/index.mjs",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup ./src/index.ts -d build --format esm,cjs --dts",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.19.0",
|
||||||
|
"@scm-manager/eslint-config": "^2.16.0",
|
||||||
|
"@scm-manager/prettier-config": "^2.10.1",
|
||||||
|
"@scm-manager/tsconfig": "^2.13.0",
|
||||||
|
"@scm-manager/ui-styles": "2.40.2-SNAPSHOT",
|
||||||
|
"@storybook/addon-actions": "^6.5.10",
|
||||||
|
"@storybook/addon-essentials": "^6.5.10",
|
||||||
|
"@storybook/addon-interactions": "^6.5.10",
|
||||||
|
"@storybook/addon-links": "^6.5.10",
|
||||||
|
"@storybook/builder-webpack5": "^6.5.10",
|
||||||
|
"@storybook/manager-webpack5": "^6.5.10",
|
||||||
|
"@storybook/react": "^6.5.10",
|
||||||
|
"@storybook/testing-library": "^0.0.13",
|
||||||
|
"@storybook/addon-docs": "^6.5.14",
|
||||||
|
"babel-loader": "^8.2.5",
|
||||||
|
"storybook-addon-mock": "^3.2.0",
|
||||||
|
"storybook-addon-themes": "^6.1.0",
|
||||||
|
"tsup": "^6.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@scm-manager/ui-components": "^2.40.2-SNAPSHOT",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"react": "17",
|
||||||
|
"react-hook-form": "7",
|
||||||
|
"react-i18next": "11",
|
||||||
|
"react-query": "3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@scm-manager/ui-buttons": "^2.40.2-SNAPSHOT"
|
||||||
|
},
|
||||||
|
"prettier": "@scm-manager/prettier-config",
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "@scm-manager/eslint-config"
|
||||||
|
}
|
||||||
|
}
|
||||||
160
scm-ui/ui-forms/src/Form.stories.mdx
Normal file
160
scm-ui/ui-forms/src/Form.stories.mdx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import Form from "./Form";
|
||||||
|
import FormRow from "./FormRow";
|
||||||
|
import ControlledInputField from "./input/ControlledInputField";
|
||||||
|
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||||
|
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||||
|
|
||||||
|
<Meta title="Form" />
|
||||||
|
|
||||||
|
<Story name="Creation">
|
||||||
|
<Form
|
||||||
|
onSubmit={console.log}
|
||||||
|
translationPath={["sample", "form"]}
|
||||||
|
defaultValues={{
|
||||||
|
name: "",
|
||||||
|
password: "",
|
||||||
|
active: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledInputField name="name" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledSecretConfirmationField name="password" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledCheckboxField name="active" />
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Editing">
|
||||||
|
<Form
|
||||||
|
onSubmit={console.log}
|
||||||
|
translationPath={["sample", "form"]}
|
||||||
|
defaultValues={{
|
||||||
|
name: "trillian",
|
||||||
|
password: "secret",
|
||||||
|
active: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledInputField name="name" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledSecretConfirmationField name="password" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledCheckboxField name="active" />
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="GlobalConfiguration">
|
||||||
|
<Form
|
||||||
|
onSubmit={console.log}
|
||||||
|
translationPath={["sample", "form"]}
|
||||||
|
defaultValues={{
|
||||||
|
url: "",
|
||||||
|
filter: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
roleLevel: "",
|
||||||
|
updateIssues: false,
|
||||||
|
disableRepoConfig: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ watch }) => (
|
||||||
|
<>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledInputField name="url" label="URL" helpText="URL of Jira installation (with context path)." />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledInputField name="filter" label="Project Filter" helpText="Filters for jira project key." />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledCheckboxField
|
||||||
|
name="updateIssues"
|
||||||
|
label="Update Jira Issues"
|
||||||
|
helpText="Enable the automatic update function."
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow hide={watch("filter")}>
|
||||||
|
<ControlledInputField
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
helpText="Jira username for connection."
|
||||||
|
className="is-half"
|
||||||
|
/>
|
||||||
|
<ControlledInputField
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
helpText="Jira password for connection."
|
||||||
|
type="password"
|
||||||
|
className="is-half"
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow hide={watch("filter")}>
|
||||||
|
<ControlledInputField
|
||||||
|
name="roleLevel"
|
||||||
|
label="Role Visibility"
|
||||||
|
helpText="Defines for which Project Role the comments are visible."
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledCheckboxField
|
||||||
|
name="disableRepoConfig"
|
||||||
|
label="Do not allow repository configuration"
|
||||||
|
helpText="Do not allow repository owners to configure jira instances."
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="RepoConfiguration">
|
||||||
|
<Form
|
||||||
|
onSubmit={console.log}
|
||||||
|
translationPath={["sample", "form"]}
|
||||||
|
defaultValues={{
|
||||||
|
url: "",
|
||||||
|
option: "",
|
||||||
|
anotherOption: "",
|
||||||
|
disableA: false,
|
||||||
|
disableB: false,
|
||||||
|
disableC: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ControlledInputField name="url" />
|
||||||
|
<ControlledInputField name="option" />
|
||||||
|
<ControlledInputField name="anotherOption" />
|
||||||
|
<ControlledCheckboxField name="disableA" />
|
||||||
|
<ControlledCheckboxField name="disableB" />
|
||||||
|
<ControlledCheckboxField name="disableC" />
|
||||||
|
</Form>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="ReadOnly">
|
||||||
|
<Form
|
||||||
|
onSubmit={console.log}
|
||||||
|
translationPath={["sample", "form"]}
|
||||||
|
defaultValues={{
|
||||||
|
name: "trillian",
|
||||||
|
password: "secret",
|
||||||
|
active: true,
|
||||||
|
}}
|
||||||
|
readOnly
|
||||||
|
>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledInputField name="name" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledSecretConfirmationField name="password" />
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledCheckboxField name="active" />
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
</Story>
|
||||||
161
scm-ui/ui-forms/src/Form.tsx
Normal file
161
scm-ui/ui-forms/src/Form.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from "react";
|
||||||
|
import { DeepPartial, SubmitHandler, useForm, UseFormReturn } from "react-hook-form";
|
||||||
|
import { ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||||
|
import { ScmFormContextProvider } from "./ScmFormContext";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button } from "@scm-manager/ui-buttons";
|
||||||
|
import FormRow from "./FormRow";
|
||||||
|
import ControlledInputField from "./input/ControlledInputField";
|
||||||
|
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||||
|
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||||
|
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||||
|
UseFormReturn<T>,
|
||||||
|
"register" | "unregister" | "handleSubmit" | "control"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label, hide }) => {
|
||||||
|
if (!label) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notification is-success">
|
||||||
|
<button className="delete" onClick={hide} />
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props<FormType extends Record<string, unknown>, DefaultValues extends FormType> = {
|
||||||
|
children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
|
||||||
|
translationPath: [namespace: string, prefix: string];
|
||||||
|
onSubmit: SubmitHandler<FormType>;
|
||||||
|
defaultValues: Omit<DefaultValues, keyof HalRepresentation>;
|
||||||
|
readOnly?: boolean;
|
||||||
|
submitButtonTestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType>({
|
||||||
|
children,
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
translationPath,
|
||||||
|
readOnly,
|
||||||
|
submitButtonTestId,
|
||||||
|
}: Props<FormType, DefaultValues>) {
|
||||||
|
const form = useForm<FormType>({
|
||||||
|
mode: "onChange",
|
||||||
|
defaultValues: defaultValues as DeepPartial<FormType>,
|
||||||
|
});
|
||||||
|
const { formState, handleSubmit, reset } = form;
|
||||||
|
const [ns, prefix] = translationPath;
|
||||||
|
const { t } = useTranslation(ns, { keyPrefix: prefix });
|
||||||
|
const { isDirty, isValid, isSubmitting, isSubmitSuccessful } = formState;
|
||||||
|
const [error, setError] = useState<Error | null | undefined>();
|
||||||
|
const [showSuccessNotification, setShowSuccessNotification] = useState(false);
|
||||||
|
|
||||||
|
// See https://react-hook-form.com/api/useform/reset/
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitSuccessful) {
|
||||||
|
setShowSuccessNotification(true);
|
||||||
|
reset(defaultValues as never);
|
||||||
|
}
|
||||||
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
|
}, [isSubmitSuccessful]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDirty) {
|
||||||
|
setShowSuccessNotification(false);
|
||||||
|
}
|
||||||
|
}, [isDirty]);
|
||||||
|
|
||||||
|
const translateWithFallback = useCallback<typeof t>(
|
||||||
|
(key, ...args) => {
|
||||||
|
const translation = t(key, ...(args as any));
|
||||||
|
if (translation === `${prefix}.${key}`) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
},
|
||||||
|
[prefix, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
async (data) => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
return await onSubmit(data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSubmit]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScmFormContextProvider {...form} readOnly={isSubmitting || readOnly} t={translateWithFallback}>
|
||||||
|
<form onSubmit={handleSubmit(submit)}>
|
||||||
|
{showSuccessNotification ? (
|
||||||
|
<SuccessNotification
|
||||||
|
label={translateWithFallback("submit-success-notification")}
|
||||||
|
hide={() => setShowSuccessNotification(false)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{typeof children === "function" ? children(form) : children}
|
||||||
|
{error ? <ErrorNotification error={error} /> : null}
|
||||||
|
{!readOnly ? (
|
||||||
|
<Level
|
||||||
|
right={
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
testId={submitButtonTestId ?? "submit-button"}
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
{t("submit")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
</ScmFormContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Object.assign(Form, {
|
||||||
|
Row: FormRow,
|
||||||
|
Input: ControlledInputField,
|
||||||
|
Checkbox: ControlledCheckboxField,
|
||||||
|
SecretConfirmation: ControlledSecretConfirmationField,
|
||||||
|
});
|
||||||
37
scm-ui/ui-forms/src/FormRow.tsx
Normal file
37
scm-ui/ui-forms/src/FormRow.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { HTMLProps } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const FormRow = React.forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||||
|
({ className, children, hidden, ...rest }, ref) =>
|
||||||
|
hidden ? null : (
|
||||||
|
<div ref={ref} className={classNames("columns", className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FormRow;
|
||||||
42
scm-ui/ui-forms/src/ScmFormContext.tsx
Normal file
42
scm-ui/ui-forms/src/ScmFormContext.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { PropsWithChildren, useContext } from "react";
|
||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
|
type ContextType<T = any> = UseFormReturn<T> & {
|
||||||
|
t: TFunction;
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScmFormContext = React.createContext<ContextType>(null as unknown as ContextType);
|
||||||
|
|
||||||
|
export function ScmFormContextProvider<T>({ children, ...props }: PropsWithChildren<ContextType<T>>) {
|
||||||
|
return <ScmFormContext.Provider value={props}>{children}</ScmFormContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScmFormContext() {
|
||||||
|
return useContext(ScmFormContext);
|
||||||
|
}
|
||||||
34
scm-ui/ui-forms/src/base/Control.tsx
Normal file
34
scm-ui/ui-forms/src/base/Control.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, HTMLProps } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const Control: FC<HTMLProps<HTMLDivElement>> = ({ className, children, ...rest }) => (
|
||||||
|
<div className={classNames("control", className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Control;
|
||||||
33
scm-ui/ui-forms/src/base/Field.tsx
Normal file
33
scm-ui/ui-forms/src/base/Field.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, HTMLProps } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const Field: FC<HTMLProps<HTMLDivElement>> = ({ className, children, ...rest }) => (
|
||||||
|
<div className={classNames("field", className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Field;
|
||||||
34
scm-ui/ui-forms/src/base/field-message/FieldMessage.tsx
Normal file
34
scm-ui/ui-forms/src/base/field-message/FieldMessage.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { createVariantClass, Variant } from "../../variants";
|
||||||
|
|
||||||
|
type Props = { variant?: Variant; className?: string; children?: ReactNode };
|
||||||
|
|
||||||
|
const FieldMessage = ({ variant, className, children }: Props) => (
|
||||||
|
<p className={classNames("help", createVariantClass(variant), className)}>{children}</p>
|
||||||
|
);
|
||||||
|
export default FieldMessage;
|
||||||
36
scm-ui/ui-forms/src/base/help/Help.tsx
Normal file
36
scm-ui/ui-forms/src/base/help/Help.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
type Props = { text?: string; className?: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Implement tooltip
|
||||||
|
*/
|
||||||
|
const Help = ({ text, className }: Props) => (
|
||||||
|
<span className={classNames("fas fa-fw fa-question-circle has-text-blue-light", className)} title={text} />
|
||||||
|
);
|
||||||
|
export default Help;
|
||||||
33
scm-ui/ui-forms/src/base/label/Label.tsx
Normal file
33
scm-ui/ui-forms/src/base/label/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, HTMLProps } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const Label: FC<HTMLProps<HTMLLabelElement>> = ({ className, children, ...rest }) => (
|
||||||
|
<label className={classNames("label", className)} {...rest}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
export default Label;
|
||||||
26
scm-ui/ui-forms/src/checkbox/Checkbox.stories.mdx
Normal file
26
scm-ui/ui-forms/src/checkbox/Checkbox.stories.mdx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {Meta, Story} from "@storybook/addon-docs";
|
||||||
|
import Checkbox from "./Checkbox";
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
title="Checkbox"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<Checkbox name="name"/>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithHardcodedText">
|
||||||
|
<Checkbox name="name" label="Name" helpText="A help text"/>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithStyling">
|
||||||
|
<Checkbox name="name" className="has-background-blue-light"/>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithInitialFocus">
|
||||||
|
<Checkbox name="name" autoFocus/>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Readonly">
|
||||||
|
<Checkbox name="name" checked readOnly/>
|
||||||
|
</Story>
|
||||||
85
scm-ui/ui-forms/src/checkbox/Checkbox.tsx
Normal file
85
scm-ui/ui-forms/src/checkbox/Checkbox.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { InputHTMLAttributes } from "react";
|
||||||
|
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||||
|
import Help from "../base/help/Help";
|
||||||
|
|
||||||
|
type InputFieldProps = {
|
||||||
|
label: string;
|
||||||
|
helpText?: string;
|
||||||
|
testId?: string;
|
||||||
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "type">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://bulma.io/documentation/form/checkbox/
|
||||||
|
*/
|
||||||
|
const Checkbox = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||||
|
({ readOnly, label, value, name, checked, defaultChecked, defaultValue, testId, helpText, ...props }, ref) => (
|
||||||
|
// @ts-ignore bulma uses the disabled attribute on labels, although it is not part of the html spec
|
||||||
|
<label className="checkbox" disabled={readOnly || props.disabled}>
|
||||||
|
{readOnly ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-1"
|
||||||
|
ref={ref}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
{...props}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-1"
|
||||||
|
ref={ref}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
{...props}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{label}
|
||||||
|
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
export default Checkbox;
|
||||||
39
scm-ui/ui-forms/src/checkbox/CheckboxField.tsx
Normal file
39
scm-ui/ui-forms/src/checkbox/CheckboxField.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Field from "../base/Field";
|
||||||
|
import Control from "../base/Control";
|
||||||
|
import Checkbox from "./Checkbox";
|
||||||
|
|
||||||
|
type Props = React.ComponentProps<typeof Checkbox>;
|
||||||
|
|
||||||
|
const CheckboxField = React.forwardRef<HTMLInputElement, Props>(({ className, ...props }, ref) => (
|
||||||
|
<Field className={className}>
|
||||||
|
<Control>
|
||||||
|
<Checkbox ref={ref} {...props} />
|
||||||
|
</Control>
|
||||||
|
</Field>
|
||||||
|
));
|
||||||
|
export default CheckboxField;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import Form from "../Form";
|
||||||
|
import ControlledCheckboxField from "./ControlledCheckboxField";
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
title="ControlledCheckboxField"
|
||||||
|
decorators={[
|
||||||
|
(Story) => (
|
||||||
|
<Form onSubmit={console.log} defaultValues={{ checkOne: false, checkTwo: true, checkThree: false }} translationPath={["sample", "form"]}>
|
||||||
|
<Story />
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<ControlledCheckboxField name="checkOne" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithHardcodedText">
|
||||||
|
<ControlledCheckboxField name="checkOne" label="Name" helpText="A help text" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithStyling">
|
||||||
|
<ControlledCheckboxField name="checkOne" className="has-background-blue-light" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithInitialFocus">
|
||||||
|
<ControlledCheckboxField name="checkOne" autoFocus={true} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithReadonly">
|
||||||
|
<ControlledCheckboxField name="checkOne" readOnly />
|
||||||
|
<ControlledCheckboxField name="checkTwo" disabled />
|
||||||
|
<ControlledCheckboxField name="checkThree" />
|
||||||
|
</Story>
|
||||||
76
scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.tsx
Normal file
76
scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentProps } from "react";
|
||||||
|
import { Controller, ControllerRenderProps, Path, RegisterOptions } from "react-hook-form";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useScmFormContext } from "../ScmFormContext";
|
||||||
|
import CheckboxField from "./CheckboxField";
|
||||||
|
|
||||||
|
type Props<T extends Record<string, unknown>> = Omit<
|
||||||
|
ComponentProps<typeof CheckboxField>,
|
||||||
|
"label" | "defaultValue" | "required" | keyof ControllerRenderProps
|
||||||
|
> & {
|
||||||
|
name: Path<T>;
|
||||||
|
label?: string;
|
||||||
|
rules?: Pick<RegisterOptions, "deps">;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ControlledInputField<T extends Record<string, unknown>>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
rules,
|
||||||
|
className,
|
||||||
|
testId,
|
||||||
|
defaultChecked,
|
||||||
|
readOnly,
|
||||||
|
...props
|
||||||
|
}: Props<T>) {
|
||||||
|
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||||
|
const labelTranslation = label || t(`${name}.label`) || "";
|
||||||
|
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
rules={rules}
|
||||||
|
defaultValue={defaultChecked as never}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CheckboxField
|
||||||
|
className={classNames("column", className)}
|
||||||
|
readOnly={readOnly ?? formReadonly}
|
||||||
|
defaultChecked={field.value}
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
label={labelTranslation}
|
||||||
|
helpText={helpTextTranslation}
|
||||||
|
testId={testId ?? `checkbox-${name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlledInputField;
|
||||||
26
scm-ui/ui-forms/src/index.ts
Normal file
26
scm-ui/ui-forms/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as Form } from "./Form";
|
||||||
|
export * from "./resourceHooks";
|
||||||
36
scm-ui/ui-forms/src/input/ControlledInputField.stories.mdx
Normal file
36
scm-ui/ui-forms/src/input/ControlledInputField.stories.mdx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import Form from "../Form";
|
||||||
|
import ControlledInputField from "./ControlledInputField";
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
title="ControlledInputField"
|
||||||
|
decorators={[
|
||||||
|
(Story) => (
|
||||||
|
<Form onSubmit={console.log} defaultValues={{name: "Initial value"}} translationPath={["sample", "form"]}>
|
||||||
|
<Story />
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<ControlledInputField name="name" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithHardcodedText">
|
||||||
|
<ControlledInputField name="name" label="Name" helpText="A help text" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithStyling">
|
||||||
|
<ControlledInputField name="name" className="has-background-blue-light" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithCheck">
|
||||||
|
<ControlledInputField name="name" rules={{
|
||||||
|
required: true
|
||||||
|
}} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithInitialFocus">
|
||||||
|
<ControlledInputField name="name" autoFocus={true} />
|
||||||
|
</Story>
|
||||||
77
scm-ui/ui-forms/src/input/ControlledInputField.tsx
Normal file
77
scm-ui/ui-forms/src/input/ControlledInputField.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentProps } from "react";
|
||||||
|
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useScmFormContext } from "../ScmFormContext";
|
||||||
|
import InputField from "./InputField";
|
||||||
|
|
||||||
|
type Props<T extends Record<string, unknown>> = Omit<
|
||||||
|
ComponentProps<typeof InputField>,
|
||||||
|
"error" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
|
||||||
|
> & {
|
||||||
|
rules?: ComponentProps<typeof Controller>["rules"];
|
||||||
|
name: Path<T>;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ControlledInputField<T extends Record<string, unknown>>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
rules,
|
||||||
|
className,
|
||||||
|
testId,
|
||||||
|
defaultValue,
|
||||||
|
readOnly,
|
||||||
|
...props
|
||||||
|
}: Props<T>) {
|
||||||
|
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||||
|
const labelTranslation = label || t(`${name}.label`) || "";
|
||||||
|
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
rules={rules}
|
||||||
|
defaultValue={defaultValue as never}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<InputField
|
||||||
|
className={classNames("column", className)}
|
||||||
|
readOnly={readOnly ?? formReadonly}
|
||||||
|
required={rules?.required as boolean}
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
label={labelTranslation}
|
||||||
|
helpText={helpTextTranslation}
|
||||||
|
error={fieldState.error ? fieldState.error.message || t(`${name}.error.${fieldState.error.type}`) : undefined}
|
||||||
|
testId={testId ?? `input-${name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlledInputField;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import Form from "../Form";
|
||||||
|
import ControlledSecretConfirmationField from "./ControlledSecretConfirmationField";
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
title="ControlledSecretConfirmationField"
|
||||||
|
decorators={[
|
||||||
|
(Story) => (
|
||||||
|
<Form onSubmit={console.log} defaultValues={{ password: "", passwordConfirmation: "" }} translationPath={["sample", "form"]}>
|
||||||
|
<Story />
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<ControlledSecretConfirmationField name="password" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithHardcodedText">
|
||||||
|
<ControlledSecretConfirmationField name="password" label="Password" confirmationLabel="Password Confirmation" helpText="A help text" confirmationHelpText="Another help text" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithStyling">
|
||||||
|
<ControlledSecretConfirmationField name="password" className="has-background-blue-light" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithCheck">
|
||||||
|
<ControlledSecretConfirmationField
|
||||||
|
name="password"
|
||||||
|
rules={{
|
||||||
|
required: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithInitialFocus">
|
||||||
|
<ControlledSecretConfirmationField name="password" autoFocus={true} />
|
||||||
|
</Story>
|
||||||
121
scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.tsx
Normal file
121
scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentProps } from "react";
|
||||||
|
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useScmFormContext } from "../ScmFormContext";
|
||||||
|
import InputField from "./InputField";
|
||||||
|
|
||||||
|
type Props<T extends Record<string, unknown>> = Omit<
|
||||||
|
ComponentProps<typeof InputField>,
|
||||||
|
"error" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
|
||||||
|
> & {
|
||||||
|
rules?: ComponentProps<typeof Controller>["rules"];
|
||||||
|
name: Path<T>;
|
||||||
|
label?: string;
|
||||||
|
confirmationLabel?: string;
|
||||||
|
confirmationHelpText?: string;
|
||||||
|
confirmationErrorMessage?: string;
|
||||||
|
confirmationTestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ControlledSecretConfirmationField<T extends Record<string, unknown>>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
confirmationLabel,
|
||||||
|
helpText,
|
||||||
|
confirmationHelpText,
|
||||||
|
rules,
|
||||||
|
confirmationErrorMessage,
|
||||||
|
className,
|
||||||
|
testId,
|
||||||
|
confirmationTestId,
|
||||||
|
defaultValue,
|
||||||
|
readOnly,
|
||||||
|
...props
|
||||||
|
}: Props<T>) {
|
||||||
|
const { control, watch, t, readOnly: formReadonly } = useScmFormContext();
|
||||||
|
const labelTranslation = label || t(`${name}.label`) || "";
|
||||||
|
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||||
|
const confirmationLabelTranslation = confirmationLabel || t(`${name}.confirmation.label`) || "";
|
||||||
|
const confirmationHelpTextTranslation = confirmationHelpText || t(`${name}.confirmation.helpText`);
|
||||||
|
const confirmationErrorMessageTranslation = confirmationErrorMessage || t(`${name}.confirmation.errorMessage`);
|
||||||
|
const secretValue = watch(name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue as never}
|
||||||
|
rules={{
|
||||||
|
...rules,
|
||||||
|
deps: [`${name}Confirmation`],
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<InputField
|
||||||
|
className={classNames("column", className)}
|
||||||
|
readOnly={readOnly ?? formReadonly}
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
required={rules?.required as boolean}
|
||||||
|
type="password"
|
||||||
|
label={labelTranslation}
|
||||||
|
helpText={helpTextTranslation}
|
||||||
|
error={
|
||||||
|
fieldState.error ? fieldState.error.message || t(`${name}.error.${fieldState.error.type}`) : undefined
|
||||||
|
}
|
||||||
|
testId={testId ?? `input-${name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`${name}Confirmation`}
|
||||||
|
defaultValue={defaultValue as never}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<InputField
|
||||||
|
className={classNames("column", className)}
|
||||||
|
type="password"
|
||||||
|
readOnly={readOnly ?? formReadonly}
|
||||||
|
disabled={props.disabled}
|
||||||
|
{...field}
|
||||||
|
label={confirmationLabelTranslation}
|
||||||
|
helpText={confirmationHelpTextTranslation}
|
||||||
|
error={
|
||||||
|
fieldState.error
|
||||||
|
? fieldState.error.message || t(`${name}.confirmation.error.${fieldState.error.type}`)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
testId={confirmationTestId ?? `input-${name}-confirmation`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
rules={{
|
||||||
|
validate: (value) => secretValue === value || confirmationErrorMessageTranslation,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
scm-ui/ui-forms/src/input/Input.stories.mdx
Normal file
22
scm-ui/ui-forms/src/input/Input.stories.mdx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import Input from "./Input"
|
||||||
|
|
||||||
|
<Meta title="Input" />
|
||||||
|
|
||||||
|
This will be our latest input component
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<Input />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Variant">
|
||||||
|
<Input variant="danger" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Custom Class">
|
||||||
|
<Input className="is-warning" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="With Ref">
|
||||||
|
<Input ref={r => {r.focus()}} />
|
||||||
|
</Story>
|
||||||
46
scm-ui/ui-forms/src/input/Input.tsx
Normal file
46
scm-ui/ui-forms/src/input/Input.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { InputHTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { createVariantClass, Variant } from "../variants";
|
||||||
|
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
variant?: Variant;
|
||||||
|
testId?: string;
|
||||||
|
} & InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, Props>(({ variant, className, testId, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className={classNames("input", createVariantClass(variant), className)}
|
||||||
|
{...props}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Input;
|
||||||
22
scm-ui/ui-forms/src/input/InputField.stories.mdx
Normal file
22
scm-ui/ui-forms/src/input/InputField.stories.mdx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Meta, Story } from "@storybook/addon-docs";
|
||||||
|
import InputField from "./InputField";
|
||||||
|
|
||||||
|
<Meta title="InputField" />
|
||||||
|
|
||||||
|
This will be our first form field molecule
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<InputField label="MyInput" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithHelp">
|
||||||
|
<InputField label="MyInput" helpText="You can do all sorts of things with this input" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithError">
|
||||||
|
<InputField label="MyInput" error="This field is super required" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithWidth">
|
||||||
|
<InputField label="MyInput" className="column is-half" />
|
||||||
|
</Story>
|
||||||
60
scm-ui/ui-forms/src/input/InputField.tsx
Normal file
60
scm-ui/ui-forms/src/input/InputField.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Field from "../base/Field";
|
||||||
|
import Control from "../base/Control";
|
||||||
|
import Label from "../base/label/Label";
|
||||||
|
import FieldMessage from "../base/field-message/FieldMessage";
|
||||||
|
import Input from "./Input";
|
||||||
|
import Help from "../base/help/Help";
|
||||||
|
|
||||||
|
type InputFieldProps = {
|
||||||
|
label: string;
|
||||||
|
helpText?: string;
|
||||||
|
error?: string;
|
||||||
|
type?: "text" | "password" | "email" | "tel";
|
||||||
|
} & Omit<React.ComponentProps<typeof Input>, "type">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://bulma.io/documentation/form/input/
|
||||||
|
*/
|
||||||
|
const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||||
|
({ label, helpText, error, className, ...props }, ref) => {
|
||||||
|
const variant = error ? "danger" : undefined;
|
||||||
|
return (
|
||||||
|
<Field className={className}>
|
||||||
|
<Label>
|
||||||
|
{label}
|
||||||
|
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||||
|
</Label>
|
||||||
|
<Control>
|
||||||
|
<Input variant={variant} ref={ref} {...props}></Input>
|
||||||
|
</Control>
|
||||||
|
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default InputField;
|
||||||
152
scm-ui/ui-forms/src/resourceHooks.ts
Normal file
152
scm-ui/ui-forms/src/resourceHooks.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient, requiredLink } from "@scm-manager/ui-api";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import { HalRepresentation, Link } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type QueryKeyPair = [singular: string, plural: string];
|
||||||
|
type LinkOrHalLink = string | [entity: HalRepresentation, link: string] | HalRepresentation;
|
||||||
|
const unwrapLink = (input: LinkOrHalLink, linkName: string) => {
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return requiredLink(input[0], input[1]);
|
||||||
|
} else if (typeof input === "string") {
|
||||||
|
return input;
|
||||||
|
} else {
|
||||||
|
return (input._links[linkName] as Link).href;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type MutationResult<I, O = unknown> = {
|
||||||
|
submit: (resource: I) => Promise<O>;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
submissionResult?: O;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MutatingResourceOptions = {
|
||||||
|
contentType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResource = <I, O = never>(link: string, contentType: string) => {
|
||||||
|
return (payload: I): Promise<O> => {
|
||||||
|
return apiClient
|
||||||
|
.post(link, payload, contentType)
|
||||||
|
.then((response) => {
|
||||||
|
const location = response.headers.get("Location");
|
||||||
|
if (!location) {
|
||||||
|
throw new Error("Server does not return required Location header");
|
||||||
|
}
|
||||||
|
return apiClient.get(location);
|
||||||
|
})
|
||||||
|
.then((response) => response.json());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateResourceOptions = MutatingResourceOptions;
|
||||||
|
|
||||||
|
export const useCreateResource = <I, O>(
|
||||||
|
link: string,
|
||||||
|
[entityKey, collectionName]: QueryKeyPair,
|
||||||
|
idFactory: (createdResource: O) => string,
|
||||||
|
{ contentType = "application/json" }: CreateResourceOptions = {}
|
||||||
|
): MutationResult<I, O> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { mutateAsync, data, isLoading, error } = useMutation<O, Error, I>(createResource<I, O>(link, contentType), {
|
||||||
|
onSuccess: (result) => {
|
||||||
|
queryClient.setQueryData([entityKey, idFactory(result)], result);
|
||||||
|
return queryClient.invalidateQueries(collectionName);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
submit: (payload: I) => mutateAsync(payload),
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
submissionResult: data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateResourceOptions = MutatingResourceOptions & {
|
||||||
|
collectionName?: QueryKeyPair;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateResource = <T>(
|
||||||
|
link: LinkOrHalLink,
|
||||||
|
idFactory: (createdResource: T) => string,
|
||||||
|
{
|
||||||
|
contentType = "application/json",
|
||||||
|
collectionName: [entityQueryKey, collectionName] = ["", ""],
|
||||||
|
}: UpdateResourceOptions = {}
|
||||||
|
): MutationResult<T> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { mutateAsync, isLoading, error, data } = useMutation<unknown, Error, T>(
|
||||||
|
(resource) => apiClient.put(unwrapLink(link, "update"), resource, contentType),
|
||||||
|
{
|
||||||
|
onSuccess: async (_, payload) => {
|
||||||
|
await queryClient.invalidateQueries(entityQueryKey ? [entityQueryKey, idFactory(payload)] : idFactory(payload));
|
||||||
|
if (collectionName) {
|
||||||
|
await queryClient.invalidateQueries(collectionName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
submit: (resource: T) => mutateAsync(resource),
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
submissionResult: data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeleteResourceOptions = {
|
||||||
|
collectionName?: QueryKeyPair;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteResource = <T extends HalRepresentation>(
|
||||||
|
idFactory: (createdResource: T) => string,
|
||||||
|
{ collectionName: [entityQueryKey, collectionName] = ["", ""] }: DeleteResourceOptions = {}
|
||||||
|
): MutationResult<T> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { mutateAsync, isLoading, error, data } = useMutation<unknown, Error, T>(
|
||||||
|
(resource) => {
|
||||||
|
const deleteUrl = (resource._links.delete as Link).href;
|
||||||
|
return apiClient.delete(deleteUrl);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async (_, resource) => {
|
||||||
|
const id = idFactory(resource);
|
||||||
|
await queryClient.removeQueries(entityQueryKey ? [entityQueryKey, id] : id);
|
||||||
|
if (collectionName) {
|
||||||
|
await queryClient.invalidateQueries(collectionName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
submit: (resource: T) => mutateAsync(resource),
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
submissionResult: data,
|
||||||
|
};
|
||||||
|
};
|
||||||
27
scm-ui/ui-forms/src/variants.ts
Normal file
27
scm-ui/ui-forms/src/variants.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const variants = ["danger"] as const;
|
||||||
|
export type Variant = typeof variants[number];
|
||||||
|
export const createVariantClass = (variant?: Variant) => (variant ? `is-${variant}` : undefined);
|
||||||
3
scm-ui/ui-forms/tsconfig.json
Normal file
3
scm-ui/ui-forms/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@scm-manager/tsconfig"
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"query-string": "6.14.1",
|
"query-string": "6.14.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-hook-form": "^7.5.1",
|
"react-hook-form": "^7.5.1",
|
||||||
"react-i18next": "^10.13.1",
|
"react-i18next": "11",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-router": "^5.3.1",
|
"react-router": "^5.3.1",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "17",
|
"react": "17",
|
||||||
"react-i18next": "10"
|
"react-i18next": "11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mousetrap": "1.6.5"
|
"mousetrap": "1.6.5"
|
||||||
@@ -50,4 +50,4 @@
|
|||||||
"jest-extended/all"
|
"jest-extended/all"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type UseShortcutOptions = {
|
|||||||
*
|
*
|
||||||
* If no description is supplied, there will be no entry in the shortcut summary table.
|
* If no description is supplied, there will be no entry in the shortcut summary table.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,16 +12,17 @@
|
|||||||
"@scm-manager/ui-text": "2.40.2-SNAPSHOT",
|
"@scm-manager/ui-text": "2.40.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-shortcuts": "2.40.2-SNAPSHOT",
|
"@scm-manager/ui-shortcuts": "2.40.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-legacy": "2.40.2-SNAPSHOT",
|
"@scm-manager/ui-legacy": "2.40.2-SNAPSHOT",
|
||||||
|
"@scm-manager/ui-forms": "2.40.2-SNAPSHOT",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"i18next": "^19.6.0",
|
"i18next": "21",
|
||||||
"i18next-browser-languagedetector": "^4.0.0",
|
"i18next-browser-languagedetector": "6",
|
||||||
"i18next-fetch-backend": "^2.2.0",
|
"i18next-fetch-backend": "4",
|
||||||
"query-string": "6.14.1",
|
"query-string": "6.14.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-hook-form": "^7.5.1",
|
"react-hook-form": "^7.5.1",
|
||||||
"react-i18next": "^10.13.1",
|
"react-i18next": "11",
|
||||||
"react-router": "^5.3.1",
|
"react-router": "^5.3.1",
|
||||||
"react-router-dom": "^5.3.1",
|
"react-router-dom": "^5.3.1",
|
||||||
"react-select": "^2.1.2",
|
"react-select": "^2.1.2",
|
||||||
@@ -69,4 +70,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,52 @@
|
|||||||
},
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "Benutzer erstellen",
|
"title": "Benutzer erstellen",
|
||||||
"subtitle": "Erstellen eines neuen Benutzers"
|
"subtitle": "Erstellen eines neuen Benutzers",
|
||||||
|
"form": {
|
||||||
|
"submit": "Speichern",
|
||||||
|
"submit-success-notification": "Der Benutzer wurde erfolgreich aktualisiert",
|
||||||
|
"name": {
|
||||||
|
"label": "Benutzername",
|
||||||
|
"helpText": "Einzigartiger Name des Benutzers.",
|
||||||
|
"error": {
|
||||||
|
"validate": "Dieser Name ist ungültig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"label": "Anzeigename",
|
||||||
|
"helpText": "Anzeigename des Benutzers.",
|
||||||
|
"error": {
|
||||||
|
"validate": "Dieser Anzeigename ist ungültig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"label": "E-Mail",
|
||||||
|
"helpText": "E-Mail Adresse des Benutzers.",
|
||||||
|
"error": {
|
||||||
|
"validate": "Diese E-Mail ist ungültig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external": {
|
||||||
|
"label": "Extern",
|
||||||
|
"helpText": "Der Benutzer wird über ein Fremdsystem verwaltet."
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Neues Passwort",
|
||||||
|
"error": {
|
||||||
|
"validate": "Das Passwort muss zwischen 6 und 1024 Zeichen lang sein"
|
||||||
|
},
|
||||||
|
"confirmation": {
|
||||||
|
"label": "Passwort wiederholen",
|
||||||
|
"error": {
|
||||||
|
"validate": "Passwörter müssen identisch sein"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"label": "Aktiv",
|
||||||
|
"helpText": "Aktivierung oder Deaktivierung eines Benutzers."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"button": "Benutzer löschen",
|
"button": "Benutzer löschen",
|
||||||
|
|||||||
@@ -49,7 +49,52 @@
|
|||||||
},
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "Create User",
|
"title": "Create User",
|
||||||
"subtitle": "Create a new user"
|
"subtitle": "Create a new user",
|
||||||
|
"form": {
|
||||||
|
"submit": "Submit",
|
||||||
|
"submit-success-notification": "The user was updated successfully",
|
||||||
|
"name": {
|
||||||
|
"label": "User Name",
|
||||||
|
"helpText": "Unique name of the user.",
|
||||||
|
"error": {
|
||||||
|
"validate": "This name is invalid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"label": "Display Name",
|
||||||
|
"helpText": "Display name of the user.",
|
||||||
|
"error": {
|
||||||
|
"validate": "This display name is invalid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"label": "Email",
|
||||||
|
"helpText": "Email address of the user.",
|
||||||
|
"error": {
|
||||||
|
"validate": "This email address is invalid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external": {
|
||||||
|
"label": "External",
|
||||||
|
"helpText": "This user is managed by an external system."
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "New Password",
|
||||||
|
"error": {
|
||||||
|
"validate": "Password has to be between 6 and 1024 characters"
|
||||||
|
},
|
||||||
|
"confirmation": {
|
||||||
|
"label": "Confirm New Password",
|
||||||
|
"error": {
|
||||||
|
"validate": "Passwords have to be identical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"label": "Active",
|
||||||
|
"helpText": "Activate or deactivate the user."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"button": "Delete User",
|
"button": "Delete User",
|
||||||
|
|||||||
@@ -42,7 +42,50 @@
|
|||||||
},
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "Crear usuario",
|
"title": "Crear usuario",
|
||||||
"subtitle": "Crear un nuevo usuario"
|
"subtitle": "Crear un nuevo usuario",
|
||||||
|
"form": {
|
||||||
|
"name": {
|
||||||
|
"label": "Nombre de usuario",
|
||||||
|
"helpText": "Nombre único del usuario.",
|
||||||
|
"error": {
|
||||||
|
"validate": "El nombre es incorrecto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"label": "Nombre a mostrar",
|
||||||
|
"helpText": "Nombre de usuario a mostrar.",
|
||||||
|
"error": {
|
||||||
|
"validate": "El nombre a mostrar es incorrecto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"label": "Correo electrónico",
|
||||||
|
"helpText": "Dirección de correo electrónico del usuario.",
|
||||||
|
"error": {
|
||||||
|
"validate": "El correo electrónico es incorrecto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external": {
|
||||||
|
"label": "Externo",
|
||||||
|
"helpText": "This user is managed by an external system."
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Nueva contraseña",
|
||||||
|
"error": {
|
||||||
|
"validate": "La contraseña debe tener entre 6 y 1024 caracteres"
|
||||||
|
},
|
||||||
|
"confirmation": {
|
||||||
|
"label": "Confirme la contraseña",
|
||||||
|
"error": {
|
||||||
|
"validate": "Las contraseñas deben ser identicas"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"label": "Activo",
|
||||||
|
"helpText": "Activar o desactivar el usuario."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"button": "Borrar usuario",
|
"button": "Borrar usuario",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
// @ts-ignore
|
|
||||||
import Backend from "i18next-fetch-backend";
|
import Backend from "i18next-fetch-backend";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
@@ -48,19 +47,18 @@ i18n
|
|||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false // not needed for react!!
|
escapeValue: false, // not needed for react!!
|
||||||
},
|
},
|
||||||
|
|
||||||
react: {
|
react: {
|
||||||
wait: true,
|
useSuspense: false,
|
||||||
useSuspense: false
|
|
||||||
},
|
},
|
||||||
|
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: loadPath,
|
loadPath: loadPath,
|
||||||
init: {
|
init: {
|
||||||
credentials: "same-origin"
|
credentials: "same-origin",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// configure LanguageDetector
|
// configure LanguageDetector
|
||||||
@@ -69,8 +67,8 @@ i18n
|
|||||||
// we only use browser configuration
|
// we only use browser configuration
|
||||||
order: ["navigator"],
|
order: ["navigator"],
|
||||||
// we do not cache the detected language
|
// we do not cache the detected language
|
||||||
caches: []
|
caches: [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
|
|
||||||
import UserForm from "./UserForm";
|
|
||||||
import { User } from "@scm-manager/ui-types";
|
|
||||||
import "@scm-manager/ui-tests";
|
|
||||||
|
|
||||||
describe("for user creation", () => {
|
|
||||||
const fillForm = (userId: string, displayName: string, password: string, confirmation: string) => {
|
|
||||||
fireEvent.change(screen.getByTestId("input-username"), {
|
|
||||||
target: { value: userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-displayname"), {
|
|
||||||
target: { value: displayName },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-password"), {
|
|
||||||
target: { value: password },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-password-confirmation"), {
|
|
||||||
target: { value: confirmation },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should allow to create user", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fillForm("trillian", "Tricia McMillan", "password", "password");
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit empty form", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit form without user id", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fillForm("", "Arthur Dent", "password", "password");
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit form without display name", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fillForm("trillian", "", "password", "password");
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit form without password", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fillForm("trillian", "Tricia McMillan", "", "");
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit form with wrong password confirmation", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fillForm("trillian", "Tricia McMillan", "password", "different");
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for user edit", () => {
|
|
||||||
const user: User = {
|
|
||||||
name: "trillian",
|
|
||||||
mail: "tricia@hog.space",
|
|
||||||
displayName: "Tricia McMillan",
|
|
||||||
password: undefined,
|
|
||||||
active: true,
|
|
||||||
external: false,
|
|
||||||
_links: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should allow to edit user with changed display name", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-displayname"), {
|
|
||||||
target: { value: "Just Tricia" },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow to edit user with changed email", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-mail"), {
|
|
||||||
target: { value: "tricia@hg2g.com" },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow to edit user with changed active flag", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("checkbox-active"));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to submit unchanged user", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to edit user with incorrect email", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-mail"), {
|
|
||||||
target: { value: "do_not_reply" },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should prevent to edit user with empty display name", () => {
|
|
||||||
const mockSubmitForm = jest.fn();
|
|
||||||
|
|
||||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("input-displayname"), {
|
|
||||||
target: { value: "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("submit-button"));
|
|
||||||
|
|
||||||
expect(mockSubmitForm).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { User } from "@scm-manager/ui-types";
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
InputField,
|
|
||||||
Level,
|
|
||||||
PasswordConfirmation,
|
|
||||||
SubmitButton,
|
|
||||||
Subtitle,
|
|
||||||
validation as validator,
|
|
||||||
} from "@scm-manager/ui-components";
|
|
||||||
import * as userValidator from "./userValidation";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
submitForm: (p: User) => void;
|
|
||||||
user?: User;
|
|
||||||
loading?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserForm: FC<Props> = ({ submitForm, user, loading }) => {
|
|
||||||
const [t] = useTranslation("users");
|
|
||||||
const [userState, setUserState] = useState<User>({
|
|
||||||
name: "",
|
|
||||||
displayName: "",
|
|
||||||
mail: "",
|
|
||||||
password: "",
|
|
||||||
active: true,
|
|
||||||
external: false,
|
|
||||||
_links: {},
|
|
||||||
});
|
|
||||||
const [mailValidationError, setMailValidationError] = useState(false);
|
|
||||||
const [displayNameValidationError, setDisplayNameValidationError] = useState(false);
|
|
||||||
const [nameValidationError, setNameValidationError] = useState(false);
|
|
||||||
const [passwordValid, setPasswordValid] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
setUserState(user);
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const createUserComponentsAreInvalid = () => {
|
|
||||||
if (!user) {
|
|
||||||
return nameValidationError || !userState.name || (!userState.external && !passwordValid);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const editUserComponentsAreUnchanged = () => {
|
|
||||||
if (user) {
|
|
||||||
return (
|
|
||||||
user.displayName === userState.displayName &&
|
|
||||||
user.mail === userState.mail &&
|
|
||||||
user.active === userState.active &&
|
|
||||||
user.external === userState.external
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isInvalid = () => {
|
|
||||||
return (
|
|
||||||
createUserComponentsAreInvalid() ||
|
|
||||||
editUserComponentsAreUnchanged() ||
|
|
||||||
mailValidationError ||
|
|
||||||
displayNameValidationError ||
|
|
||||||
nameValidationError ||
|
|
||||||
(!user && userState && !userState.external && !userState.password) ||
|
|
||||||
!userState.displayName
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!isInvalid()) {
|
|
||||||
submitForm(userState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordChangeField = (
|
|
||||||
<PasswordConfirmation
|
|
||||||
passwordChanged={(password, isPasswordValid) => {
|
|
||||||
setPasswordValid(isPasswordValid);
|
|
||||||
setUserState({ ...userState, password });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
let nameField = null;
|
|
||||||
let subtitle = null;
|
|
||||||
if (!user) {
|
|
||||||
// create new user
|
|
||||||
nameField = (
|
|
||||||
<div className="column is-half">
|
|
||||||
<InputField
|
|
||||||
label={t("user.name")}
|
|
||||||
onChange={(name) => {
|
|
||||||
setNameValidationError(!!name && !validator.isNameValid(name));
|
|
||||||
setUserState({ ...userState, name });
|
|
||||||
}}
|
|
||||||
value={userState ? userState.name : ""}
|
|
||||||
validationError={nameValidationError}
|
|
||||||
errorMessage={t("validation.name-invalid")}
|
|
||||||
helpText={t("help.usernameHelpText")}
|
|
||||||
testId="input-username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// edit existing user
|
|
||||||
subtitle = <Subtitle subtitle={t("userForm.subtitle")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{subtitle}
|
|
||||||
<form onSubmit={submit}>
|
|
||||||
<div className="columns is-multiline">
|
|
||||||
{nameField}
|
|
||||||
<div className="column is-half">
|
|
||||||
<InputField
|
|
||||||
label={t("user.displayName")}
|
|
||||||
onChange={(displayName) => {
|
|
||||||
setDisplayNameValidationError(!userValidator.isDisplayNameValid(displayName));
|
|
||||||
setUserState({ ...userState, displayName });
|
|
||||||
}}
|
|
||||||
value={userState ? userState.displayName : ""}
|
|
||||||
validationError={displayNameValidationError}
|
|
||||||
errorMessage={t("validation.displayname-invalid")}
|
|
||||||
helpText={t("help.displayNameHelpText")}
|
|
||||||
testId="input-displayname"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="column is-half">
|
|
||||||
<InputField
|
|
||||||
label={t("user.mail")}
|
|
||||||
onChange={(mail) => {
|
|
||||||
setMailValidationError(!!mail && !validator.isMailValid(mail));
|
|
||||||
setUserState({ ...userState, mail });
|
|
||||||
}}
|
|
||||||
value={userState ? userState.mail : ""}
|
|
||||||
validationError={mailValidationError}
|
|
||||||
errorMessage={t("validation.mail-invalid")}
|
|
||||||
helpText={t("help.mailHelpText")}
|
|
||||||
testId="input-mail"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!user && (
|
|
||||||
<>
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column">
|
|
||||||
<Checkbox
|
|
||||||
label={t("user.externalFlag")}
|
|
||||||
onChange={(external) => setUserState({ ...userState, external })}
|
|
||||||
checked={userState.external}
|
|
||||||
helpText={t("help.externalFlagHelpText")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!userState.external && (
|
|
||||||
<>
|
|
||||||
{!user && passwordChangeField}
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column">
|
|
||||||
<Checkbox
|
|
||||||
label={t("user.active")}
|
|
||||||
onChange={(active) => setUserState({ ...userState, active })}
|
|
||||||
checked={userState ? userState.active : false}
|
|
||||||
helpText={t("help.activeHelpText")}
|
|
||||||
testId="checkbox-active"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Level right={<SubmitButton disabled={isInvalid()} loading={loading} label={t("userForm.button.submit")} />} />
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserForm;
|
|
||||||
@@ -22,28 +22,97 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Page } from "@scm-manager/ui-components";
|
|
||||||
import UserForm from "../components/UserForm";
|
|
||||||
import { useCreateUser } from "@scm-manager/ui-api";
|
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRequiredIndexLink } from "@scm-manager/ui-api";
|
||||||
|
import { Page } from "@scm-manager/ui-components";
|
||||||
|
import { Form, useCreateResource } from "@scm-manager/ui-forms";
|
||||||
|
import * as userValidator from "../components/userValidation";
|
||||||
|
import { User, UserCreation } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type UserCreationForm = Pick<UserCreation, "password" | "name" | "displayName" | "active" | "external" | "mail"> & {
|
||||||
|
passwordConfirmation: string;
|
||||||
|
};
|
||||||
|
|
||||||
const CreateUser: FC = () => {
|
const CreateUser: FC = () => {
|
||||||
const [t] = useTranslation("users");
|
const [t] = useTranslation("users");
|
||||||
const { error, isLoading, user, create } = useCreateUser();
|
const indexLink = useRequiredIndexLink("users");
|
||||||
|
const { submit, submissionResult: createdUser } = useCreateResource<UserCreationForm, User>(
|
||||||
|
indexLink,
|
||||||
|
["user", "users"],
|
||||||
|
(user) => user.name,
|
||||||
|
{
|
||||||
|
contentType: "application/vnd.scmm-user+json;v=2",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!!user) {
|
if (!!createdUser) {
|
||||||
return <Redirect to={`/user/${user.name}`} />;
|
return <Redirect to={`/user/${createdUser.name}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page title={t("createUser.title")} subtitle={t("createUser.subtitle")} showContentOnError={true}>
|
||||||
title={t("createUser.title")}
|
<Form
|
||||||
subtitle={t("createUser.subtitle")}
|
onSubmit={submit}
|
||||||
error={error || undefined}
|
translationPath={["users", "createUser.form"]}
|
||||||
showContentOnError={true}
|
defaultValues={{
|
||||||
>
|
name: "",
|
||||||
<UserForm submitForm={create} loading={isLoading} />
|
password: "",
|
||||||
|
passwordConfirmation: "",
|
||||||
|
active: true,
|
||||||
|
external: false,
|
||||||
|
displayName: "",
|
||||||
|
mail: "",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ watch }) => (
|
||||||
|
<>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Input
|
||||||
|
name="name"
|
||||||
|
rules={{
|
||||||
|
validate: userValidator.isNameValid,
|
||||||
|
}}
|
||||||
|
testId="input-username"
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
name="displayName"
|
||||||
|
rules={{
|
||||||
|
validate: userValidator.isDisplayNameValid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Input
|
||||||
|
name="mail"
|
||||||
|
className="is-half"
|
||||||
|
rules={{
|
||||||
|
validate: (email) => !email || userValidator.isMailValid(email),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Checkbox
|
||||||
|
name="external"
|
||||||
|
rules={{
|
||||||
|
deps: ["password"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row hidden={watch("external")}>
|
||||||
|
<Form.SecretConfirmation
|
||||||
|
name="password"
|
||||||
|
rules={{
|
||||||
|
validate: userValidator.isPasswordValid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row hidden={watch("external")}>
|
||||||
|
<Form.Checkbox name="active" />
|
||||||
|
</Form.Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,30 +22,52 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import UserForm from "../components/UserForm";
|
|
||||||
import DeleteUser from "./DeleteUser";
|
import DeleteUser from "./DeleteUser";
|
||||||
import { User } from "@scm-manager/ui-types";
|
import { User } from "@scm-manager/ui-types";
|
||||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
|
||||||
import UserConverter from "../components/UserConverter";
|
import UserConverter from "../components/UserConverter";
|
||||||
import { useUpdateUser } from "@scm-manager/ui-api";
|
import { Form, useUpdateResource } from "@scm-manager/ui-forms";
|
||||||
import UpdateNotification from "../../components/UpdateNotification";
|
import * as userValidator from "../components/userValidation";
|
||||||
|
import { Subtitle } from "@scm-manager/ui-components";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditUser: FC<Props> = ({ user }) => {
|
const EditUser: FC<Props> = ({ user }) => {
|
||||||
const { error, isLoading, update, isUpdated } = useUpdateUser();
|
const [t] = useTranslation("users");
|
||||||
|
const { submit } = useUpdateResource<User>(user, (user) => user.name, {
|
||||||
|
contentType: "application/vnd.scmm-user+json;v=2",
|
||||||
|
collectionName: ["user", "users"],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<UpdateNotification isUpdated={isUpdated} />
|
<Subtitle subtitle={t("userForm.subtitle")} />
|
||||||
<ErrorNotification error={error || undefined} />
|
<Form translationPath={["users", "createUser.form"]} defaultValues={user} onSubmit={submit}>
|
||||||
<UserForm submitForm={update} user={user} loading={isLoading} />
|
<Form.Row>
|
||||||
|
<Form.Input
|
||||||
|
name="displayName"
|
||||||
|
rules={{
|
||||||
|
validate: userValidator.isDisplayNameValid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
name="mail"
|
||||||
|
className="is-half"
|
||||||
|
rules={{
|
||||||
|
validate: (email) => !email || userValidator.isMailValid(email),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row hidden={user.external}>
|
||||||
|
<Form.Checkbox name="active" />
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
<hr />
|
<hr />
|
||||||
<UserConverter user={user} />
|
<UserConverter user={user} />
|
||||||
<DeleteUser user={user} />
|
<DeleteUser user={user} />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user