mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-05-07 07:26:07 +02: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
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"
|
||||
}
|
||||
Reference in New Issue
Block a user