mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-05-06 08:37:13 +02:00
Move frontendcomponents into ui-core
The new architecture has been implemented. Co-authored-by: Eduard Heimbuch<eduard.heimbuch@cloudogu.com> Committed-by: Eduard Heimbuch<eduard.heimbuch@cloudogu.com> Pushed-by: Tarik Gürsoy<tarik.guersoy@cloudogu.com> Committed-by: Tarik Gürsoy<tarik.guersoy@cloudogu.com> Pushed-by: Eduard Heimbuch<eduard.heimbuch@cloudogu.com> Co-authored-by: Tarik Gürsoy<tarik.guersoy@cloudogu.com> Reviewed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
@@ -1,57 +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.
|
||||
*/
|
||||
|
||||
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
|
||||
@@ -1,92 +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.
|
||||
*/
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -1,26 +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.
|
||||
-->
|
||||
|
||||
<link id="ui-theme" data-theme="light" rel="stylesheet" type="text/css" href="/ui-theme-light.css">
|
||||
|
||||
@@ -1,72 +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, { 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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -22,6 +22,22 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export const variants = ["danger"] as const;
|
||||
export type Variant = typeof variants[number];
|
||||
export const createVariantClass = (variant?: Variant) => (variant ? `is-${variant}` : undefined);
|
||||
export {
|
||||
Field,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
ConfigurationForm,
|
||||
SelectField,
|
||||
ComboboxField,
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
useCreateResource,
|
||||
useUpdateResource,
|
||||
useDeleteResource,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupField,
|
||||
ChipInputField,
|
||||
Form
|
||||
} from "@scm-manager/ui-core";
|
||||
@@ -2,52 +2,15 @@
|
||||
"name": "@scm-manager/ui-forms",
|
||||
"private": false,
|
||||
"version": "3.0.0-SNAPSHOT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"module": "build/index.mjs",
|
||||
"main": "index.ts",
|
||||
"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": "3.0.0-SNAPSHOT",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-docs": "^6.5.14",
|
||||
"@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",
|
||||
"babel-loader": "^8.2.5",
|
||||
"storybook-addon-mock": "^3.2.0",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
"tsup": "^6.2.3"
|
||||
"@scm-manager/tsconfig": "^2.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scm-manager/ui-components": "3.0.0-SNAPSHOT",
|
||||
"classnames": "^2.3.1",
|
||||
"react": "17",
|
||||
"react-hook-form": "7",
|
||||
"react-i18next": "11",
|
||||
"react-query": "3",
|
||||
"styled-components": "5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.0.1",
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||
"@scm-manager/ui-api": "3.0.0-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "3.0.0-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "3.0.0-SNAPSHOT"
|
||||
"@scm-manager/ui-core": "3.0.0-SNAPSHOT"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
"eslintConfig": {
|
||||
@@ -56,4 +19,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +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, { useCallback, useEffect } from "react";
|
||||
import { ScmFormPathContextProvider, useScmFormPathContext } from "./FormPathContext";
|
||||
import { ScmFormContextProvider, useScmFormContext } from "./ScmFormContext";
|
||||
import { DeepPartial, UseFieldArrayReturn, useForm, UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
import { prefixWithoutIndices } from "./helpers";
|
||||
import { useScmFormListContext } from "./ScmFormListContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||
UseFormReturn<T>,
|
||||
"register" | "unregister" | "handleSubmit" | "control"
|
||||
>;
|
||||
|
||||
type Props<FormType extends Record<string, unknown>, DefaultValues extends FormType> = {
|
||||
children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
|
||||
defaultValues: DefaultValues;
|
||||
submit?: (data: FormType, append: UseFieldArrayReturn["append"]) => unknown;
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
disableSubmitWhenDirty?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
*/
|
||||
function AddListEntryForm<FormType extends Record<string, unknown>, DefaultValues extends FormType>({
|
||||
children,
|
||||
defaultValues,
|
||||
submit,
|
||||
disableSubmitWhenDirty = true,
|
||||
}: Props<FormType, DefaultValues>) {
|
||||
const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form" });
|
||||
const { readOnly, t } = useScmFormContext();
|
||||
const nameWithPrefix = useScmFormPathContext();
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const { append, isNested } = useScmFormListContext();
|
||||
const form = useForm<FormType, DefaultValues>({
|
||||
mode: "onChange",
|
||||
defaultValues: defaultValues as DeepPartial<FormType>,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
formState: { isSubmitSuccessful, isDirty, isValid },
|
||||
} = form;
|
||||
|
||||
const translateWithExtraPrefix = useCallback<typeof t>(
|
||||
(key, ...args) => t(`${prefixedNameWithoutIndices}.${key}`, ...(args as any)),
|
||||
[prefixedNameWithoutIndices, t]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback((data) => (submit ? submit(data, append) : append(data)), [append, submit]);
|
||||
|
||||
const submitButtonLabel = translateWithExtraPrefix("add", {
|
||||
defaultValue: defaultTranslate("list.add.label", { entity: translateWithExtraPrefix("entity") }),
|
||||
});
|
||||
const titleLabel = translateWithExtraPrefix("title", {
|
||||
defaultValue: defaultTranslate("list.title.label", { entity: translateWithExtraPrefix("entity") }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, [defaultValues, isSubmitSuccessful, reset]);
|
||||
|
||||
if (readOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScmFormContextProvider {...form} t={translateWithExtraPrefix} formId={nameWithPrefix}>
|
||||
<ScmFormPathContextProvider path="">
|
||||
<h3 className="subtitle is-5">{titleLabel}</h3>
|
||||
<form id={nameWithPrefix} onSubmit={form.handleSubmit(onSubmit)} noValidate></form>
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
<div className="level-left">
|
||||
<Button
|
||||
form={nameWithPrefix}
|
||||
type="submit"
|
||||
variant={isNested ? undefined : "secondary"}
|
||||
disabled={(disableSubmitWhenDirty && !isDirty) || !isValid}
|
||||
>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<StyledHr />
|
||||
</ScmFormPathContextProvider>
|
||||
</ScmFormContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddListEntryForm;
|
||||
@@ -1,59 +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 { useConfigLink } from "@scm-manager/ui-api";
|
||||
import { Loading } from "@scm-manager/ui-components";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import Form from "./Form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props<T extends HalRepresentation> =
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
Omit<ComponentProps<typeof Form<T>>, "onSubmit" | "defaultValues" | "readOnly" | "successMessageFallback">
|
||||
& {
|
||||
link: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
export function ConfigurationForm<T extends HalRepresentation>({ link, children, ...formProps }: Props<T>) {
|
||||
const { initialConfiguration, isReadOnly, update, isLoading } = useConfigLink<T>(link);
|
||||
const [t] = useTranslation("commons", { keyPrefix: "form" });
|
||||
|
||||
if (isLoading || !initialConfiguration) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={update} defaultValues={initialConfiguration} readOnly={isReadOnly}
|
||||
successMessageFallback={t("submit-success-notification")} {...formProps}>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigurationForm;
|
||||
@@ -1,453 +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.
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import React, { useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Form from "./Form";
|
||||
import FormRow from "./FormRow";
|
||||
import ControlledInputField from "./input/ControlledInputField";
|
||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||
import ControlledList from "./list/ControlledList";
|
||||
import ControlledSelectField from "./select/ControlledSelectField";
|
||||
import ControlledColumn from "./table/ControlledColumn";
|
||||
import ControlledTable from "./table/ControlledTable";
|
||||
import AddListEntryForm from "./AddListEntryForm";
|
||||
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import ControlledChipInputField from "./chip-input/ControlledChipInputField";
|
||||
import Combobox from "./combobox/Combobox";
|
||||
import ControlledComboboxField from "./combobox/ControlledComboboxField";
|
||||
import { defaultOptionFactory } from "./helpers";
|
||||
import ChipInput from "./headless-chip-input/ChipInput";
|
||||
|
||||
export type SimpleWebHookConfiguration = {
|
||||
urlPattern: string;
|
||||
executeOnEveryCommit: boolean;
|
||||
sendCommitData: boolean;
|
||||
method: string;
|
||||
headers: WebhookHeader[];
|
||||
};
|
||||
|
||||
export type WebhookHeader = {
|
||||
key: string;
|
||||
value: string;
|
||||
concealed: boolean;
|
||||
};
|
||||
|
||||
storiesOf("Forms", module)
|
||||
.add("Creation", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "",
|
||||
password: "",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField rules={{ required: true }} name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Editing", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
passwordConfirmation: "",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Reset", () => {
|
||||
type FormType = {
|
||||
name: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
active: boolean;
|
||||
date: string;
|
||||
};
|
||||
const [defaultValues, setValues] = useState({
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
passwordConfirmation: "secret",
|
||||
active: true,
|
||||
date: "",
|
||||
});
|
||||
const resetValues = useRef({
|
||||
name: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
active: false,
|
||||
date: "",
|
||||
});
|
||||
return (
|
||||
<Form<FormType>
|
||||
onSubmit={(vals) => {
|
||||
console.log(vals);
|
||||
setValues(vals);
|
||||
}}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={defaultValues as HalRepresentation & FormType}
|
||||
withResetTo={resetValues.current}
|
||||
withDiscardChanges
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" rules={{ required: true }} />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledInputField name="date" type="date" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
})
|
||||
.add("Layout", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "",
|
||||
password: "",
|
||||
method: "",
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
<ControlledSelectField name="method">
|
||||
{["", "post", "get", "put"].map((value) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</ControlledSelectField>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("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 hidden={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 hidden={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>
|
||||
))
|
||||
.add("RepoConfiguration", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
url: "",
|
||||
option: "",
|
||||
anotherOption: "",
|
||||
disableA: false,
|
||||
disableB: false,
|
||||
disableC: true,
|
||||
labels: ["test"],
|
||||
people: [{ displayName: "Trillian", id: 2 }],
|
||||
author: null,
|
||||
}}
|
||||
>
|
||||
<ControlledInputField name="url" />
|
||||
<ControlledInputField name="option" />
|
||||
<ControlledInputField name="anotherOption" />
|
||||
<ControlledCheckboxField name="disableA" />
|
||||
<ControlledCheckboxField name="disableB" />
|
||||
<ControlledCheckboxField name="disableC" />
|
||||
<ControlledChipInputField name="labels" />
|
||||
<ControlledChipInputField
|
||||
name="people"
|
||||
optionFactory={(person) => ({ label: person.displayName, value: person })}
|
||||
>
|
||||
<Combobox
|
||||
options={[
|
||||
{ label: "Zenod", value: { id: 1, displayName: "Zenod" } },
|
||||
{ label: "Arthur", value: { id: 3, displayName: "Arthur" } },
|
||||
{ label: "Cookie Monster", value: { id: 4, displayName: "Cookie Monster" } },
|
||||
]}
|
||||
/>
|
||||
</ControlledChipInputField>
|
||||
<ControlledComboboxField
|
||||
options={["Zenod", "Arthur", "Trillian"].map(defaultOptionFactory)}
|
||||
name="author"
|
||||
nullable
|
||||
/>
|
||||
</Form>
|
||||
))
|
||||
.add("ReadOnly", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
active: true,
|
||||
labels: ["test", "hero"],
|
||||
}}
|
||||
readOnly
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledChipInputField name="labels" aria-label="Test" placeholder="New label..." />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Nested", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
webhooks: [
|
||||
{
|
||||
urlPattern: "https://hitchhiker.com",
|
||||
executeOnEveryCommit: false,
|
||||
sendCommitData: false,
|
||||
method: "post",
|
||||
headers: [
|
||||
{
|
||||
key: "test",
|
||||
value: "val",
|
||||
concealed: false,
|
||||
},
|
||||
{
|
||||
key: "secret",
|
||||
value: "password",
|
||||
concealed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
urlPattern: "http://test.com",
|
||||
executeOnEveryCommit: false,
|
||||
sendCommitData: false,
|
||||
method: "get",
|
||||
headers: [
|
||||
{
|
||||
key: "other",
|
||||
value: "thing",
|
||||
concealed: true,
|
||||
},
|
||||
{
|
||||
key: "stuff",
|
||||
value: "haha",
|
||||
concealed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
withResetTo={{
|
||||
webhooks: [
|
||||
{
|
||||
urlPattern: "https://other.com",
|
||||
executeOnEveryCommit: true,
|
||||
sendCommitData: true,
|
||||
method: "get",
|
||||
headers: [
|
||||
{
|
||||
key: "change",
|
||||
value: "new",
|
||||
concealed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
urlPattern: "http://things.com",
|
||||
executeOnEveryCommit: false,
|
||||
sendCommitData: true,
|
||||
method: "put",
|
||||
headers: [
|
||||
{
|
||||
key: "stuff",
|
||||
value: "haha",
|
||||
concealed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<ScmFormListContextProvider name="webhooks">
|
||||
<ControlledList withDelete>
|
||||
{({ value: webhook }) => (
|
||||
<>
|
||||
<FormRow>
|
||||
<ControlledSelectField name="method">
|
||||
{["post", "get", "put"].map((value) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</ControlledSelectField>
|
||||
<ControlledInputField name="urlPattern" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="executeOnEveryCommit" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="sendCommitData" />
|
||||
</FormRow>
|
||||
<details className="has-background-dark-25 mb-2 p-2">
|
||||
<summary className="is-clickable">Headers</summary>
|
||||
<div>
|
||||
<ScmFormListContextProvider name="headers">
|
||||
<ControlledTable withDelete>
|
||||
<ControlledColumn name="key" />
|
||||
<ControlledColumn name="value" />
|
||||
<ControlledColumn name="concealed">{(value) => (value ? <b>Hallo</b> : null)}</ControlledColumn>
|
||||
</ControlledTable>
|
||||
<AddListEntryForm defaultValues={{ key: "", value: "", concealed: false }}>
|
||||
<FormRow>
|
||||
<ControlledInputField
|
||||
name="key"
|
||||
rules={{
|
||||
validate: (newKey) =>
|
||||
!(webhook as SimpleWebHookConfiguration).headers.some(({ key }) => newKey === key),
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledInputField name="value" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="concealed" />
|
||||
</FormRow>
|
||||
</AddListEntryForm>
|
||||
</ScmFormListContextProvider>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</ControlledList>
|
||||
</ScmFormListContextProvider>
|
||||
</Form>
|
||||
))
|
||||
.add("Controlled Chip Input with add", () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
branches: ["main", "develop"],
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledChipInputField name="branches" ref={ref} />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
@@ -1,214 +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, 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 styled from "styled-components";
|
||||
import { setValues } from "./helpers";
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||
UseFormReturn<T>,
|
||||
"register" | "unregister" | "handleSubmit" | "control"
|
||||
>;
|
||||
|
||||
const ButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
`;
|
||||
|
||||
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: DefaultValues;
|
||||
readOnly?: boolean;
|
||||
submitButtonTestId?: string;
|
||||
/**
|
||||
* Renders a button which resets the form to its default.
|
||||
* This reflects the default browser behavior for a form *reset*.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
withDiscardChanges?: boolean;
|
||||
/**
|
||||
* Renders a button which acts as if a user manually updated all fields to supplied values.
|
||||
* The default use-case for this is to clear forms and this is also how the button is labelled.
|
||||
* You can also use it to reset the form to an original state, but it is then advised to change the button label
|
||||
* to *Reset to Defaults* by defining the *reset* translation in the form's translation object's root.
|
||||
*
|
||||
* > *Important Note:* This mechanism cannot be used to change the number of items in lists,
|
||||
* > neither on the root level nor nested.
|
||||
* > It is therefore advised not to use this property when lists or nested forms are involved.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
withResetTo?: DefaultValues;
|
||||
/**
|
||||
* Message to display after a successful submit if no translation key is defined.
|
||||
*
|
||||
* If this is not supplied and the root level `submit-success-notification` translation key is not set,
|
||||
* no message is displayed at all.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
successMessageFallback?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType = FormType>({
|
||||
children,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
translationPath,
|
||||
readOnly,
|
||||
withResetTo,
|
||||
withDiscardChanges,
|
||||
successMessageFallback,
|
||||
submitButtonTestId,
|
||||
}: Props<FormType, DefaultValues>) {
|
||||
const form = useForm<FormType>({
|
||||
mode: "onChange",
|
||||
defaultValues: defaultValues as DeepPartial<FormType>,
|
||||
});
|
||||
const { formState, handleSubmit, reset, setValue } = form;
|
||||
const [ns, prefix] = translationPath;
|
||||
const { t } = useTranslation(ns, { keyPrefix: prefix });
|
||||
const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form" });
|
||||
const translateWithFallback = useCallback<typeof t>(
|
||||
(key, ...args) => {
|
||||
const translation = t(key, ...(args as any));
|
||||
if (translation === `${prefix}.${key}`) {
|
||||
return "";
|
||||
}
|
||||
return translation;
|
||||
},
|
||||
[prefix, t]
|
||||
);
|
||||
const { isDirty, isValid, isSubmitting, isSubmitSuccessful } = formState;
|
||||
const [error, setError] = useState<Error | null | undefined>();
|
||||
const [showSuccessNotification, setShowSuccessNotification] = useState(false);
|
||||
const submitButtonLabel = t("submit", { defaultValue: defaultTranslate("submit") });
|
||||
const resetButtonLabel = t("reset", { defaultValue: defaultTranslate("reset") });
|
||||
const discardChangesButtonLabel = t("discardChanges", { defaultValue: defaultTranslate("discardChanges") });
|
||||
const successNotification = translateWithFallback("submit-success-notification", {
|
||||
defaultValue: successMessageFallback,
|
||||
});
|
||||
const overwriteValues = useCallback(() => {
|
||||
if (withResetTo) {
|
||||
setValues(withResetTo, setValue);
|
||||
}
|
||||
}, [setValue, withResetTo]);
|
||||
|
||||
// See https://react-hook-form.com/api/useform/reset/
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
setShowSuccessNotification(true);
|
||||
}
|
||||
}, [isSubmitSuccessful]);
|
||||
|
||||
useEffect(() => reset(defaultValues as never), [defaultValues, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
setShowSuccessNotification(false);
|
||||
}
|
||||
}, [isDirty]);
|
||||
|
||||
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} formId={prefix}>
|
||||
<form onSubmit={handleSubmit(submit)} onReset={() => reset()} id={prefix} noValidate></form>
|
||||
{showSuccessNotification ? (
|
||||
<SuccessNotification label={successNotification} hide={() => setShowSuccessNotification(false)} />
|
||||
) : null}
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
{!readOnly ? (
|
||||
<Level
|
||||
right={
|
||||
<ButtonsContainer>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
testId={submitButtonTestId ?? "submit-button"}
|
||||
disabled={!isDirty || !isValid}
|
||||
isLoading={isSubmitting}
|
||||
form={prefix}
|
||||
>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
{withDiscardChanges ? (
|
||||
<Button type="reset" form={prefix} testId={`${prefix}-discard-changes-button`}>
|
||||
{discardChangesButtonLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
{withResetTo ? (
|
||||
<Button form={prefix} onClick={overwriteValues} testId={`${prefix}-reset-button`}>
|
||||
{resetButtonLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
</ButtonsContainer>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</ScmFormContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form;
|
||||
@@ -1,73 +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, useContext, useMemo } from "react";
|
||||
|
||||
const ScmFormPathContext = React.createContext<string>("");
|
||||
|
||||
export function useScmFormPathContext() {
|
||||
return useContext(ScmFormPathContext);
|
||||
}
|
||||
|
||||
export const ScmFormPathContextProvider: FC<{ path: string }> = ({ children, path }) => (
|
||||
<ScmFormPathContext.Provider value={path}>{children}</ScmFormPathContext.Provider>
|
||||
);
|
||||
|
||||
/**
|
||||
* This component removes redundancy by declaring a prefix that is shared by all enclosed form-related components.
|
||||
* It might be helpful when the data structure of a list's items does not correspond with the individual list item's
|
||||
* form structure.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
* @example ```
|
||||
* // For data of structure
|
||||
* {
|
||||
* subForm: { foo: boolean, bar: string };
|
||||
* flag: boolean;
|
||||
* tag: string;
|
||||
* }
|
||||
* // Because we use ConfigurationForm we get and update the whole object.
|
||||
* // We only want to create a form for 'subForm' without having to repeat it for every subField
|
||||
* // while keeping the original data structure for the whole form. *
|
||||
*
|
||||
* // Using this component we can write:
|
||||
* <Form.PathContext path="subForm">
|
||||
* <Form.Input name="foo" />
|
||||
* <Form.Checkbox name="bar" />
|
||||
* </Form.PathContext>
|
||||
*
|
||||
* // Instead of
|
||||
*
|
||||
* <Form.Input name="subForm.foo" />
|
||||
* <Form.Checkbox name="subForm.bar" />
|
||||
*
|
||||
* // This pattern becomes useful in complex or large forms.
|
||||
* ```
|
||||
*/
|
||||
export const ScmNestedFormPathContextProvider: FC<{ path: string }> = ({ children, path }) => {
|
||||
const prefix = useScmFormPathContext();
|
||||
const pathWithPrefix = useMemo(() => (prefix ? `${prefix}.${path}` : path), [path, prefix]);
|
||||
return <ScmFormPathContext.Provider value={pathWithPrefix}>{children}</ScmFormPathContext.Provider>;
|
||||
};
|
||||
@@ -1,37 +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, { 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;
|
||||
@@ -1,43 +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, { 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;
|
||||
formId: string;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,65 +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, useContext, useMemo } from "react";
|
||||
import { FieldValues, useFieldArray, UseFieldArrayReturn } from "react-hook-form";
|
||||
import { ScmFormPathContextProvider, useScmFormPathContext } from "./FormPathContext";
|
||||
import { useScmFormContext } from "./ScmFormContext";
|
||||
|
||||
type ContextType<T extends FieldValues = any> = UseFieldArrayReturn<T> & { isNested: boolean };
|
||||
|
||||
const ScmFormListContext = React.createContext<ContextType>(null as unknown as ContextType);
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up an array field to be displayed in an enclosed *Form.Table* or *Form.List*.
|
||||
* A *Form.AddListEntryForm* can be used to implement a sub-form for adding new entries to the list.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
*/
|
||||
export const ScmFormListContextProvider: FC<Props> = ({ name, children }) => {
|
||||
const { control } = useScmFormContext();
|
||||
const prefix = useScmFormPathContext();
|
||||
const parentForm = useScmFormListContext();
|
||||
const nameWithPrefix = useMemo(() => (prefix ? `${prefix}.${name}` : name), [name, prefix]);
|
||||
const fieldArray = useFieldArray({
|
||||
control,
|
||||
name: nameWithPrefix,
|
||||
});
|
||||
const value = useMemo(() => ({ ...fieldArray, isNested: !!parentForm }), [fieldArray, parentForm]);
|
||||
|
||||
return (
|
||||
<ScmFormPathContextProvider path={nameWithPrefix}>
|
||||
<ScmFormListContext.Provider value={value}>{children}</ScmFormListContext.Provider>
|
||||
</ScmFormPathContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useScmFormListContext() {
|
||||
return useContext(ScmFormListContext);
|
||||
}
|
||||
@@ -1,34 +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, { HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Control = React.forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(({ className, children, ...rest }, ref) => (
|
||||
<div className={classNames("control", className)} {...rest} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default Control;
|
||||
@@ -1,35 +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, HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Field: FC<HTMLProps<HTMLDivElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
|
||||
as = "div",
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => React.createElement(as, { className: classNames("field", className), ...rest }, children);
|
||||
|
||||
export default Field;
|
||||
@@ -1,34 +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, { 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;
|
||||
@@ -1,34 +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 from "react";
|
||||
import { Tooltip } from "@scm-manager/ui-overlays";
|
||||
|
||||
type Props = { text?: string; className?: string };
|
||||
const Help = ({ text, className }: Props) => (
|
||||
<Tooltip className={className} message={text}>
|
||||
<i className="fas fa-fw fa-question-circle has-text-blue-light" aria-hidden />
|
||||
</Tooltip>
|
||||
);
|
||||
export default Help;
|
||||
@@ -1,35 +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, HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Label: FC<HTMLProps<HTMLLabelElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
|
||||
as = "label",
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => React.createElement(as, { className: classNames("label", className), ...rest }, children);
|
||||
|
||||
export default Label;
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
@@ -1,118 +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, { InputHTMLAttributes } from "react";
|
||||
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||
import Help from "../base/help/Help";
|
||||
import styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const StyledInput = styled.input`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
margin-left: -0.75rem;
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
type InputFieldProps = {
|
||||
label: string;
|
||||
helpText?: string;
|
||||
testId?: string;
|
||||
labelClassName?: string;
|
||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, "type">;
|
||||
|
||||
/**
|
||||
* @see https://bulma.io/documentation/form/checkbox/
|
||||
*/
|
||||
const Checkbox = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||
(
|
||||
{
|
||||
readOnly,
|
||||
label,
|
||||
className,
|
||||
labelClassName,
|
||||
value,
|
||||
name,
|
||||
checked,
|
||||
defaultChecked,
|
||||
defaultValue,
|
||||
testId,
|
||||
helpText,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<StyledLabel
|
||||
className={classNames("checkbox is-align-items-center", labelClassName)}
|
||||
// @ts-ignore bulma uses the disabled attribute on labels, although it is not part of the html spec
|
||||
disabled={readOnly || props.disabled}
|
||||
>
|
||||
{readOnly ? (
|
||||
<>
|
||||
<input
|
||||
type="hidden"
|
||||
name={name}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
readOnly
|
||||
/>
|
||||
<StyledInput
|
||||
type="checkbox"
|
||||
className={classNames("m-3", className)}
|
||||
ref={ref}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
{...props}
|
||||
{...createAttributesForTesting(testId)}
|
||||
disabled
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<StyledInput
|
||||
type="checkbox"
|
||||
className={classNames("m-3", className)}
|
||||
ref={ref}
|
||||
name={name}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
{...props}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</StyledLabel>
|
||||
)
|
||||
);
|
||||
export default Checkbox;
|
||||
@@ -1,39 +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 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;
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
@@ -1,82 +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, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path, RegisterOptions } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import CheckboxField from "./CheckboxField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
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,
|
||||
testId,
|
||||
defaultChecked,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultChecked as never}
|
||||
render={({ field }) => (
|
||||
<CheckboxField
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
checked={field.value}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
testId={testId ?? `checkbox-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledInputField;
|
||||
@@ -1,75 +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 { storiesOf } from "@storybook/react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import ChipInputField from "./ChipInputField";
|
||||
import Combobox from "../combobox/Combobox";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
import ChipInput from "../headless-chip-input/ChipInput";
|
||||
|
||||
storiesOf("Chip Input Field", module)
|
||||
.add("Default", () => {
|
||||
const [value, setValue] = useState<Option<string>[]>([]);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<ChipInputField
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Test Chips"
|
||||
placeholder="Type a new chip name and press enter to add"
|
||||
aria-label="My personal chip list"
|
||||
ref={ref}
|
||||
/>
|
||||
<ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
|
||||
</>
|
||||
);
|
||||
})
|
||||
.add("With Autocomplete", () => {
|
||||
const people = ["Durward Reynolds", "Kenton Towne", "Therese Wunsch", "Benedict Kessler", "Katelyn Rohan"];
|
||||
|
||||
const [value, setValue] = useState<Option<string>[]>([]);
|
||||
|
||||
return (
|
||||
<ChipInputField
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Persons"
|
||||
placeholder="Enter a new person"
|
||||
aria-label="Enter a new person"
|
||||
>
|
||||
<Combobox
|
||||
options={(query: string) =>
|
||||
Promise.resolve(
|
||||
people
|
||||
.map<Option<string>>((p) => ({ label: p, value: p }))
|
||||
.filter((t) => !value.some((val) => val.label === t.label) && t.label.startsWith(query))
|
||||
.concat({ label: query, value: query, displayValue: `Use '${query}'` })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ChipInputField>
|
||||
);
|
||||
});
|
||||
@@ -1,169 +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, { KeyboardEventHandler, PropsWithRef, ReactElement, Ref, RefObject, useCallback } from "react";
|
||||
import { createAttributesForTesting, useGeneratedId } from "@scm-manager/ui-components";
|
||||
import Field from "../base/Field";
|
||||
import Label from "../base/label/Label";
|
||||
import Help from "../base/help/Help";
|
||||
import FieldMessage from "../base/field-message/FieldMessage";
|
||||
import styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
import { createVariantClass } from "../variants";
|
||||
import ChipInput, { NewChipInput } from "../headless-chip-input/ChipInput";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withForwardRef } from "../helpers";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
|
||||
const StyledChipInput: typeof ChipInput = styled(ChipInput)`
|
||||
min-height: 40px;
|
||||
height: min-content;
|
||||
gap: 0.5rem;
|
||||
&:focus-within {
|
||||
border: 1px solid var(--scm-info-color);
|
||||
box-shadow: rgba(51, 178, 232, 0.25) 0px 0px 0px 0.125em;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInput = styled(NewChipInput)`
|
||||
color: var(--scm-secondary-more-color);
|
||||
font-size: 1rem;
|
||||
height: initial;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
` as unknown as typeof NewChipInput;
|
||||
|
||||
const StyledDelete = styled(ChipInput.Chip.Delete)`
|
||||
&:focus {
|
||||
outline-offset: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type InputFieldProps<T> = {
|
||||
label: string;
|
||||
createDeleteText?: (value: string) => string;
|
||||
helpText?: string;
|
||||
error?: string;
|
||||
testId?: string;
|
||||
id?: string;
|
||||
children?: ReactElement;
|
||||
placeholder?: string;
|
||||
onChange?: (newValue: Option<T>[]) => void;
|
||||
value?: Option<T>[] | null;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
isNewItemDuplicate?: (existingItem: Option<T>, newItem: Option<T>) => boolean;
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const ChipInputField = function ChipInputField<T>(
|
||||
{
|
||||
label,
|
||||
helpText,
|
||||
readOnly,
|
||||
disabled,
|
||||
error,
|
||||
createDeleteText,
|
||||
onChange,
|
||||
placeholder,
|
||||
value,
|
||||
className,
|
||||
testId,
|
||||
id,
|
||||
children,
|
||||
isLoading,
|
||||
isNewItemDuplicate,
|
||||
...props
|
||||
}: PropsWithRef<InputFieldProps<T>>,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const [t] = useTranslation("commons", { keyPrefix: "form.chipList" });
|
||||
const deleteTextCallback = useCallback(
|
||||
(item) => (createDeleteText ? createDeleteText(item) : t("delete", { item })),
|
||||
[createDeleteText, t]
|
||||
);
|
||||
const inputId = useGeneratedId(id ?? testId);
|
||||
const labelId = useGeneratedId();
|
||||
const inputDescriptionId = useGeneratedId();
|
||||
const variant = error ? "danger" : undefined;
|
||||
return (
|
||||
<Field className={className} aria-owns={inputId}>
|
||||
<Label id={labelId}>
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<div className={classNames("control", { "is-loading": isLoading })}>
|
||||
<StyledChipInput
|
||||
value={value}
|
||||
onChange={(e) => onChange && onChange(e ?? [])}
|
||||
className="is-flex is-flex-wrap-wrap input"
|
||||
aria-labelledby={labelId}
|
||||
disabled={readOnly || disabled}
|
||||
isNewItemDuplicate={isNewItemDuplicate}
|
||||
>
|
||||
{value?.map((option, index) => (
|
||||
<ChipInput.Chip key={option.label} className="tag is-light is-overflow-hidden">
|
||||
<span className="is-ellipsis-overflow">{option.label}</span>
|
||||
<StyledDelete aria-label={deleteTextCallback(option.label)} index={index} className="delete is-small" />
|
||||
</ChipInput.Chip>
|
||||
))}
|
||||
<StyledInput
|
||||
{...props}
|
||||
className={classNames(
|
||||
"is-borderless",
|
||||
"has-background-transparent",
|
||||
"is-shadowless",
|
||||
"input",
|
||||
"is-ellipsis-overflow",
|
||||
createVariantClass(variant)
|
||||
)}
|
||||
placeholder={!readOnly && !disabled ? placeholder : ""}
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
aria-describedby={inputDescriptionId}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
{children ? children : null}
|
||||
</StyledInput>
|
||||
</StyledChipInput>
|
||||
</div>
|
||||
<VisuallyHidden aria-hidden id={inputDescriptionId}>
|
||||
{t("input.description")}
|
||||
</VisuallyHidden>
|
||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
export default withForwardRef(ChipInputField);
|
||||
@@ -1,111 +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, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { defaultOptionFactory, prefixWithoutIndices, withForwardRef } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import ChipInputField from "./ChipInputField";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
Parameters<typeof ChipInputField>[0],
|
||||
"error" | "createDeleteText" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
|
||||
> & {
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
name: Path<T>;
|
||||
label?: string;
|
||||
defaultValue?: string[];
|
||||
createDeleteText?: (value: string) => string;
|
||||
optionFactory?: (val: any) => Option<unknown>;
|
||||
ref?: React.ForwardedRef<HTMLInputElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
function ControlledChipInputField<T extends Record<string, unknown>>(
|
||||
{
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
placeholder,
|
||||
className,
|
||||
createDeleteText,
|
||||
children,
|
||||
optionFactory = defaultOptionFactory,
|
||||
...props
|
||||
}: Props<T>,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const placeholderTranslation = placeholder || t(`${prefixedNameWithoutIndices}.placeholder`) || "";
|
||||
const ariaLabelTranslation = t(`${prefixedNameWithoutIndices}.ariaLabel`);
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { value, onChange, ...field }, fieldState }) => (
|
||||
<ChipInputField
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
placeholder={placeholderTranslation}
|
||||
aria-label={ariaLabelTranslation}
|
||||
value={value ? value.map(optionFactory) : []}
|
||||
onChange={(selectedOptions) => onChange(selectedOptions.map((item) => item.value))}
|
||||
{...props}
|
||||
{...field}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
className={classNames("column", className)}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</ChipInputField>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withForwardRef(ControlledChipInputField);
|
||||
@@ -1,125 +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 { storiesOf } from "@storybook/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import Combobox from "./Combobox";
|
||||
import { Combobox as HeadlessCombobox } from "@headlessui/react";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
import { Link, BrowserRouter } from "react-router-dom";
|
||||
|
||||
const waitFor = (ms: number) =>
|
||||
function <T>(result: T) {
|
||||
return new Promise<T>((resolve) => setTimeout(() => resolve(result), ms));
|
||||
};
|
||||
|
||||
const data = [
|
||||
{ label: "Trillian", value: "1" },
|
||||
{ label: "Arthur", value: "2" },
|
||||
{ label: "Zaphod", value: "3" },
|
||||
];
|
||||
|
||||
const linkData = [{ label: "Link111111111111111111111111111111111111", value: "1" }];
|
||||
|
||||
storiesOf("Combobox", module)
|
||||
.add("Options array", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
return <Combobox options={data} value={value} onChange={setValue} />;
|
||||
})
|
||||
.add("Options function", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
return <Combobox options={() => data} value={value} onChange={setValue} />;
|
||||
})
|
||||
.add("Options function as promise", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
return (
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
options={(query) => Promise.resolve(data.filter((t) => t.label.startsWith(query))).then(waitFor(1000))}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add("Children as Element", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue} onQueryChange={setQuery}>
|
||||
{query ? (
|
||||
<HeadlessCombobox.Option value={{ label: query, value: query }} key={query} as={Fragment}>
|
||||
{({ active }) => <Combobox.Option isActive={active}>{`Create ${query}`}</Combobox.Option>}
|
||||
</HeadlessCombobox.Option>
|
||||
) : null}
|
||||
<HeadlessCombobox.Option value={{ label: "All", value: "All" }} key="all" as={Fragment}>
|
||||
{({ active }) => <Combobox.Option isActive={active}>All</Combobox.Option>}
|
||||
</HeadlessCombobox.Option>
|
||||
<>
|
||||
{data.map((o) => (
|
||||
<HeadlessCombobox.Option value={o} key={o.value} as={Fragment}>
|
||||
{({ active }) => <Combobox.Option isActive={active}>{o.label}</Combobox.Option>}
|
||||
</HeadlessCombobox.Option>
|
||||
))}
|
||||
</>
|
||||
</Combobox>
|
||||
);
|
||||
})
|
||||
.add("Children as render props", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
return (
|
||||
<Combobox options={data} value={value} onChange={setValue}>
|
||||
{(o) => (
|
||||
<HeadlessCombobox.Option value={o} key={o.value} as={Fragment}>
|
||||
{({ active }) => <Combobox.Option isActive={active}>{o.label}</Combobox.Option>}
|
||||
</HeadlessCombobox.Option>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
})
|
||||
.add("Links as render props", () => {
|
||||
const [value, setValue] = useState<Option<string>>();
|
||||
const [query, setQuery] = useState("Hello");
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Combobox
|
||||
className="input is-small omni-search-bar"
|
||||
placeholder={"Placeholder"}
|
||||
value={value}
|
||||
options={linkData}
|
||||
onChange={setValue}
|
||||
onQueryChange={setQuery}
|
||||
>
|
||||
{(o) => (
|
||||
<HeadlessCombobox.Option value={{ label: o.label, value: query, displayValue: o.value }} key={o.value} as={Fragment}>
|
||||
{({ active }) => (
|
||||
<Combobox.Option isActive={active}>
|
||||
<Link to={o.label}>{o.label}</Link>
|
||||
</Combobox.Option>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
)}
|
||||
</Combobox>
|
||||
</BrowserRouter>
|
||||
);
|
||||
});
|
||||
@@ -1,223 +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, {
|
||||
ForwardedRef,
|
||||
Fragment,
|
||||
KeyboardEventHandler,
|
||||
ReactElement,
|
||||
Ref,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Combobox as HeadlessCombobox } from "@headlessui/react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { withForwardRef } from "../helpers";
|
||||
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
|
||||
const OptionsWrapper = styled(HeadlessCombobox.Options).attrs({
|
||||
className: "is-flex is-flex-direction-column has-rounded-border has-box-shadow is-overflow-hidden",
|
||||
})`
|
||||
z-index: 400;
|
||||
top: 2.5rem;
|
||||
border: var(--scm-border);
|
||||
background-color: var(--scm-secondary-background);
|
||||
max-width: 35ch;
|
||||
width: 35ch;
|
||||
|
||||
&:empty {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledComboboxOption = styled.li.attrs({
|
||||
className: "px-3 py-2 has-text-inherit is-clickable is-size-6 is-borderless has-background-transparent",
|
||||
})<{ isActive: boolean }>`
|
||||
line-height: inherit;
|
||||
background-color: ${({ isActive }) => (isActive ? "var(--scm-column-selection)" : "")};
|
||||
word-break: break-all;
|
||||
&[data-disabled] {
|
||||
color: unset !important;
|
||||
opacity: 40%;
|
||||
cursor: unset !important;
|
||||
}
|
||||
> a {
|
||||
color: inherit !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type BaseProps<T> = {
|
||||
className?: string;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLElement>;
|
||||
value?: Option<T> | null;
|
||||
onChange?: (value?: Option<T>) => void;
|
||||
onBlur?: () => void;
|
||||
disabled?: boolean;
|
||||
defaultValue?: Option<T>;
|
||||
nullable?: boolean;
|
||||
readOnly?: boolean;
|
||||
onQueryChange?: (value: string) => void;
|
||||
id?: string;
|
||||
placeholder?: string;
|
||||
"aria-describedby"?: string;
|
||||
"aria-labelledby"?: string;
|
||||
"aria-label"?: string;
|
||||
testId?: string;
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
form?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type ComboboxProps<T> =
|
||||
| (BaseProps<T> & {
|
||||
options: Array<Option<T>> | ((query: string) => Array<Option<T>> | Promise<Array<Option<T>>>);
|
||||
children?: (option: Option<T>, index: number) => ReactElement;
|
||||
})
|
||||
| (BaseProps<T> & { children: Array<ReactElement | null>; options?: never });
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.45.0
|
||||
*/
|
||||
function ComboboxComponent<T>(props: ComboboxProps<T>, ref: ForwardedRef<HTMLInputElement>) {
|
||||
const [query, setQuery] = useState("");
|
||||
const { onQueryChange } = props;
|
||||
|
||||
useEffect(() => onQueryChange && onQueryChange(query), [onQueryChange, query]);
|
||||
|
||||
let options;
|
||||
|
||||
if (Array.isArray(props.children)) {
|
||||
options = props.children;
|
||||
} else if (typeof props.options === "function") {
|
||||
options = <ComboboxOptions<T> children={props.children} options={props.options} query={query} />;
|
||||
} else {
|
||||
options = props.options?.map((o, index) =>
|
||||
typeof props.children === "function" ? (
|
||||
props.children(o, index)
|
||||
) : (
|
||||
<HeadlessCombobox.Option value={o} key={o.label} as={Fragment}>
|
||||
{({ active }) => <StyledComboboxOption isActive={active}>{o.displayValue ?? o.label}</StyledComboboxOption>}
|
||||
</HeadlessCombobox.Option>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
as="div"
|
||||
value={props.value}
|
||||
onChange={(e?: Option<T>) => props.onChange && props.onChange(e)}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
name={props.name}
|
||||
form={props.form}
|
||||
defaultValue={props.defaultValue}
|
||||
// @ts-ignore
|
||||
nullable={props.nullable}
|
||||
className="is-relative is-flex-grow-1 is-flex"
|
||||
by="value"
|
||||
>
|
||||
<HeadlessCombobox.Input<Option<T>>
|
||||
displayValue={(o) => o?.label}
|
||||
ref={ref}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className={classNames(props.className, "is-full-width", "input", "is-ellipsis-overflow")}
|
||||
aria-describedby={props["aria-describedby"]}
|
||||
aria-labelledby={props["aria-labelledby"]}
|
||||
aria-label={props["aria-label"]}
|
||||
id={props.id}
|
||||
placeholder={props.placeholder}
|
||||
onBlur={props.onBlur}
|
||||
autoComplete="off"
|
||||
onKeyDown={(e) => {
|
||||
props.onKeyDown && props.onKeyDown(e);
|
||||
}}
|
||||
{...createAttributesForTesting(props.testId)}
|
||||
/>
|
||||
<OptionsWrapper className="is-absolute">{options}</OptionsWrapper>
|
||||
</HeadlessCombobox>
|
||||
);
|
||||
}
|
||||
|
||||
type ComboboxOptionsProps<T> = {
|
||||
options: (query: string) => Array<Option<T>> | Promise<Array<Option<T>>>;
|
||||
children?: (option: Option<T>, index: number) => ReactElement;
|
||||
query: string;
|
||||
};
|
||||
|
||||
function ComboboxOptions<T>({ query, options, children }: ComboboxOptionsProps<T>) {
|
||||
const [optionsData, setOptionsData] = useState<Array<Option<T>>>([]);
|
||||
const activePromise = useRef<Promise<Array<Option<T>>>>();
|
||||
|
||||
useEffect(() => {
|
||||
const optionsExec = options(query);
|
||||
if (optionsExec instanceof Promise) {
|
||||
activePromise.current = optionsExec;
|
||||
optionsExec
|
||||
.then((newOptions) => {
|
||||
if (activePromise.current === optionsExec) {
|
||||
setOptionsData(newOptions);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (activePromise.current === optionsExec) {
|
||||
setOptionsData([]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setOptionsData(optionsExec);
|
||||
}
|
||||
}, [options, query]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{optionsData?.map((o, index) =>
|
||||
typeof children === "function" ? (
|
||||
children(o, index)
|
||||
) : (
|
||||
<HeadlessCombobox.Option value={o} key={o.label} as={Fragment}>
|
||||
{({ active }) => <StyledComboboxOption isActive={active}>{o.displayValue ?? o.label}</StyledComboboxOption>}
|
||||
</HeadlessCombobox.Option>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Combobox = Object.assign(withForwardRef(ComboboxComponent), {
|
||||
Option: StyledComboboxOption,
|
||||
});
|
||||
|
||||
export default Combobox;
|
||||
@@ -1,62 +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 Field from "../base/Field";
|
||||
import Label from "../base/label/Label";
|
||||
import Help from "../base/help/Help";
|
||||
import React from "react";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
import { withForwardRef } from "../helpers";
|
||||
import Combobox, { ComboboxProps } from "./Combobox";
|
||||
import classNames from "classnames";
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.45.0
|
||||
*/
|
||||
const ComboboxField = function ComboboxField<T>(
|
||||
{
|
||||
label,
|
||||
helpText,
|
||||
error,
|
||||
className,
|
||||
isLoading,
|
||||
...props
|
||||
}: ComboboxProps<T> & { label: string; helpText?: string; error?: string; isLoading?: boolean },
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const labelId = useGeneratedId();
|
||||
return (
|
||||
<Field className={className}>
|
||||
<Label id={labelId}>
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<div className={classNames("control", { "is-loading": isLoading })}>
|
||||
<Combobox {...props} ref={ref} aria-labelledby={labelId} />
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
export default withForwardRef(ComboboxField);
|
||||
@@ -1,96 +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, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { defaultOptionFactory, prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import ComboboxField from "./ComboboxField";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
Parameters<typeof ComboboxField>[0],
|
||||
"error" | "label" | keyof ControllerRenderProps
|
||||
> & {
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
name: Path<T>;
|
||||
label?: string;
|
||||
optionFactory?: (val: any) => Option<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.45.0
|
||||
*/
|
||||
function ControlledComboboxField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
className,
|
||||
optionFactory = defaultOptionFactory,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field: { onChange, value, ...field }, fieldState }) => (
|
||||
// @ts-ignore
|
||||
<ComboboxField
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
onChange={(e) => onChange(e?.value)}
|
||||
value={optionFactory(value)}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `select-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledComboboxField;
|
||||
@@ -1,237 +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, {
|
||||
ButtonHTMLAttributes,
|
||||
ComponentProps,
|
||||
ComponentType,
|
||||
Context,
|
||||
createContext,
|
||||
HTMLAttributes,
|
||||
KeyboardEventHandler,
|
||||
LiHTMLAttributes,
|
||||
ReactElement,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
import { mergeRefs, withForwardRef } from "../helpers";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
|
||||
type ChipInputContextType<T> = {
|
||||
add(newValue: Option<T>): void;
|
||||
remove(index: number): void;
|
||||
inputRef: RefObject<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
const ChipInputContext = createContext<ChipInputContextType<never>>(null as unknown as ChipInputContextType<never>);
|
||||
|
||||
function getChipInputContext<T>() {
|
||||
return ChipInputContext as unknown as Context<ChipInputContextType<T>>;
|
||||
}
|
||||
|
||||
type CustomNewChipInputProps<T> = {
|
||||
onChange: (newValue: Option<T>) => void;
|
||||
value?: Option<T> | null;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
ref?: React.Ref<HTMLInputElement>;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
"aria-describedby"?: string;
|
||||
};
|
||||
|
||||
const DefaultNewChipInput = React.forwardRef<HTMLInputElement, CustomNewChipInputProps<string>>(
|
||||
({ onChange, value, ...props }, ref) => {
|
||||
const handleKeyDown = useCallback<KeyboardEventHandler<HTMLInputElement>>(
|
||||
(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (e.currentTarget.value) {
|
||||
onChange({ label: e.currentTarget.value, value: e.currentTarget.value });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="is-flex-grow-1">
|
||||
<input onKeyDown={handleKeyDown} {...props} ref={ref} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type ChipDeleteProps = {
|
||||
asChild?: boolean;
|
||||
index: number;
|
||||
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick">;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const ChipDelete = React.forwardRef<HTMLButtonElement, ChipDeleteProps>(({ asChild, index, ...props }, ref) => {
|
||||
const { remove, disabled } = useContext(ChipInputContext);
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Comp {...props} ref={ref} type="button" onClick={() => remove(index)} />;
|
||||
});
|
||||
|
||||
type NewChipInputProps = {
|
||||
children?: ReactElement | null;
|
||||
ref?: React.Ref<HTMLInputElement>;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
"aria-describedby"?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const NewChipInput = withForwardRef(function NewChipInput<T>(
|
||||
props: NewChipInputProps,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const { add, disabled, readOnly, inputRef } = useContext(getChipInputContext<T>());
|
||||
|
||||
const Comp = props.children ? Slot : DefaultNewChipInput;
|
||||
return React.createElement(Comp as unknown as ComponentType<CustomNewChipInputProps<T>>, {
|
||||
...props,
|
||||
onChange: add,
|
||||
readOnly,
|
||||
disabled,
|
||||
value: null,
|
||||
ref: mergeRefs(ref, inputRef),
|
||||
});
|
||||
});
|
||||
|
||||
type ChipProps = { asChild?: boolean } & LiHTMLAttributes<HTMLLIElement>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const Chip = React.forwardRef<HTMLLIElement, ChipProps>(({ asChild, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "li";
|
||||
|
||||
return <Comp {...props} ref={ref} />;
|
||||
});
|
||||
|
||||
type Props<T> = {
|
||||
value?: Option<T>[] | null;
|
||||
onChange?: (newValue?: Option<T>[]) => void;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
isNewItemDuplicate?: (existingItem: Option<T>, newItem: Option<T>) => boolean;
|
||||
} & Omit<HTMLAttributes<HTMLUListElement>, "onChange">;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const ChipInput = withForwardRef(function ChipInput<T>(
|
||||
{ children, value = [], disabled, readOnly, onChange, isNewItemDuplicate, ...props }: Props<T>,
|
||||
ref: React.ForwardedRef<HTMLUListElement>
|
||||
) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const isInactive = useMemo(() => disabled || readOnly, [disabled, readOnly]);
|
||||
const add = useCallback<(newValue: Option<T>) => void>(
|
||||
(newItem) => {
|
||||
if (
|
||||
!isInactive &&
|
||||
!value?.some((item) =>
|
||||
isNewItemDuplicate
|
||||
? isNewItemDuplicate(item, newItem)
|
||||
: item.label === newItem.label || item.value === newItem.value
|
||||
)
|
||||
) {
|
||||
if (onChange) {
|
||||
onChange([...(value ?? []), newItem]);
|
||||
}
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
[isInactive, isNewItemDuplicate, onChange, value]
|
||||
);
|
||||
const remove = useCallback(
|
||||
(index: number) => !isInactive && onChange && onChange(value?.filter((_, itdx) => itdx !== index)),
|
||||
[isInactive, onChange, value]
|
||||
);
|
||||
const Context = getChipInputContext<T>();
|
||||
return (
|
||||
<Context.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
disabled,
|
||||
readOnly,
|
||||
add,
|
||||
remove,
|
||||
inputRef,
|
||||
}),
|
||||
[add, disabled, readOnly, remove]
|
||||
)}
|
||||
>
|
||||
<ul {...props} ref={ref}>
|
||||
{children}
|
||||
</ul>
|
||||
</Context.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const AddButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
Omit<ComponentProps<typeof Button>, "onClick"> & { inputRef: RefObject<HTMLInputElement | null> }
|
||||
>(({ inputRef, children, ...props }, ref) => (
|
||||
<Button
|
||||
{...props}
|
||||
onClick={() => inputRef.current?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }))}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
));
|
||||
|
||||
export default Object.assign(ChipInput, {
|
||||
Chip: Object.assign(Chip, {
|
||||
Delete: ChipDelete,
|
||||
}),
|
||||
AddButton,
|
||||
});
|
||||
@@ -1,74 +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 { UseFormReturn } from "react-hook-form";
|
||||
import { ForwardedRef, forwardRef, MutableRefObject, Ref, RefCallback } from "react";
|
||||
|
||||
export function prefixWithoutIndices(path: string): string {
|
||||
return path.replace(/(\.\d+)/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like {@link setValue} but recursively applies the whole {@link newValues} object.
|
||||
*
|
||||
* > *Important Note:* This deeply overwrites input values of fields in arrays,
|
||||
* > but does **NOT** add or remove items to existing arrays.
|
||||
* > This can therefore not be used to clear lists.
|
||||
*/
|
||||
export function setValues<T>(newValues: T, setValue: UseFormReturn<T>["setValue"], path = "") {
|
||||
for (const [key, val] of Object.entries(newValues)) {
|
||||
if (val !== null && typeof val === "object") {
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((subVal, idx) => setValues(subVal, setValue, path ? `${path}.${key}.${idx}` : `${key}.${idx}`));
|
||||
} else {
|
||||
setValues(val, setValue, path ? `${path}.${key}` : key);
|
||||
}
|
||||
} else {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
setValue(fullPath as any, val, { shouldValidate: !fullPath.endsWith("Confirmation"), shouldDirty: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function withForwardRef<T extends { name: string }>(component: T): T {
|
||||
return forwardRef(component as unknown as any) as any;
|
||||
}
|
||||
|
||||
export const defaultOptionFactory = (item: any) =>
|
||||
typeof item === "object" && item !== null && "value" in item && typeof item["label"] === "string"
|
||||
? item
|
||||
: { label: item as string, value: item };
|
||||
|
||||
export function mergeRefs<T>(...refs: Array<RefCallback<T> | MutableRefObject<T> | ForwardedRef<T>>) {
|
||||
return (el: T) =>
|
||||
refs.forEach((ref) => {
|
||||
if (ref) {
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else {
|
||||
ref.current = el;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,85 +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 FormCmp from "./Form";
|
||||
import FormRow from "./FormRow";
|
||||
import ControlledInputField from "./input/ControlledInputField";
|
||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||
import ControlledSelectField from "./select/ControlledSelectField";
|
||||
import ControlledChipInputField from "./chip-input/ControlledChipInputField";
|
||||
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
||||
import ControlledList from "./list/ControlledList";
|
||||
import ControlledTable from "./table/ControlledTable";
|
||||
import ControlledColumn from "./table/ControlledColumn";
|
||||
import AddListEntryForm from "./AddListEntryForm";
|
||||
import { ScmNestedFormPathContextProvider } from "./FormPathContext";
|
||||
import ControlledComboboxField from "./combobox/ControlledComboboxField";
|
||||
import ChipInputFieldComponent from "./chip-input/ChipInputField";
|
||||
import ChipInput from "./headless-chip-input/ChipInput";
|
||||
import ControlledRadioGroupField from "./radio-button/ControlledRadioGroupField";
|
||||
import RadioGroupComponent from "./radio-button/RadioGroup";
|
||||
import RadioButton from "./radio-button/RadioButton";
|
||||
import RadioGroupFieldComponent from "./radio-button/RadioGroupField";
|
||||
|
||||
export { default as Field } from "./base/Field";
|
||||
export { default as Checkbox } from "./checkbox/Checkbox";
|
||||
export { default as Combobox } from "./combobox/Combobox";
|
||||
export { default as ConfigurationForm } from "./ConfigurationForm";
|
||||
export { default as SelectField } from "./select/SelectField";
|
||||
export { default as ComboboxField } from "./combobox/ComboboxField";
|
||||
export { default as Input } from "./input/Input";
|
||||
export { default as Textarea } from "./input/Textarea";
|
||||
export { default as Select } from "./select/Select";
|
||||
export * from "./resourceHooks";
|
||||
export { default as Label } from "./base/label/Label";
|
||||
|
||||
const RadioGroupExport = {
|
||||
Option: RadioButton,
|
||||
};
|
||||
|
||||
export const RadioGroup = Object.assign(RadioGroupComponent, RadioGroupExport);
|
||||
export const RadioGroupField = Object.assign(RadioGroupFieldComponent, RadioGroupExport);
|
||||
|
||||
export const ChipInputField = Object.assign(ChipInputFieldComponent, {
|
||||
AddButton: ChipInput.AddButton,
|
||||
});
|
||||
|
||||
export const Form = Object.assign(FormCmp, {
|
||||
Row: FormRow,
|
||||
Input: ControlledInputField,
|
||||
Checkbox: ControlledCheckboxField,
|
||||
SecretConfirmation: ControlledSecretConfirmationField,
|
||||
Select: ControlledSelectField,
|
||||
PathContext: ScmNestedFormPathContextProvider,
|
||||
ListContext: ScmFormListContextProvider,
|
||||
List: ControlledList,
|
||||
AddListEntryForm: AddListEntryForm,
|
||||
Table: Object.assign(ControlledTable, {
|
||||
Column: ControlledColumn,
|
||||
}),
|
||||
ChipInput: ControlledChipInputField,
|
||||
Combobox: ControlledComboboxField,
|
||||
RadioGroup: Object.assign(ControlledRadioGroupField, RadioGroupExport),
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
@@ -1,87 +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, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
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,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
form={formId}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledInputField;
|
||||
@@ -1,39 +0,0 @@
|
||||
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>
|
||||
@@ -1,138 +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, { ComponentProps, useCallback, useMemo } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
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, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
const confirmationLabelTranslation = confirmationLabel || t(`${prefixedNameWithoutIndices}.confirmation.label`) || "";
|
||||
const confirmationHelpTextTranslation =
|
||||
confirmationHelpText || t(`${prefixedNameWithoutIndices}.confirmation.helpText`);
|
||||
const confirmationErrorMessageTranslation =
|
||||
confirmationErrorMessage || t(`${prefixedNameWithoutIndices}.confirmation.errorMessage`);
|
||||
const secretValue = watch(nameWithPrefix);
|
||||
const validateConfirmField = useCallback(
|
||||
(value) => secretValue === value || confirmationErrorMessageTranslation,
|
||||
[confirmationErrorMessageTranslation, secretValue]
|
||||
);
|
||||
const confirmFieldRules = useMemo(
|
||||
() => ({
|
||||
validate: validateConfirmField,
|
||||
}),
|
||||
[validateConfirmField]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
defaultValue={defaultValue as never}
|
||||
rules={{
|
||||
...rules,
|
||||
deps: [`${nameWithPrefix}Confirmation`],
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
className={classNames("column", className)}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
{...props}
|
||||
{...field}
|
||||
form={formId}
|
||||
required={rules?.required as boolean}
|
||||
type="password"
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`${nameWithPrefix}Confirmation`}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<InputField
|
||||
className={classNames("column", className)}
|
||||
type="password"
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
disabled={props.disabled}
|
||||
{...field}
|
||||
form={formId}
|
||||
label={confirmationLabelTranslation}
|
||||
helpText={confirmationHelpTextTranslation}
|
||||
error={
|
||||
error ? error.message || t(`${prefixedNameWithoutIndices}.confirmation.error.${error.type}`) : undefined
|
||||
}
|
||||
testId={confirmationTestId ?? `input-${nameWithPrefix}-confirmation`}
|
||||
/>
|
||||
)}
|
||||
rules={confirmFieldRules}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
@@ -1,46 +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, { 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;
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
@@ -1,61 +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 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";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
|
||||
type InputFieldProps = {
|
||||
label: string;
|
||||
helpText?: string;
|
||||
error?: string;
|
||||
} & React.ComponentProps<typeof Input>;
|
||||
|
||||
/**
|
||||
* @see https://bulma.io/documentation/form/input/
|
||||
*/
|
||||
const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||
({ label, helpText, error, className, id, ...props }, ref) => {
|
||||
const inputId = useGeneratedId(id ?? props.testId);
|
||||
const variant = error ? "danger" : undefined;
|
||||
return (
|
||||
<Field className={className}>
|
||||
<Label htmlFor={inputId}>
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<Control>
|
||||
<Input variant={variant} ref={ref} id={inputId} {...props}></Input>
|
||||
</Control>
|
||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default InputField;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
import Textarea from "./Textarea"
|
||||
|
||||
<Meta title="Textarea" />
|
||||
|
||||
This will be our latest Textarea component
|
||||
|
||||
<Story name="Default">
|
||||
<Textarea />
|
||||
</Story>
|
||||
|
||||
<Story name="With Variant">
|
||||
<Textarea variant="danger" />
|
||||
</Story>
|
||||
|
||||
<Story name="With Custom Class">
|
||||
<Textarea className="is-warning" />
|
||||
</Story>
|
||||
|
||||
<Story name="With Ref">
|
||||
<Textarea ref={r => {r.focus()}} />
|
||||
</Story>
|
||||
|
||||
<Story name="With Default Value">
|
||||
<Textarea>
|
||||
value
|
||||
</Textarea>
|
||||
</Story>
|
||||
@@ -1,46 +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, { 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<HTMLTextAreaElement>;
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, Props>(({ variant, className, testId, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={classNames("textarea", createVariantClass(variant), className)}
|
||||
{...props}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Textarea;
|
||||
@@ -1,88 +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 from "react";
|
||||
import { Path, PathValue } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { ScmFormPathContextProvider, useScmFormPathContext } from "../FormPathContext";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import { useScmFormListContext } from "../ScmFormListContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ArrayItemType<T> = T extends Array<infer U> ? U : unknown;
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>, PATH extends Path<T>> = {
|
||||
value: ArrayItemType<PathValue<T, PATH>>;
|
||||
index: number;
|
||||
remove: () => void;
|
||||
};
|
||||
|
||||
type Props<T extends Record<string, unknown>, PATH extends Path<T>> = {
|
||||
withDelete?: boolean;
|
||||
children: ((renderProps: RenderProps<T, PATH>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
*/
|
||||
function ControlledList<T extends Record<string, unknown>, PATH extends Path<T>>({
|
||||
withDelete,
|
||||
children,
|
||||
}: Props<T, PATH>) {
|
||||
const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form" });
|
||||
const { readOnly, t } = useScmFormContext();
|
||||
const nameWithPrefix = useScmFormPathContext();
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const { fields, remove } = useScmFormListContext();
|
||||
|
||||
const deleteButtonTranslation = t(`${prefixedNameWithoutIndices}.delete`, {
|
||||
defaultValue: defaultTranslate("list.delete.label", {
|
||||
entity: t(`${prefixedNameWithoutIndices}.entity`),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((value, index) => (
|
||||
<ScmFormPathContextProvider key={value.id} path={`${nameWithPrefix}.${index}`}>
|
||||
{typeof children === "function"
|
||||
? children({ value: value as never, index, remove: () => remove(index) })
|
||||
: children}
|
||||
{withDelete && !readOnly ? (
|
||||
<div className="level-right">
|
||||
<Button variant="signal" onClick={() => remove(index)}>
|
||||
{deleteButtonTranslation}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<hr />
|
||||
</ScmFormPathContextProvider>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledList;
|
||||
@@ -1,94 +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, { ComponentProps } from "react";
|
||||
import RadioGroupField from "./RadioGroupField";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import { RadioButtonContextProvider } from "./RadioButtonContext";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof RadioGroupField>,
|
||||
"label" | keyof ControllerRenderProps
|
||||
> & {
|
||||
name: Path<T>;
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
label?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.48.0
|
||||
*/
|
||||
function ControlledRadioGroupField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
defaultValue,
|
||||
children,
|
||||
fieldClassName,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadOnly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label ?? t(`${prefixedNameWithoutIndices}.label`) ?? "";
|
||||
const helpTextTranslation = helpText ?? t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => (
|
||||
<RadioButtonContextProvider t={t} prefix={prefixedNameWithoutIndices} formId={formId}>
|
||||
<RadioGroupField
|
||||
defaultValue={defaultValue}
|
||||
disabled={readOnly ?? formReadOnly}
|
||||
required={rules?.required as boolean}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
fieldClassName={classNames("column", fieldClassName)}
|
||||
onValueChange={field.onChange}
|
||||
{...props}
|
||||
{...field}
|
||||
name={nameWithPrefix}
|
||||
>
|
||||
{children}
|
||||
</RadioGroupField>
|
||||
</RadioButtonContextProvider>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledRadioGroupField;
|
||||
@@ -1,226 +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, { useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import RadioButton from "./RadioButton";
|
||||
import RadioGroup from "./RadioGroup";
|
||||
import RadioGroupField from "./RadioGroupField";
|
||||
import Form from "../Form";
|
||||
import ControlledRadioGroupField from "./ControlledRadioGroupField";
|
||||
import ControlledInputField from "../input/ControlledInputField";
|
||||
import FormRow from "../FormRow";
|
||||
import { ScmFormListContextProvider } from "../ScmFormListContext";
|
||||
import ControlledList from "../list/ControlledList";
|
||||
import ControlledTable from "../table/ControlledTable";
|
||||
import ControlledColumn from "../table/ControlledColumn";
|
||||
import AddListEntryForm from "../AddListEntryForm";
|
||||
|
||||
storiesOf("Radio Group", module)
|
||||
.add("Uncontrolled State", () => {
|
||||
return (
|
||||
<RadioGroup name="starter_pokemon">
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroup>
|
||||
);
|
||||
})
|
||||
.add("Controlled State", () => {
|
||||
const [value, setValue] = useState<string | undefined>(undefined);
|
||||
return (
|
||||
<RadioGroup name="starter_pokemon" value={value} onValueChange={setValue}>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroup>
|
||||
);
|
||||
})
|
||||
.add("Radio Group Field", () => {
|
||||
const [value, setValue] = useState<string | undefined>(undefined);
|
||||
return (
|
||||
<RadioGroupField
|
||||
label="Choose your starter"
|
||||
helpText="Choose wisely"
|
||||
name="starter_pokemon"
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroupField>
|
||||
);
|
||||
})
|
||||
.add("Controlled Radio Group Field", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ name: "", starter_pokemon: null }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" labelClassName="is-block" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</ControlledRadioGroupField>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Readonly Controlled Radio Group Field", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ name: "Red", starter_pokemon: "SQUIRTLE" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
readOnly={true}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" labelClassName="is-block" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</ControlledRadioGroupField>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("With options", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ starter_pokemon: "CHARMANDER" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
options={[
|
||||
{ value: "CHARMANDER", label: "Charmander" },
|
||||
{ value: "SQUIRTLE", label: "Squirtle" },
|
||||
{ value: "BULBASAUR", label: "Bulbasaur", helpText: "Dont pick this one" },
|
||||
]}
|
||||
/>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Without label prop", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ starter_pokemon: "CHARMANDER" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
options={[
|
||||
{ value: "CHARMANDER" },
|
||||
{ value: "SQUIRTLE" },
|
||||
{ value: "BULBASAUR", helpText: "Dont pick this one" },
|
||||
]}
|
||||
/>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Nested", () => (
|
||||
<Form
|
||||
translationPath={["sample", "form"]}
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
defaultValues={{
|
||||
trainers: [
|
||||
{
|
||||
name: "Red",
|
||||
team: [{ nickname: "Charlie", pokemon: "CHARMANDER" }],
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
team: [{ nickname: "Squirtle", pokemon: "SQUIRTLE" }],
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
team: [{ nickname: "Plant", pokemon: "SQUIRTLE" }],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<ScmFormListContextProvider name={"trainers"}>
|
||||
<ControlledList withDelete>
|
||||
{({ value: trainers }) => (
|
||||
<>
|
||||
<FormRow>
|
||||
<ControlledInputField name={"name"} />
|
||||
</FormRow>
|
||||
<details className="has-background-dark-25 mb-2 p-2">
|
||||
<summary className="is-clickable">Team</summary>
|
||||
<div>
|
||||
<ScmFormListContextProvider name={"team"}>
|
||||
<ControlledTable withDelete>
|
||||
<ControlledColumn name="nickname" />
|
||||
<ControlledColumn name="pokemon" />
|
||||
</ControlledTable>
|
||||
<AddListEntryForm defaultValues={{ nickname: "", pokemon: null }}>
|
||||
<FormRow>
|
||||
<ControlledInputField name="nickname" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="pokemon"
|
||||
label="Starter Pokemon"
|
||||
rules={{ required: true }}
|
||||
options={[{ value: "CHARMANDER" }, { value: "SQUIRTLE" }, { value: "BULBASAUR" }]}
|
||||
/>
|
||||
</FormRow>
|
||||
</AddListEntryForm>
|
||||
</ScmFormListContextProvider>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</ControlledList>
|
||||
</ScmFormListContextProvider>
|
||||
</Form>
|
||||
));
|
||||
@@ -1,116 +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, { ComponentProps } from "react";
|
||||
import classNames from "classnames";
|
||||
import Help from "../base/help/Help";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import { createAttributesForTesting, useGeneratedId } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
import { useRadioButtonContext } from "./RadioButtonContext";
|
||||
|
||||
const StyledRadioButton = styled(RadioGroup.Item)`
|
||||
all: unset;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: var(--scm-border);
|
||||
border-radius: 100%;
|
||||
|
||||
:hover {
|
||||
border-color: var(--scm-hover-color);
|
||||
}
|
||||
|
||||
:hover *::after {
|
||||
background-color: var(--scm-info-hover-color);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
background-color: var(--scm-dark-color-25);
|
||||
border-color: var(--scm-hover-color);
|
||||
}
|
||||
|
||||
:disabled *::after {
|
||||
background-color: var(--scm-info-color);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIndicator = styled(RadioGroup.Indicator)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--scm-info-color);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
id?: string;
|
||||
testId?: string;
|
||||
indicatorClassName?: string;
|
||||
label?: string;
|
||||
labelClassName?: string;
|
||||
helpText?: string;
|
||||
} & ComponentProps<typeof RadioGroup.Item>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.48.0
|
||||
*/
|
||||
const RadioButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ id, testId, indicatorClassName, label, labelClassName, className, helpText, value, ...props }, ref) => {
|
||||
const context = useRadioButtonContext();
|
||||
const inputId = useGeneratedId(id);
|
||||
const labelKey = `${context?.prefix}.radio.${value}`;
|
||||
|
||||
return (
|
||||
<label className={classNames("radio is-flex is-align-items-center", labelClassName)} htmlFor={inputId}>
|
||||
<StyledRadioButton
|
||||
form={context?.formId}
|
||||
id={inputId}
|
||||
value={value}
|
||||
ref={ref}
|
||||
className={classNames("mr-3 mt-3 mb-3", className)}
|
||||
{...props}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
<StyledIndicator className={indicatorClassName} />
|
||||
</StyledRadioButton>
|
||||
{label ?? context?.t(labelKey) ?? value}
|
||||
{helpText ? <Help className="ml-3" text={helpText} /> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default RadioButton;
|
||||
@@ -1,42 +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, { createContext, FC, useContext } from "react";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
type ContextType = {
|
||||
t: TFunction;
|
||||
prefix: string;
|
||||
formId?: string;
|
||||
};
|
||||
|
||||
const RadioButtonContext = createContext<ContextType | null>(null);
|
||||
|
||||
export function useRadioButtonContext() {
|
||||
return useContext(RadioButtonContext);
|
||||
}
|
||||
|
||||
export const RadioButtonContextProvider: FC<ContextType> = ({ children, ...props }) => (
|
||||
<RadioButtonContext.Provider value={props}>{children}</RadioButtonContext.Provider>
|
||||
);
|
||||
@@ -1,49 +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, { ComponentProps } from "react";
|
||||
import Control from "../base/Control";
|
||||
import * as RadixRadio from "@radix-ui/react-radio-group";
|
||||
import RadioButton from "./RadioButton";
|
||||
|
||||
type Props = {
|
||||
options?: { value: string; label?: string; helpText?: string }[];
|
||||
} & ComponentProps<typeof RadixRadio.Root>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.48.0
|
||||
*/
|
||||
const RadioGroup = React.forwardRef<HTMLDivElement, Props>(({ options, children, className, ...props }, ref) => (
|
||||
<RadixRadio.Root {...props} asChild>
|
||||
<Control ref={ref} className={className}>
|
||||
{children ??
|
||||
options?.map((option) => (
|
||||
<RadioButton key={option.value} value={option.value} label={option.label} helpText={option.helpText} />
|
||||
))}
|
||||
</Control>
|
||||
</RadixRadio.Root>
|
||||
));
|
||||
|
||||
export default RadioGroup;
|
||||
@@ -1,58 +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, { ComponentProps } from "react";
|
||||
import Field from "../base/Field";
|
||||
import Label from "../base/label/Label";
|
||||
import Help from "../base/help/Help";
|
||||
import RadioGroup from "./RadioGroup";
|
||||
|
||||
type Props = {
|
||||
fieldClassName?: string;
|
||||
labelClassName?: string;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
} & ComponentProps<typeof RadioGroup>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.48.0
|
||||
*/
|
||||
const RadioGroupField = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ fieldClassName, labelClassName, label, helpText, children, ...props }, ref) => {
|
||||
return (
|
||||
<Field className={fieldClassName} as="fieldset">
|
||||
<Label className={labelClassName} as="legend">
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<RadioGroup ref={ref} {...props}>
|
||||
{children}
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default RadioGroupField;
|
||||
@@ -1,164 +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 { 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;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,87 +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, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import SelectField from "./SelectField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof SelectField>,
|
||||
"error" | "label" | "required" | keyof ControllerRenderProps
|
||||
> & {
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
name: Path<T>;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
function ControlledSelectField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<SelectField
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `select-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledSelectField;
|
||||
@@ -1,57 +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, { InputHTMLAttributes, Key, OptionHTMLAttributes } from "react";
|
||||
import classNames from "classnames";
|
||||
import { createVariantClass, Variant } from "../variants";
|
||||
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
variant?: Variant;
|
||||
options?: Array<OptionHTMLAttributes<HTMLOptionElement> & { label: string }>;
|
||||
testId?: string;
|
||||
} & InputHTMLAttributes<HTMLSelectElement>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const Select = React.forwardRef<HTMLSelectElement, Props>(
|
||||
({ variant, children, className, options, testId, ...props }, ref) => (
|
||||
<div className={classNames("select", { "is-multiple": props.multiple }, createVariantClass(variant), className)}>
|
||||
<select ref={ref} {...props} {...createAttributesForTesting(testId)} className={className}>
|
||||
{options
|
||||
? options.map((opt) => (
|
||||
<option {...opt} key={opt.value as Key}>
|
||||
{opt.label}
|
||||
{opt.children}
|
||||
</option>
|
||||
))
|
||||
: children}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Select;
|
||||
@@ -1,63 +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 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 Help from "../base/help/Help";
|
||||
import Select from "./Select";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
helpText?: string;
|
||||
error?: string;
|
||||
} & React.ComponentProps<typeof Select>;
|
||||
|
||||
/**
|
||||
* @see https://bulma.io/documentation/form/select/
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const SelectField = React.forwardRef<HTMLSelectElement, Props>(
|
||||
({ label, helpText, error, className, id, ...props }, ref) => {
|
||||
const selectId = useGeneratedId(id ?? props.testId);
|
||||
const variant = error ? "danger" : undefined;
|
||||
return (
|
||||
<Field className={className}>
|
||||
<Label htmlFor={selectId}>
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<Control>
|
||||
<Select id={selectId} variant={variant} ref={ref} className="is-full-width" {...props}></Select>
|
||||
</Control>
|
||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default SelectField;
|
||||
@@ -1,49 +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, HTMLAttributes, useMemo } from "react";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useWatch } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
children?: (value: unknown) => React.ReactNode | React.ReactNode[];
|
||||
} & HTMLAttributes<HTMLTableCellElement>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
*/
|
||||
const ControlledColumn: FC<Props> = ({ name, children, ...props }) => {
|
||||
const { control } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = useMemo(() => (formPathPrefix ? `${formPathPrefix}.${name}` : name), [formPathPrefix, name]);
|
||||
const value = useWatch({ control, name: nameWithPrefix, disabled: typeof children === "function" });
|
||||
const allValues = useWatch({ control, name: formPathPrefix, disabled: typeof children !== "function" });
|
||||
|
||||
return <td {...props}>{typeof children === "function" ? children(allValues) : value}</td>;
|
||||
};
|
||||
|
||||
export default ControlledColumn;
|
||||
@@ -1,99 +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, { ReactElement } from "react";
|
||||
import { Path, PathValue } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { ScmFormPathContextProvider, useScmFormPathContext } from "../FormPathContext";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormListContext } from "../ScmFormListContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>, PATH extends Path<T>> = {
|
||||
value: PathValue<T, PATH>;
|
||||
index: number;
|
||||
remove: () => void;
|
||||
};
|
||||
|
||||
type Props<T extends Record<string, unknown>, PATH extends Path<T>> = {
|
||||
withDelete?: boolean;
|
||||
children: ((renderProps: RenderProps<T, PATH>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.43.0
|
||||
*/
|
||||
function ControlledTable<T extends Record<string, unknown>, PATH extends Path<T>>({
|
||||
withDelete,
|
||||
children,
|
||||
className,
|
||||
}: Props<T, PATH>) {
|
||||
const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form.table" });
|
||||
const { readOnly, t } = useScmFormContext();
|
||||
const nameWithPrefix = useScmFormPathContext();
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const { fields, remove } = useScmFormListContext();
|
||||
const deleteLabel = t(`${prefixedNameWithoutIndices}.delete`) || defaultTranslate("delete.label");
|
||||
const emptyTableLabel = t(`${prefixedNameWithoutIndices}.empty`) || defaultTranslate("empty.label");
|
||||
const actionHeaderLabel = t(`${prefixedNameWithoutIndices}.action.label`) || defaultTranslate("headers.action.label");
|
||||
|
||||
return (
|
||||
<table className={classNames("table content is-hoverable", className)}>
|
||||
<thead>
|
||||
<tr>
|
||||
{React.Children.map(children, (child) => (
|
||||
<th>{t(`${prefixedNameWithoutIndices}.${(child as ReactElement).props.name}.label`)}</th>
|
||||
))}
|
||||
{withDelete && !readOnly ? <th className="has-text-right">{actionHeaderLabel}</th> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.length === 0 ? <tr><td colSpan={1000}><Notification type="info">{emptyTableLabel}</Notification></td></tr> : null}
|
||||
{fields.map((value, index) => (
|
||||
<ScmFormPathContextProvider key={value.id} path={`${nameWithPrefix}.${index}`}>
|
||||
<tr>
|
||||
{children}
|
||||
{withDelete && !readOnly ? (
|
||||
<td className="has-text-right">
|
||||
<Button className="px-4" onClick={() => remove(index)} aria-label={deleteLabel}>
|
||||
<span className="icon is-small">
|
||||
<i className="fas fa-trash" />
|
||||
</span>
|
||||
</Button>
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
</ScmFormPathContextProvider>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledTable;
|
||||
Reference in New Issue
Block a user