diff --git a/scm-ui/ui-components/src/markdown/LazyMarkdownView.tsx b/scm-ui/ui-components/src/markdown/LazyMarkdownView.tsx
index 9f5aac0226..b26a7c913e 100644
--- a/scm-ui/ui-components/src/markdown/LazyMarkdownView.tsx
+++ b/scm-ui/ui-components/src/markdown/LazyMarkdownView.tsx
@@ -241,4 +241,4 @@ class LazyMarkdownView extends React.Component {
}
}
-export default withRouter(withTranslation("repos")(LazyMarkdownView));
+export default withTranslation("repos")(withRouter(LazyMarkdownView));
diff --git a/scm-ui/ui-forms/.storybook/RemoveThemesPlugin.js b/scm-ui/ui-forms/.storybook/RemoveThemesPlugin.js
new file mode 100644
index 0000000000..9976554354
--- /dev/null
+++ b/scm-ui/ui-forms/.storybook/RemoveThemesPlugin.js
@@ -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
diff --git a/scm-ui/ui-forms/.storybook/main.js b/scm-ui/ui-forms/.storybook/main.js
new file mode 100644
index 0000000000..ca2439380d
--- /dev/null
+++ b/scm-ui/ui-forms/.storybook/main.js
@@ -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;
+ },
+};
diff --git a/scm-ui/ui-forms/.storybook/preview-head.html b/scm-ui/ui-forms/.storybook/preview-head.html
new file mode 100644
index 0000000000..d1c34ac7ef
--- /dev/null
+++ b/scm-ui/ui-forms/.storybook/preview-head.html
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/scm-ui/ui-forms/.storybook/preview.js b/scm-ui/ui-forms/.storybook/preview.js
new file mode 100644
index 0000000000..bcf0b7184a
--- /dev/null
+++ b/scm-ui/ui-forms/.storybook/preview.js
@@ -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) => (
+
+
+
+ ),
+ ],
+ themes: {
+ Decorator,
+ clearable: false,
+ default: "light",
+ list: [
+ { name: "light", color: "#fff" },
+ { name: "highcontrast", color: "#050514" },
+ { name: "dark", color: "#121212" },
+ ],
+ },
+};
diff --git a/scm-ui/ui-forms/package.json b/scm-ui/ui-forms/package.json
new file mode 100644
index 0000000000..04e72c8cf9
--- /dev/null
+++ b/scm-ui/ui-forms/package.json
@@ -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"
+ }
+}
diff --git a/scm-ui/ui-forms/src/Form.stories.mdx b/scm-ui/ui-forms/src/Form.stories.mdx
new file mode 100644
index 0000000000..e9982c6a65
--- /dev/null
+++ b/scm-ui/ui-forms/src/Form.stories.mdx
@@ -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";
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/Form.tsx b/scm-ui/ui-forms/src/Form.tsx
new file mode 100644
index 0000000000..28431eac0e
--- /dev/null
+++ b/scm-ui/ui-forms/src/Form.tsx
@@ -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> = Omit<
+ UseFormReturn,
+ "register" | "unregister" | "handleSubmit" | "control"
+>;
+
+const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label, hide }) => {
+ if (!label) {
+ return null;
+ }
+
+ return (
+
+
+ {label}
+
+ );
+};
+
+type Props, DefaultValues extends FormType> = {
+ children: ((renderProps: RenderProps) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
+ translationPath: [namespace: string, prefix: string];
+ onSubmit: SubmitHandler;
+ defaultValues: Omit;
+ readOnly?: boolean;
+ submitButtonTestId?: string;
+};
+
+function Form, DefaultValues extends FormType>({
+ children,
+ onSubmit,
+ defaultValues,
+ translationPath,
+ readOnly,
+ submitButtonTestId,
+}: Props) {
+ const form = useForm({
+ mode: "onChange",
+ defaultValues: defaultValues as DeepPartial,
+ });
+ 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();
+ 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(
+ (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 (
+
+
+
+ );
+}
+
+export default Object.assign(Form, {
+ Row: FormRow,
+ Input: ControlledInputField,
+ Checkbox: ControlledCheckboxField,
+ SecretConfirmation: ControlledSecretConfirmationField,
+});
diff --git a/scm-ui/ui-forms/src/FormRow.tsx b/scm-ui/ui-forms/src/FormRow.tsx
new file mode 100644
index 0000000000..b385e988ad
--- /dev/null
+++ b/scm-ui/ui-forms/src/FormRow.tsx
@@ -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>(
+ ({ className, children, hidden, ...rest }, ref) =>
+ hidden ? null : (
+
+ {children}
+
+ )
+);
+
+export default FormRow;
diff --git a/scm-ui/ui-forms/src/ScmFormContext.tsx b/scm-ui/ui-forms/src/ScmFormContext.tsx
new file mode 100644
index 0000000000..4d70876de7
--- /dev/null
+++ b/scm-ui/ui-forms/src/ScmFormContext.tsx
@@ -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 = UseFormReturn & {
+ t: TFunction;
+ readOnly?: boolean;
+};
+
+const ScmFormContext = React.createContext(null as unknown as ContextType);
+
+export function ScmFormContextProvider({ children, ...props }: PropsWithChildren>) {
+ return {children};
+}
+
+export function useScmFormContext() {
+ return useContext(ScmFormContext);
+}
diff --git a/scm-ui/ui-forms/src/base/Control.tsx b/scm-ui/ui-forms/src/base/Control.tsx
new file mode 100644
index 0000000000..7db727ba0c
--- /dev/null
+++ b/scm-ui/ui-forms/src/base/Control.tsx
@@ -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> = ({ className, children, ...rest }) => (
+
+ {children}
+
+);
+
+export default Control;
diff --git a/scm-ui/ui-forms/src/base/Field.tsx b/scm-ui/ui-forms/src/base/Field.tsx
new file mode 100644
index 0000000000..16e180ae94
--- /dev/null
+++ b/scm-ui/ui-forms/src/base/Field.tsx
@@ -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> = ({ className, children, ...rest }) => (
+
+ {children}
+
+);
+export default Field;
diff --git a/scm-ui/ui-forms/src/base/field-message/FieldMessage.tsx b/scm-ui/ui-forms/src/base/field-message/FieldMessage.tsx
new file mode 100644
index 0000000000..42a55bf72e
--- /dev/null
+++ b/scm-ui/ui-forms/src/base/field-message/FieldMessage.tsx
@@ -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) => (
+
{children}
+);
+export default FieldMessage;
diff --git a/scm-ui/ui-forms/src/base/help/Help.tsx b/scm-ui/ui-forms/src/base/help/Help.tsx
new file mode 100644
index 0000000000..ad4e08619e
--- /dev/null
+++ b/scm-ui/ui-forms/src/base/help/Help.tsx
@@ -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) => (
+
+);
+export default Help;
diff --git a/scm-ui/ui-forms/src/base/label/Label.tsx b/scm-ui/ui-forms/src/base/label/Label.tsx
new file mode 100644
index 0000000000..79d468fce2
--- /dev/null
+++ b/scm-ui/ui-forms/src/base/label/Label.tsx
@@ -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> = ({ className, children, ...rest }) => (
+
+);
+export default Label;
diff --git a/scm-ui/ui-forms/src/checkbox/Checkbox.stories.mdx b/scm-ui/ui-forms/src/checkbox/Checkbox.stories.mdx
new file mode 100644
index 0000000000..2f7ed30a8b
--- /dev/null
+++ b/scm-ui/ui-forms/src/checkbox/Checkbox.stories.mdx
@@ -0,0 +1,26 @@
+import {Meta, Story} from "@storybook/addon-docs";
+import Checkbox from "./Checkbox";
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/checkbox/Checkbox.tsx b/scm-ui/ui-forms/src/checkbox/Checkbox.tsx
new file mode 100644
index 0000000000..f80d547bd6
--- /dev/null
+++ b/scm-ui/ui-forms/src/checkbox/Checkbox.tsx
@@ -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, "type">;
+
+/**
+ * @see https://bulma.io/documentation/form/checkbox/
+ */
+const Checkbox = React.forwardRef(
+ ({ 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
+
+ )
+);
+export default Checkbox;
diff --git a/scm-ui/ui-forms/src/checkbox/CheckboxField.tsx b/scm-ui/ui-forms/src/checkbox/CheckboxField.tsx
new file mode 100644
index 0000000000..03fc6f82fa
--- /dev/null
+++ b/scm-ui/ui-forms/src/checkbox/CheckboxField.tsx
@@ -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;
+
+const CheckboxField = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+export default CheckboxField;
diff --git a/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.stories.mdx b/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.stories.mdx
new file mode 100644
index 0000000000..466161e437
--- /dev/null
+++ b/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.stories.mdx
@@ -0,0 +1,36 @@
+import { Meta, Story } from "@storybook/addon-docs";
+import Form from "../Form";
+import ControlledCheckboxField from "./ControlledCheckboxField";
+
+ (
+
+ ),
+ ]}
+/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.tsx b/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.tsx
new file mode 100644
index 0000000000..ae20e877dc
--- /dev/null
+++ b/scm-ui/ui-forms/src/checkbox/ControlledCheckboxField.tsx
@@ -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> = Omit<
+ ComponentProps,
+ "label" | "defaultValue" | "required" | keyof ControllerRenderProps
+> & {
+ name: Path;
+ label?: string;
+ rules?: Pick;
+};
+
+function ControlledInputField>({
+ name,
+ label,
+ helpText,
+ rules,
+ className,
+ testId,
+ defaultChecked,
+ readOnly,
+ ...props
+}: Props) {
+ const { control, t, readOnly: formReadonly } = useScmFormContext();
+ const labelTranslation = label || t(`${name}.label`) || "";
+ const helpTextTranslation = helpText || t(`${name}.helpText`);
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+export default ControlledInputField;
diff --git a/scm-ui/ui-forms/src/index.ts b/scm-ui/ui-forms/src/index.ts
new file mode 100644
index 0000000000..c590733aed
--- /dev/null
+++ b/scm-ui/ui-forms/src/index.ts
@@ -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";
diff --git a/scm-ui/ui-forms/src/input/ControlledInputField.stories.mdx b/scm-ui/ui-forms/src/input/ControlledInputField.stories.mdx
new file mode 100644
index 0000000000..93493ea9b1
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/ControlledInputField.stories.mdx
@@ -0,0 +1,36 @@
+import { Meta, Story } from "@storybook/addon-docs";
+import Form from "../Form";
+import ControlledInputField from "./ControlledInputField";
+
+ (
+
+ ),
+ ]}
+/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/input/ControlledInputField.tsx b/scm-ui/ui-forms/src/input/ControlledInputField.tsx
new file mode 100644
index 0000000000..d1503e03d4
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/ControlledInputField.tsx
@@ -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> = Omit<
+ ComponentProps,
+ "error" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
+> & {
+ rules?: ComponentProps["rules"];
+ name: Path;
+ label?: string;
+};
+
+function ControlledInputField>({
+ name,
+ label,
+ helpText,
+ rules,
+ className,
+ testId,
+ defaultValue,
+ readOnly,
+ ...props
+}: Props) {
+ const { control, t, readOnly: formReadonly } = useScmFormContext();
+ const labelTranslation = label || t(`${name}.label`) || "";
+ const helpTextTranslation = helpText || t(`${name}.helpText`);
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+export default ControlledInputField;
diff --git a/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.stories.mdx b/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.stories.mdx
new file mode 100644
index 0000000000..4745b7b8cd
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.stories.mdx
@@ -0,0 +1,39 @@
+import { Meta, Story } from "@storybook/addon-docs";
+import Form from "../Form";
+import ControlledSecretConfirmationField from "./ControlledSecretConfirmationField";
+
+ (
+
+ ),
+ ]}
+/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.tsx b/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.tsx
new file mode 100644
index 0000000000..e571eaeb60
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/ControlledSecretConfirmationField.tsx
@@ -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> = Omit<
+ ComponentProps,
+ "error" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
+> & {
+ rules?: ComponentProps["rules"];
+ name: Path;
+ label?: string;
+ confirmationLabel?: string;
+ confirmationHelpText?: string;
+ confirmationErrorMessage?: string;
+ confirmationTestId?: string;
+};
+
+export default function ControlledSecretConfirmationField>({
+ name,
+ label,
+ confirmationLabel,
+ helpText,
+ confirmationHelpText,
+ rules,
+ confirmationErrorMessage,
+ className,
+ testId,
+ confirmationTestId,
+ defaultValue,
+ readOnly,
+ ...props
+}: Props) {
+ 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 (
+ <>
+ (
+
+ )}
+ />
+ (
+
+ )}
+ rules={{
+ validate: (value) => secretValue === value || confirmationErrorMessageTranslation,
+ }}
+ />
+ >
+ );
+}
diff --git a/scm-ui/ui-forms/src/input/Input.stories.mdx b/scm-ui/ui-forms/src/input/Input.stories.mdx
new file mode 100644
index 0000000000..32970386b1
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/Input.stories.mdx
@@ -0,0 +1,22 @@
+import { Meta, Story } from "@storybook/addon-docs";
+import Input from "./Input"
+
+
+
+This will be our latest input component
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {r.focus()}} />
+
diff --git a/scm-ui/ui-forms/src/input/Input.tsx b/scm-ui/ui-forms/src/input/Input.tsx
new file mode 100644
index 0000000000..cc72eb04ed
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/Input.tsx
@@ -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;
+
+const Input = React.forwardRef(({ variant, className, testId, ...props }, ref) => {
+ return (
+
+ );
+});
+
+export default Input;
diff --git a/scm-ui/ui-forms/src/input/InputField.stories.mdx b/scm-ui/ui-forms/src/input/InputField.stories.mdx
new file mode 100644
index 0000000000..761470af7e
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/InputField.stories.mdx
@@ -0,0 +1,22 @@
+import { Meta, Story } from "@storybook/addon-docs";
+import InputField from "./InputField";
+
+
+
+This will be our first form field molecule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-ui/ui-forms/src/input/InputField.tsx b/scm-ui/ui-forms/src/input/InputField.tsx
new file mode 100644
index 0000000000..c10f61b344
--- /dev/null
+++ b/scm-ui/ui-forms/src/input/InputField.tsx
@@ -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, "type">;
+
+/**
+ * @see https://bulma.io/documentation/form/input/
+ */
+const InputField = React.forwardRef(
+ ({ label, helpText, error, className, ...props }, ref) => {
+ const variant = error ? "danger" : undefined;
+ return (
+
+
+
+
+
+ {error ? {error} : null}
+
+ );
+ }
+);
+export default InputField;
diff --git a/scm-ui/ui-forms/src/resourceHooks.ts b/scm-ui/ui-forms/src/resourceHooks.ts
new file mode 100644
index 0000000000..4e8ace4621
--- /dev/null
+++ b/scm-ui/ui-forms/src/resourceHooks.ts
@@ -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 = {
+ submit: (resource: I) => Promise;
+ isLoading: boolean;
+ error: Error | null;
+ submissionResult?: O;
+};
+
+type MutatingResourceOptions = {
+ contentType?: string;
+};
+
+const createResource = (link: string, contentType: string) => {
+ return (payload: I): Promise => {
+ 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 = (
+ link: string,
+ [entityKey, collectionName]: QueryKeyPair,
+ idFactory: (createdResource: O) => string,
+ { contentType = "application/json" }: CreateResourceOptions = {}
+): MutationResult => {
+ const queryClient = useQueryClient();
+ const { mutateAsync, data, isLoading, error } = useMutation(createResource(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 = (
+ link: LinkOrHalLink,
+ idFactory: (createdResource: T) => string,
+ {
+ contentType = "application/json",
+ collectionName: [entityQueryKey, collectionName] = ["", ""],
+ }: UpdateResourceOptions = {}
+): MutationResult => {
+ const queryClient = useQueryClient();
+ const { mutateAsync, isLoading, error, data } = useMutation(
+ (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 = (
+ idFactory: (createdResource: T) => string,
+ { collectionName: [entityQueryKey, collectionName] = ["", ""] }: DeleteResourceOptions = {}
+): MutationResult => {
+ const queryClient = useQueryClient();
+ const { mutateAsync, isLoading, error, data } = useMutation(
+ (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,
+ };
+};
diff --git a/scm-ui/ui-forms/src/variants.ts b/scm-ui/ui-forms/src/variants.ts
new file mode 100644
index 0000000000..bf86eeb543
--- /dev/null
+++ b/scm-ui/ui-forms/src/variants.ts
@@ -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);
diff --git a/scm-ui/ui-forms/tsconfig.json b/scm-ui/ui-forms/tsconfig.json
new file mode 100644
index 0000000000..7e3ee63a2d
--- /dev/null
+++ b/scm-ui/ui-forms/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@scm-manager/tsconfig"
+}
diff --git a/scm-ui/ui-plugins/package.json b/scm-ui/ui-plugins/package.json
index 916c7cf1ba..377aba0155 100644
--- a/scm-ui/ui-plugins/package.json
+++ b/scm-ui/ui-plugins/package.json
@@ -12,7 +12,7 @@
"query-string": "6.14.1",
"react": "^17.0.1",
"react-hook-form": "^7.5.1",
- "react-i18next": "^10.13.1",
+ "react-i18next": "11",
"react-redux": "^5.0.7",
"react-router": "^5.3.1",
"react-router-dom": "^5.3.1",
@@ -43,4 +43,4 @@
"publishConfig": {
"access": "public"
}
-}
\ No newline at end of file
+}
diff --git a/scm-ui/ui-shortcuts/package.json b/scm-ui/ui-shortcuts/package.json
index 92fd6d851a..196e1e2c84 100644
--- a/scm-ui/ui-shortcuts/package.json
+++ b/scm-ui/ui-shortcuts/package.json
@@ -18,7 +18,7 @@
},
"peerDependencies": {
"react": "17",
- "react-i18next": "10"
+ "react-i18next": "11"
},
"dependencies": {
"mousetrap": "1.6.5"
@@ -50,4 +50,4 @@
"jest-extended/all"
]
}
-}
\ No newline at end of file
+}
diff --git a/scm-ui/ui-shortcuts/src/useShortcut.ts b/scm-ui/ui-shortcuts/src/useShortcut.ts
index 690a800b0b..f2785e7652 100644
--- a/scm-ui/ui-shortcuts/src/useShortcut.ts
+++ b/scm-ui/ui-shortcuts/src/useShortcut.ts
@@ -39,7 +39,7 @@ export type UseShortcutOptions = {
*
* If no description is supplied, there will be no entry in the shortcut summary table.
*/
- description?: string;
+ description?: string | null;
};
/**
diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json
index 4b807aae1e..85c99b9f22 100644
--- a/scm-ui/ui-webapp/package.json
+++ b/scm-ui/ui-webapp/package.json
@@ -12,16 +12,17 @@
"@scm-manager/ui-text": "2.40.2-SNAPSHOT",
"@scm-manager/ui-shortcuts": "2.40.2-SNAPSHOT",
"@scm-manager/ui-legacy": "2.40.2-SNAPSHOT",
+ "@scm-manager/ui-forms": "2.40.2-SNAPSHOT",
"classnames": "^2.2.5",
"history": "^4.10.1",
- "i18next": "^19.6.0",
- "i18next-browser-languagedetector": "^4.0.0",
- "i18next-fetch-backend": "^2.2.0",
+ "i18next": "21",
+ "i18next-browser-languagedetector": "6",
+ "i18next-fetch-backend": "4",
"query-string": "6.14.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^7.5.1",
- "react-i18next": "^10.13.1",
+ "react-i18next": "11",
"react-router": "^5.3.1",
"react-router-dom": "^5.3.1",
"react-select": "^2.1.2",
@@ -69,4 +70,4 @@
"publishConfig": {
"access": "public"
}
-}
\ No newline at end of file
+}
diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json
index 388e0cd752..d8b3f2945e 100644
--- a/scm-ui/ui-webapp/public/locales/de/users.json
+++ b/scm-ui/ui-webapp/public/locales/de/users.json
@@ -49,7 +49,52 @@
},
"createUser": {
"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": {
"button": "Benutzer löschen",
diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json
index 903153a0ec..2f68df6d88 100644
--- a/scm-ui/ui-webapp/public/locales/en/users.json
+++ b/scm-ui/ui-webapp/public/locales/en/users.json
@@ -49,7 +49,52 @@
},
"createUser": {
"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": {
"button": "Delete User",
diff --git a/scm-ui/ui-webapp/public/locales/es/users.json b/scm-ui/ui-webapp/public/locales/es/users.json
index 05f8fa14e3..8895eeb024 100644
--- a/scm-ui/ui-webapp/public/locales/es/users.json
+++ b/scm-ui/ui-webapp/public/locales/es/users.json
@@ -42,7 +42,50 @@
},
"createUser": {
"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": {
"button": "Borrar usuario",
diff --git a/scm-ui/ui-webapp/src/i18n.ts b/scm-ui/ui-webapp/src/i18n.ts
index a3b6c2eba6..c4d3028f91 100644
--- a/scm-ui/ui-webapp/src/i18n.ts
+++ b/scm-ui/ui-webapp/src/i18n.ts
@@ -23,7 +23,6 @@
*/
import i18n from "i18next";
-// @ts-ignore
import Backend from "i18next-fetch-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
@@ -48,19 +47,18 @@ i18n
debug: false,
interpolation: {
- escapeValue: false // not needed for react!!
+ escapeValue: false, // not needed for react!!
},
react: {
- wait: true,
- useSuspense: false
+ useSuspense: false,
},
backend: {
loadPath: loadPath,
init: {
- credentials: "same-origin"
- }
+ credentials: "same-origin",
+ },
},
// configure LanguageDetector
@@ -69,8 +67,8 @@ i18n
// we only use browser configuration
order: ["navigator"],
// we do not cache the detected language
- caches: []
- }
+ caches: [],
+ },
});
export default i18n;
diff --git a/scm-ui/ui-webapp/src/users/components/UserForm.test.tsx b/scm-ui/ui-webapp/src/users/components/UserForm.test.tsx
deleted file mode 100644
index f5ce5415b6..0000000000
--- a/scm-ui/ui-webapp/src/users/components/UserForm.test.tsx
+++ /dev/null
@@ -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();
-
- 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();
-
- fireEvent.click(screen.getByTestId("submit-button"));
-
- expect(mockSubmitForm).not.toBeCalled();
- });
-
- it("should prevent to submit form without user id", () => {
- const mockSubmitForm = jest.fn();
-
- render();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- fireEvent.click(screen.getByTestId("submit-button"));
-
- expect(mockSubmitForm).not.toBeCalled();
- });
-
- it("should prevent to edit user with incorrect email", () => {
- const mockSubmitForm = jest.fn();
-
- render();
-
- 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();
-
- fireEvent.change(screen.getByTestId("input-displayname"), {
- target: { value: "" },
- });
-
- fireEvent.click(screen.getByTestId("submit-button"));
-
- expect(mockSubmitForm).not.toBeCalled();
- });
-});
diff --git a/scm-ui/ui-webapp/src/users/components/UserForm.tsx b/scm-ui/ui-webapp/src/users/components/UserForm.tsx
deleted file mode 100644
index eadc519130..0000000000
--- a/scm-ui/ui-webapp/src/users/components/UserForm.tsx
+++ /dev/null
@@ -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 = ({ submitForm, user, loading }) => {
- const [t] = useTranslation("users");
- const [userState, setUserState] = useState({
- 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) => {
- event.preventDefault();
- if (!isInvalid()) {
- submitForm(userState);
- }
- };
-
- const passwordChangeField = (
- {
- setPasswordValid(isPasswordValid);
- setUserState({ ...userState, password });
- }}
- />
- );
- let nameField = null;
- let subtitle = null;
- if (!user) {
- // create new user
- nameField = (
-