mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
Implement new form features
Extends existing functionality, provides new fallbacks for translations and adds capabilities to manage array properties in configurations. Committed-by: Florian Scholdei <florian.scholdei@cloudogu.com> Co-authored-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
committed by
SCM-Manager
parent
719f6c4c09
commit
dda52b8400
2
gradle/changelog/form_list.yaml
Normal file
2
gradle/changelog/form_list.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Enable developers to manage array properties in forms
|
||||
@@ -66,8 +66,9 @@ type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
* @since 2.41.0
|
||||
*/
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||
({ className, variant, isLoading, testId, type, children, ...props }, ref) => (
|
||||
<button
|
||||
type={type ?? "button"}
|
||||
{...props}
|
||||
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||
ref={ref}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
@@ -29,16 +29,13 @@ type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
class Subtitle extends React.Component<Props> {
|
||||
render() {
|
||||
const { subtitle, className, children } = this.props;
|
||||
if (subtitle) {
|
||||
return <h2 className={classNames("subtitle", className)}>{subtitle}</h2>;
|
||||
} else if (children) {
|
||||
return <h2 className={classNames("subtitle", className)}>{children}</h2>;
|
||||
}
|
||||
return null;
|
||||
const Subtitle: FC<Props> = ({ subtitle, className, children }) => {
|
||||
if (subtitle) {
|
||||
return <h2 className={classNames("subtitle", className)}>{subtitle}</h2>;
|
||||
} else if (children) {
|
||||
return <h2 className={classNames("subtitle", className)}>{children}</h2>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Subtitle;
|
||||
|
||||
@@ -37,10 +37,12 @@
|
||||
"react": "17",
|
||||
"react-hook-form": "7",
|
||||
"react-i18next": "11",
|
||||
"react-query": "3"
|
||||
"react-query": "3",
|
||||
"styled-components": "5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-buttons": "2.42.3-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.42.3-SNAPSHOT",
|
||||
"@scm-manager/ui-api": "2.42.3-SNAPSHOT"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
@@ -50,4 +52,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
scm-ui/ui-forms/src/AddListEntryForm.tsx
Normal file
123
scm-ui/ui-forms/src/AddListEntryForm.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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") }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, [defaultValues, isSubmitSuccessful, reset]);
|
||||
|
||||
if (readOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScmFormContextProvider {...form} t={translateWithExtraPrefix} formId={nameWithPrefix}>
|
||||
<ScmFormPathContextProvider path="">
|
||||
<form id={nameWithPrefix} onSubmit={form.handleSubmit(onSubmit)}></form>
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
<div className="level-right">
|
||||
<Button
|
||||
form={nameWithPrefix}
|
||||
type="submit"
|
||||
variant={isNested ? undefined : "secondary"}
|
||||
disabled={(disableSubmitWhenDirty && !isDirty) || !isValid}
|
||||
>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<StyledHr />
|
||||
</ScmFormPathContextProvider>
|
||||
</ScmFormContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddListEntryForm;
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
import Form from "./Form";
|
||||
import FormRow from "./FormRow";
|
||||
import ControlledInputField from "./input/ControlledInputField";
|
||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||
|
||||
<Meta title="Form" />
|
||||
|
||||
<Story name="Creation">
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "",
|
||||
password: "",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</Story>
|
||||
|
||||
<Story name="Editing">
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</Story>
|
||||
|
||||
<Story name="GlobalConfiguration">
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
url: "",
|
||||
filter: "",
|
||||
username: "",
|
||||
password: "",
|
||||
roleLevel: "",
|
||||
updateIssues: false,
|
||||
disableRepoConfig: false,
|
||||
}}
|
||||
>
|
||||
{({ watch }) => (
|
||||
<>
|
||||
<FormRow>
|
||||
<ControlledInputField name="url" label="URL" helpText="URL of Jira installation (with context path)." />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledInputField name="filter" label="Project Filter" helpText="Filters for jira project key." />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField
|
||||
name="updateIssues"
|
||||
label="Update Jira Issues"
|
||||
helpText="Enable the automatic update function."
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow hide={watch("filter")}>
|
||||
<ControlledInputField
|
||||
name="username"
|
||||
label="Username"
|
||||
helpText="Jira username for connection."
|
||||
className="is-half"
|
||||
/>
|
||||
<ControlledInputField
|
||||
name="password"
|
||||
label="Password"
|
||||
helpText="Jira password for connection."
|
||||
type="password"
|
||||
className="is-half"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow hide={watch("filter")}>
|
||||
<ControlledInputField
|
||||
name="roleLevel"
|
||||
label="Role Visibility"
|
||||
helpText="Defines for which Project Role the comments are visible."
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField
|
||||
name="disableRepoConfig"
|
||||
label="Do not allow repository configuration"
|
||||
helpText="Do not allow repository owners to configure jira instances."
|
||||
/>
|
||||
</FormRow>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Story>
|
||||
|
||||
<Story name="RepoConfiguration">
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
url: "",
|
||||
option: "",
|
||||
anotherOption: "",
|
||||
disableA: false,
|
||||
disableB: false,
|
||||
disableC: true,
|
||||
}}
|
||||
>
|
||||
<ControlledInputField name="url" />
|
||||
<ControlledInputField name="option" />
|
||||
<ControlledInputField name="anotherOption" />
|
||||
<ControlledCheckboxField name="disableA" />
|
||||
<ControlledCheckboxField name="disableB" />
|
||||
<ControlledCheckboxField name="disableC" />
|
||||
</Form>
|
||||
</Story>
|
||||
|
||||
<Story name="ReadOnly">
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
active: true,
|
||||
}}
|
||||
readOnly
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</Story>
|
||||
295
scm-ui/ui-forms/src/Form.stories.tsx
Normal file
295
scm-ui/ui-forms/src/Form.stories.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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 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";
|
||||
|
||||
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 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",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</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,
|
||||
}}
|
||||
>
|
||||
<ControlledInputField name="url" />
|
||||
<ControlledInputField name="option" />
|
||||
<ControlledInputField name="anotherOption" />
|
||||
<ControlledCheckboxField name="disableA" />
|
||||
<ControlledCheckboxField name="disableB" />
|
||||
<ControlledCheckboxField name="disableC" />
|
||||
</Form>
|
||||
))
|
||||
.add("ReadOnly", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
active: true,
|
||||
}}
|
||||
readOnly
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<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 }}>
|
||||
<ControlledInputField
|
||||
name="key"
|
||||
rules={{
|
||||
validate: (newKey) =>
|
||||
!(webhook as SimpleWebHookConfiguration).headers.some(({ key }) => newKey === key),
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
<ControlledInputField name="value" />
|
||||
<ControlledCheckboxField name="concealed" />
|
||||
</AddListEntryForm>
|
||||
</ScmFormListContextProvider>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</ControlledList>
|
||||
</ScmFormListContextProvider>
|
||||
</Form>
|
||||
));
|
||||
@@ -28,12 +28,7 @@ import { ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { ScmFormContextProvider } from "./ScmFormContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
import FormRow from "./FormRow";
|
||||
import ControlledInputField from "./input/ControlledInputField";
|
||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import ControlledSelectField from "./select/ControlledSelectField";
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||
UseFormReturn<T>,
|
||||
@@ -81,9 +76,24 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
const { formState, handleSubmit, reset } = 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 successNotification = translateWithFallback("submit-success-notification", {
|
||||
defaultValue: defaultTranslate("submit-success-notification"),
|
||||
});
|
||||
|
||||
// See https://react-hook-form.com/api/useform/reset/
|
||||
useEffect(() => {
|
||||
@@ -100,17 +110,6 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
}
|
||||
}, [isDirty]);
|
||||
|
||||
const translateWithFallback = useCallback<typeof t>(
|
||||
(key, ...args) => {
|
||||
const translation = t(key, ...(args as any));
|
||||
if (translation === `${prefix}.${key}`) {
|
||||
return "";
|
||||
}
|
||||
return translation;
|
||||
},
|
||||
[prefix, t]
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
async (data) => {
|
||||
setError(null);
|
||||
@@ -128,40 +127,31 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
);
|
||||
|
||||
return (
|
||||
<ScmFormContextProvider {...form} readOnly={isSubmitting || readOnly} t={translateWithFallback}>
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
{showSuccessNotification ? (
|
||||
<SuccessNotification
|
||||
label={translateWithFallback("submit-success-notification")}
|
||||
hide={() => setShowSuccessNotification(false)}
|
||||
/>
|
||||
) : null}
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
{!readOnly ? (
|
||||
<Level
|
||||
right={
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
testId={submitButtonTestId ?? "submit-button"}
|
||||
disabled={!isDirty || !isValid}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
<ScmFormContextProvider {...form} readOnly={isSubmitting || readOnly} t={translateWithFallback} formId={prefix}>
|
||||
<form onSubmit={handleSubmit(submit)} id={prefix}></form>
|
||||
{showSuccessNotification ? (
|
||||
<SuccessNotification label={successNotification} hide={() => setShowSuccessNotification(false)} />
|
||||
) : null}
|
||||
{typeof children === "function" ? children(form) : children}
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
{!readOnly ? (
|
||||
<Level
|
||||
right={
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
testId={submitButtonTestId ?? "submit-button"}
|
||||
disabled={!isDirty || !isValid}
|
||||
isLoading={isSubmitting}
|
||||
form={prefix}
|
||||
>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</ScmFormContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Object.assign(Form, {
|
||||
Row: FormRow,
|
||||
Input: ControlledInputField,
|
||||
Checkbox: ControlledCheckboxField,
|
||||
SecretConfirmation: ControlledSecretConfirmationField,
|
||||
Select: ControlledSelectField,
|
||||
});
|
||||
export default Form;
|
||||
|
||||
73
scm-ui/ui-forms/src/FormPathContext.tsx
Normal file
73
scm-ui/ui-forms/src/FormPathContext.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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>;
|
||||
};
|
||||
@@ -24,13 +24,26 @@
|
||||
|
||||
import React, { HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
const FormRowDiv = styled.div`
|
||||
.field {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormRow = React.forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||
({ className, children, hidden, ...rest }, ref) =>
|
||||
hidden ? null : (
|
||||
<div ref={ref} className={classNames("columns", className)} {...rest}>
|
||||
<FormRowDiv ref={ref} className={classNames("is-flex is-flex-wrap-wrap", className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
</FormRowDiv>
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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);
|
||||
|
||||
65
scm-ui/ui-forms/src/ScmFormListContext.tsx
Normal file
65
scm-ui/ui-forms/src/ScmFormListContext.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
@@ -23,14 +23,12 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Tooltip } from "@scm-manager/ui-overlays";
|
||||
|
||||
type Props = { text?: string; className?: string };
|
||||
|
||||
/**
|
||||
* TODO: Implement tooltip
|
||||
*/
|
||||
const Help = ({ text, className }: Props) => (
|
||||
<span className={classNames("fas fa-fw fa-question-circle has-text-blue-light", className)} title={text} />
|
||||
<Tooltip className={className} message={text}>
|
||||
<i className="fas fa-fw fa-question-circle has-text-blue-light" />
|
||||
</Tooltip>
|
||||
);
|
||||
export default Help;
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path, RegisterOptions } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import CheckboxField from "./CheckboxField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof CheckboxField>,
|
||||
@@ -42,31 +43,33 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
className,
|
||||
testId,
|
||||
defaultChecked,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const labelTranslation = label || t(`${name}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||
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={name}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultChecked as never}
|
||||
render={({ field }) => (
|
||||
<CheckboxField
|
||||
className={classNames("column", className)}
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
defaultChecked={field.value}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
testId={testId ?? `checkbox-${name}`}
|
||||
testId={testId ?? `checkbox-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
27
scm-ui/ui-forms/src/helpers.ts
Normal file
27
scm-ui/ui-forms/src/helpers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export function prefixWithoutIndices(path: string): string {
|
||||
return path.replace(/(\.\d+)/g, "");
|
||||
}
|
||||
@@ -22,6 +22,33 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export { default as Form } from "./Form";
|
||||
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 { 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";
|
||||
|
||||
export { default as ConfigurationForm } from "./ConfigurationForm";
|
||||
export * from "./resourceHooks";
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof InputField>,
|
||||
@@ -42,32 +43,38 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
className,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const labelTranslation = label || t(`${name}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||
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={name}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
className={classNames("column", className)}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
{...props}
|
||||
{...field}
|
||||
form={formId}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={fieldState.error ? fieldState.error.message || t(`${name}.error.${fieldState.error.type}`) : undefined}
|
||||
testId={testId ?? `input-${name}`}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof InputField>,
|
||||
@@ -56,60 +57,70 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, watch, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const labelTranslation = label || t(`${name}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||
const confirmationLabelTranslation = confirmationLabel || t(`${name}.confirmation.label`) || "";
|
||||
const confirmationHelpTextTranslation = confirmationHelpText || t(`${name}.confirmation.helpText`);
|
||||
const confirmationErrorMessageTranslation = confirmationErrorMessage || t(`${name}.confirmation.errorMessage`);
|
||||
const secretValue = watch(name);
|
||||
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);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
name={nameWithPrefix}
|
||||
defaultValue={defaultValue as never}
|
||||
rules={{
|
||||
...rules,
|
||||
deps: [`${name}Confirmation`],
|
||||
deps: [`${nameWithPrefix}Confirmation`],
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
className={classNames("column", className)}
|
||||
className={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(`${name}.error.${fieldState.error.type}`) : undefined
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${name}`}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`${name}Confirmation`}
|
||||
name={`${nameWithPrefix}Confirmation`}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
className={classNames("column", className)}
|
||||
className={className}
|
||||
type="password"
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
disabled={props.disabled}
|
||||
{...field}
|
||||
form={formId}
|
||||
label={confirmationLabelTranslation}
|
||||
helpText={confirmationHelpTextTranslation}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${name}.confirmation.error.${fieldState.error.type}`)
|
||||
? fieldState.error.message ||
|
||||
t(`${prefixedNameWithoutIndices}.confirmation.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={confirmationTestId ?? `input-${name}-confirmation`}
|
||||
testId={confirmationTestId ?? `input-${nameWithPrefix}-confirmation`}
|
||||
/>
|
||||
)}
|
||||
rules={{
|
||||
|
||||
88
scm-ui/ui-forms/src/list/ControlledList.tsx
Normal file
88
scm-ui/ui-forms/src/list/ControlledList.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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;
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import SelectField from "./SelectField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof SelectField>,
|
||||
@@ -42,32 +43,38 @@ function ControlledSelectField<T extends Record<string, unknown>>({
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
className,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const labelTranslation = label || t(`${name}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||
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={name}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<SelectField
|
||||
className={classNames("column", className)}
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={fieldState.error ? fieldState.error.message || t(`${name}.error.${fieldState.error.type}`) : undefined}
|
||||
testId={testId ?? `select-${name}`}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `select-${nameWithPrefix}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -22,22 +22,32 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { InputHTMLAttributes } from "react";
|
||||
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>;
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, Props>(({ variant, children, className, testId, ...props }, ref) => (
|
||||
<div className={classNames("select", { "is-multiple": props.multiple }, createVariantClass(variant), className)}>
|
||||
<select ref={ref} {...props} {...createAttributesForTesting(testId)}>
|
||||
{children}
|
||||
</select>
|
||||
</div>
|
||||
));
|
||||
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)}>
|
||||
{options
|
||||
? options.map((option) => (
|
||||
<option {...option} key={option.value as Key}>
|
||||
{option.label}
|
||||
{option.children}
|
||||
</option>
|
||||
))
|
||||
: children}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Select;
|
||||
|
||||
49
scm-ui/ui-forms/src/table/ControlledColumn.tsx
Normal file
49
scm-ui/ui-forms/src/table/ControlledColumn.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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;
|
||||
100
scm-ui/ui-forms/src/table/ControlledTable.tsx
Normal file
100
scm-ui/ui-forms/src/table/ControlledTable.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
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 actionHeaderLabel = t(`${prefixedNameWithoutIndices}.action.label`) || defaultTranslate("headers.action.label");
|
||||
|
||||
if (!fields.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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.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;
|
||||
@@ -45,6 +45,10 @@
|
||||
--scm-popover-border-color: #{$popover-border-color};
|
||||
}
|
||||
|
||||
details > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// TODO split into multiple files
|
||||
|
||||
.is-ellipsis-overflow {
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
{
|
||||
"form": {
|
||||
"submit": "Speichern",
|
||||
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
|
||||
"table": {
|
||||
"headers": {
|
||||
"action": {
|
||||
"label": "Aktion"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"label": "Löschen"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"add": {
|
||||
"label": "{{entity}} hinzufügen"
|
||||
},
|
||||
"delete": {
|
||||
"label": "{{entity}} löschen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Anmeldung",
|
||||
"subtitle": "Bitte anmelden, um fortzufahren.",
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
{
|
||||
"form": {
|
||||
"submit": "Submit",
|
||||
"submit-success-notification": "Configuration changed successfully!",
|
||||
"table": {
|
||||
"headers": {
|
||||
"action": {
|
||||
"label": "Action"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"label": "Delete"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"add": {
|
||||
"label": "Add {{entity}}"
|
||||
},
|
||||
"delete": {
|
||||
"label": "Delete {{entity}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
"subtitle": "Please login to proceed",
|
||||
|
||||
@@ -35,6 +35,9 @@ import * as ClassNames from "classnames";
|
||||
import * as QueryString from "query-string";
|
||||
import * as UIExtensions from "@scm-manager/ui-extensions";
|
||||
import * as UIComponents from "@scm-manager/ui-components";
|
||||
import * as UIButtons from "@scm-manager/ui-buttons";
|
||||
import * as UIForms from "@scm-manager/ui-forms";
|
||||
import * as UIOverlays from "@scm-manager/ui-overlays";
|
||||
import * as UIApi from "@scm-manager/ui-api";
|
||||
|
||||
declare global {
|
||||
@@ -56,6 +59,9 @@ defineStatic("classnames", ClassNames);
|
||||
defineStatic("query-string", QueryString);
|
||||
defineStatic("@scm-manager/ui-extensions", UIExtensions);
|
||||
defineStatic("@scm-manager/ui-components", UIComponents);
|
||||
defineStatic("@scm-manager/ui-buttons", UIButtons);
|
||||
defineStatic("@scm-manager/ui-forms", UIForms);
|
||||
defineStatic("@scm-manager/ui-overlays", UIOverlays);
|
||||
defineStatic("@scm-manager/ui-api", UIApi);
|
||||
|
||||
// redux is deprecated in favor of ui-api
|
||||
|
||||
Reference in New Issue
Block a user