Improve diverse form features

- General responsiveness
- Resize select component
- Fix datepicker for dark themes
- Make success notification configurable

Committed-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>

Reviewed-by: Rene Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2023-04-03 10:02:17 +02:00
committed by SCM-Manager
parent 026ffa18fd
commit b53f8bcf12
18 changed files with 285 additions and 78 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Optional reset button for forms

View File

@@ -32,7 +32,7 @@ type Result<C extends HalRepresentation> = {
configuration: C; configuration: C;
}; };
type MutationVariables<C extends HalRepresentation> = { type MutationVariables<C> = {
configuration: C; configuration: C;
contentType: string; contentType: string;
link: string; link: string;
@@ -53,8 +53,8 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
error: mutationError, error: mutationError,
mutateAsync, mutateAsync,
data: updateResponse, data: updateResponse,
} = useMutation<Response, Error, MutationVariables<C>>( } = useMutation<Response, Error, MutationVariables<Omit<C, keyof HalRepresentation>>>(
(vars: MutationVariables<C>) => apiClient.put(vars.link, vars.configuration, vars.contentType), (vars) => apiClient.put(vars.link, vars.configuration, vars.contentType),
{ {
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries(queryKey); await queryClient.invalidateQueries(queryKey);
@@ -65,7 +65,7 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
const isReadOnly = !data?.configuration._links.update; const isReadOnly = !data?.configuration._links.update;
const update = useCallback( const update = useCallback(
(configuration: C) => { (configuration: Omit<C, keyof HalRepresentation>) => {
if (data && !isReadOnly) { if (data && !isReadOnly) {
return mutateAsync({ return mutateAsync({
configuration, configuration,

View File

@@ -102,7 +102,7 @@ function AddListEntryForm<FormType extends Record<string, unknown>, DefaultValue
return ( return (
<ScmFormContextProvider {...form} t={translateWithExtraPrefix} formId={nameWithPrefix}> <ScmFormContextProvider {...form} t={translateWithExtraPrefix} formId={nameWithPrefix}>
<ScmFormPathContextProvider path=""> <ScmFormPathContextProvider path="">
<form id={nameWithPrefix} onSubmit={form.handleSubmit(onSubmit)}></form> <form id={nameWithPrefix} onSubmit={form.handleSubmit(onSubmit)} noValidate></form>
{typeof children === "function" ? children(form) : children} {typeof children === "function" ? children(form) : children}
<div className="level-right"> <div className="level-right">
<Button <Button

View File

@@ -21,13 +21,18 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * 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";
type Props<T extends HalRepresentation> = Pick<ComponentProps<typeof Form<T, T>>, "translationPath" | "children"> & { 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; link: string;
}; };
@@ -35,20 +40,17 @@ type Props<T extends HalRepresentation> = Pick<ComponentProps<typeof Form<T, T>>
* @beta * @beta
* @since 2.41.0 * @since 2.41.0
*/ */
export function ConfigurationForm<T extends HalRepresentation>({link, translationPath, children}: Props<T>) { export function ConfigurationForm<T extends HalRepresentation>({ link, children, ...formProps }: Props<T>) {
const {initialConfiguration, isReadOnly, update, isLoading} = useConfigLink<T>(link); const { initialConfiguration, isReadOnly, update, isLoading } = useConfigLink<T>(link);
const [t] = useTranslation("commons", { keyPrefix: "form" });
if (isLoading || !initialConfiguration) { if (isLoading || !initialConfiguration) {
return <Loading/>; return <Loading />;
} }
return ( return (
<Form<T, T> <Form onSubmit={update} defaultValues={initialConfiguration} readOnly={isReadOnly}
onSubmit={update} successMessageFallback={t("submit-success-notification")} {...formProps}>
translationPath={translationPath}
defaultValues={initialConfiguration}
readOnly={isReadOnly}
>
{children} {children}
</Form> </Form>
); );

View File

@@ -22,7 +22,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
/* eslint-disable no-console */ /* eslint-disable no-console */
import React from "react"; import React, { useRef, useState } from "react";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import Form from "./Form"; import Form from "./Form";
import FormRow from "./FormRow"; import FormRow from "./FormRow";
@@ -35,6 +35,7 @@ import ControlledColumn from "./table/ControlledColumn";
import ControlledTable from "./table/ControlledTable"; import ControlledTable from "./table/ControlledTable";
import AddListEntryForm from "./AddListEntryForm"; import AddListEntryForm from "./AddListEntryForm";
import { ScmFormListContextProvider } from "./ScmFormListContext"; import { ScmFormListContextProvider } from "./ScmFormListContext";
import { HalRepresentation } from "@scm-manager/ui-types";
export type SimpleWebHookConfiguration = { export type SimpleWebHookConfiguration = {
urlPattern: string; urlPattern: string;
@@ -62,7 +63,7 @@ storiesOf("Forms", module)
}} }}
> >
<FormRow> <FormRow>
<ControlledInputField name="name" /> <ControlledInputField rules={{ required: true }} name="name" />
</FormRow> </FormRow>
<FormRow> <FormRow>
<ControlledSecretConfirmationField name="password" /> <ControlledSecretConfirmationField name="password" />
@@ -79,6 +80,7 @@ storiesOf("Forms", module)
defaultValues={{ defaultValues={{
name: "trillian", name: "trillian",
password: "secret", password: "secret",
passwordConfirmation: "",
active: true, active: true,
}} }}
> >
@@ -93,6 +95,77 @@ storiesOf("Forms", module)
</FormRow> </FormRow>
</Form> </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", () => ( .add("GlobalConfiguration", () => (
<Form <Form
onSubmit={console.log} onSubmit={console.log}
@@ -242,6 +315,36 @@ storiesOf("Forms", module)
}, },
], ],
}} }}
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"> <ScmFormListContextProvider name="webhooks">
<ControlledList withDelete> <ControlledList withDelete>
@@ -273,6 +376,7 @@ storiesOf("Forms", module)
<ControlledColumn name="concealed">{(value) => (value ? <b>Hallo</b> : null)}</ControlledColumn> <ControlledColumn name="concealed">{(value) => (value ? <b>Hallo</b> : null)}</ControlledColumn>
</ControlledTable> </ControlledTable>
<AddListEntryForm defaultValues={{ key: "", value: "", concealed: false }}> <AddListEntryForm defaultValues={{ key: "", value: "", concealed: false }}>
<FormRow>
<ControlledInputField <ControlledInputField
name="key" name="key"
rules={{ rules={{
@@ -281,8 +385,13 @@ storiesOf("Forms", module)
required: true, required: true,
}} }}
/> />
</FormRow>
<FormRow>
<ControlledInputField name="value" /> <ControlledInputField name="value" />
</FormRow>
<FormRow>
<ControlledCheckboxField name="concealed" /> <ControlledCheckboxField name="concealed" />
</FormRow>
</AddListEntryForm> </AddListEntryForm>
</ScmFormListContextProvider> </ScmFormListContextProvider>
</div> </div>

View File

@@ -28,13 +28,19 @@ import { ErrorNotification, Level } from "@scm-manager/ui-components";
import { ScmFormContextProvider } from "./ScmFormContext"; import { ScmFormContextProvider } from "./ScmFormContext";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@scm-manager/ui-buttons"; import { Button } from "@scm-manager/ui-buttons";
import { HalRepresentation } from "@scm-manager/ui-types"; import styled from "styled-components";
import { setValues } from "./helpers";
type RenderProps<T extends Record<string, unknown>> = Omit< type RenderProps<T extends Record<string, unknown>> = Omit<
UseFormReturn<T>, UseFormReturn<T>,
"register" | "unregister" | "handleSubmit" | "control" "register" | "unregister" | "handleSubmit" | "control"
>; >;
const ButtonsContainer = styled.div`
display: flex;
gap: 0.75rem;
`;
const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label, hide }) => { const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label, hide }) => {
if (!label) { if (!label) {
return null; return null;
@@ -52,28 +58,60 @@ type Props<FormType extends Record<string, unknown>, DefaultValues extends FormT
children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode; children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
translationPath: [namespace: string, prefix: string]; translationPath: [namespace: string, prefix: string];
onSubmit: SubmitHandler<FormType>; onSubmit: SubmitHandler<FormType>;
defaultValues: Omit<DefaultValues, keyof HalRepresentation>; defaultValues: DefaultValues;
readOnly?: boolean; readOnly?: boolean;
submitButtonTestId?: string; 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 * @beta
* @since 2.41.0 * @since 2.41.0
*/ */
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType>({ function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType = FormType>({
children, children,
onSubmit, onSubmit,
defaultValues, defaultValues,
translationPath, translationPath,
readOnly, readOnly,
withResetTo,
withDiscardChanges,
successMessageFallback,
submitButtonTestId, submitButtonTestId,
}: Props<FormType, DefaultValues>) { }: Props<FormType, DefaultValues>) {
const form = useForm<FormType>({ const form = useForm<FormType>({
mode: "onChange", mode: "onChange",
defaultValues: defaultValues as DeepPartial<FormType>, defaultValues: defaultValues as DeepPartial<FormType>,
}); });
const { formState, handleSubmit, reset } = form; const { formState, handleSubmit, reset, setValue } = form;
const [ns, prefix] = translationPath; const [ns, prefix] = translationPath;
const { t } = useTranslation(ns, { keyPrefix: prefix }); const { t } = useTranslation(ns, { keyPrefix: prefix });
const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form" }); const [defaultTranslate] = useTranslation("commons", { keyPrefix: "form" });
@@ -91,9 +129,16 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
const [error, setError] = useState<Error | null | undefined>(); const [error, setError] = useState<Error | null | undefined>();
const [showSuccessNotification, setShowSuccessNotification] = useState(false); const [showSuccessNotification, setShowSuccessNotification] = useState(false);
const submitButtonLabel = t("submit", { defaultValue: defaultTranslate("submit") }); 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", { const successNotification = translateWithFallback("submit-success-notification", {
defaultValue: defaultTranslate("submit-success-notification"), defaultValue: successMessageFallback,
}); });
const overwriteValues = useCallback(() => {
if (withResetTo) {
setValues(withResetTo, setValue);
}
}, [setValue, withResetTo]);
// See https://react-hook-form.com/api/useform/reset/ // See https://react-hook-form.com/api/useform/reset/
useEffect(() => { useEffect(() => {
@@ -128,7 +173,7 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
return ( return (
<ScmFormContextProvider {...form} readOnly={isSubmitting || readOnly} t={translateWithFallback} formId={prefix}> <ScmFormContextProvider {...form} readOnly={isSubmitting || readOnly} t={translateWithFallback} formId={prefix}>
<form onSubmit={handleSubmit(submit)} id={prefix}></form> <form onSubmit={handleSubmit(submit)} onReset={() => reset()} id={prefix} noValidate></form>
{showSuccessNotification ? ( {showSuccessNotification ? (
<SuccessNotification label={successNotification} hide={() => setShowSuccessNotification(false)} /> <SuccessNotification label={successNotification} hide={() => setShowSuccessNotification(false)} />
) : null} ) : null}
@@ -137,6 +182,7 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
{!readOnly ? ( {!readOnly ? (
<Level <Level
right={ right={
<ButtonsContainer>
<Button <Button
type="submit" type="submit"
variant="primary" variant="primary"
@@ -147,6 +193,17 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
> >
{submitButtonLabel} {submitButtonLabel}
</Button> </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} ) : null}

View File

@@ -24,26 +24,13 @@
import React, { HTMLProps } from "react"; import React, { HTMLProps } from "react";
import classNames from "classnames"; 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>>( const FormRow = React.forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
({ className, children, hidden, ...rest }, ref) => ({ className, children, hidden, ...rest }, ref) =>
hidden ? null : ( hidden ? null : (
<FormRowDiv ref={ref} className={classNames("is-flex is-flex-wrap-wrap", className)} {...rest}> <div ref={ref} className={classNames("columns", className)} {...rest}>
{children} {children}
</FormRowDiv> </div>
) )
); );

View File

@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
import CheckboxField from "./CheckboxField"; import CheckboxField from "./CheckboxField";
import { useScmFormPathContext } from "../FormPathContext"; import { useScmFormPathContext } from "../FormPathContext";
import { prefixWithoutIndices } from "../helpers"; import { prefixWithoutIndices } from "../helpers";
import classNames from "classnames";
type Props<T extends Record<string, unknown>> = Omit< type Props<T extends Record<string, unknown>> = Omit<
ComponentProps<typeof CheckboxField>, ComponentProps<typeof CheckboxField>,
@@ -46,6 +47,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
testId, testId,
defaultChecked, defaultChecked,
readOnly, readOnly,
className,
...props ...props
}: Props<T>) { }: Props<T>) {
const { control, t, readOnly: formReadonly, formId } = useScmFormContext(); const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
@@ -64,7 +66,8 @@ function ControlledInputField<T extends Record<string, unknown>>({
<CheckboxField <CheckboxField
form={formId} form={formId}
readOnly={readOnly ?? formReadonly} readOnly={readOnly ?? formReadonly}
defaultChecked={field.value} checked={field.value}
className={classNames("column", className)}
{...props} {...props}
{...field} {...field}
label={labelTranslation} label={labelTranslation}

View File

@@ -22,6 +22,30 @@
* SOFTWARE. * SOFTWARE.
*/ */
import { UseFormReturn } from "react-hook-form";
export function prefixWithoutIndices(path: string): string { export function prefixWithoutIndices(path: string): string {
return path.replace(/(\.\d+)/g, ""); 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 });
}
}
}

View File

@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
import InputField from "./InputField"; import InputField from "./InputField";
import { useScmFormPathContext } from "../FormPathContext"; import { useScmFormPathContext } from "../FormPathContext";
import { prefixWithoutIndices } from "../helpers"; import { prefixWithoutIndices } from "../helpers";
import classNames from "classnames";
type Props<T extends Record<string, unknown>> = Omit< type Props<T extends Record<string, unknown>> = Omit<
ComponentProps<typeof InputField>, ComponentProps<typeof InputField>,
@@ -46,6 +47,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
testId, testId,
defaultValue, defaultValue,
readOnly, readOnly,
className,
...props ...props
}: Props<T>) { }: Props<T>) {
const { control, t, readOnly: formReadonly, formId } = useScmFormContext(); const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
@@ -64,6 +66,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
<InputField <InputField
readOnly={readOnly ?? formReadonly} readOnly={readOnly ?? formReadonly}
required={rules?.required as boolean} required={rules?.required as boolean}
className={classNames("column", className)}
{...props} {...props}
{...field} {...field}
form={formId} form={formId}

View File

@@ -22,12 +22,13 @@
* SOFTWARE. * SOFTWARE.
*/ */
import React, { ComponentProps } from "react"; import React, { ComponentProps, useCallback, useMemo } from "react";
import { Controller, ControllerRenderProps, Path } from "react-hook-form"; import { Controller, ControllerRenderProps, Path } from "react-hook-form";
import { useScmFormContext } from "../ScmFormContext"; import { useScmFormContext } from "../ScmFormContext";
import InputField from "./InputField"; import InputField from "./InputField";
import { useScmFormPathContext } from "../FormPathContext"; import { useScmFormPathContext } from "../FormPathContext";
import { prefixWithoutIndices } from "../helpers"; import { prefixWithoutIndices } from "../helpers";
import classNames from "classnames";
type Props<T extends Record<string, unknown>> = Omit< type Props<T extends Record<string, unknown>> = Omit<
ComponentProps<typeof InputField>, ComponentProps<typeof InputField>,
@@ -69,6 +70,16 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
const confirmationErrorMessageTranslation = const confirmationErrorMessageTranslation =
confirmationErrorMessage || t(`${prefixedNameWithoutIndices}.confirmation.errorMessage`); confirmationErrorMessage || t(`${prefixedNameWithoutIndices}.confirmation.errorMessage`);
const secretValue = watch(nameWithPrefix); const secretValue = watch(nameWithPrefix);
const validateConfirmField = useCallback(
(value) => secretValue === value || confirmationErrorMessageTranslation,
[confirmationErrorMessageTranslation, secretValue]
);
const confirmFieldRules = useMemo(
() => ({
validate: validateConfirmField,
}),
[validateConfirmField]
);
return ( return (
<> <>
@@ -82,7 +93,7 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
}} }}
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<InputField <InputField
className={className} className={classNames("column", className)}
readOnly={readOnly ?? formReadonly} readOnly={readOnly ?? formReadonly}
{...props} {...props}
{...field} {...field}
@@ -104,9 +115,9 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
control={control} control={control}
name={`${nameWithPrefix}Confirmation`} name={`${nameWithPrefix}Confirmation`}
defaultValue={defaultValue as never} defaultValue={defaultValue as never}
render={({ field, fieldState }) => ( render={({ field, fieldState: { error } }) => (
<InputField <InputField
className={className} className={classNames("column", className)}
type="password" type="password"
readOnly={readOnly ?? formReadonly} readOnly={readOnly ?? formReadonly}
disabled={props.disabled} disabled={props.disabled}
@@ -115,17 +126,12 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
label={confirmationLabelTranslation} label={confirmationLabelTranslation}
helpText={confirmationHelpTextTranslation} helpText={confirmationHelpTextTranslation}
error={ error={
fieldState.error error ? error.message || t(`${prefixedNameWithoutIndices}.confirmation.error.${error.type}`) : undefined
? fieldState.error.message ||
t(`${prefixedNameWithoutIndices}.confirmation.error.${fieldState.error.type}`)
: undefined
} }
testId={confirmationTestId ?? `input-${nameWithPrefix}-confirmation`} testId={confirmationTestId ?? `input-${nameWithPrefix}-confirmation`}
/> />
)} )}
rules={{ rules={confirmFieldRules}
validate: (value) => secretValue === value || confirmationErrorMessageTranslation,
}}
/> />
</> </>
); );

View File

@@ -35,8 +35,7 @@ type InputFieldProps = {
label: string; label: string;
helpText?: string; helpText?: string;
error?: string; error?: string;
type?: "text" | "password" | "email" | "tel"; } & React.ComponentProps<typeof Input>;
} & Omit<React.ComponentProps<typeof Input>, "type">;
/** /**
* @see https://bulma.io/documentation/form/input/ * @see https://bulma.io/documentation/form/input/

View File

@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
import SelectField from "./SelectField"; import SelectField from "./SelectField";
import { useScmFormPathContext } from "../FormPathContext"; import { useScmFormPathContext } from "../FormPathContext";
import { prefixWithoutIndices } from "../helpers"; import { prefixWithoutIndices } from "../helpers";
import classNames from "classnames";
type Props<T extends Record<string, unknown>> = Omit< type Props<T extends Record<string, unknown>> = Omit<
ComponentProps<typeof SelectField>, ComponentProps<typeof SelectField>,
@@ -46,6 +47,7 @@ function ControlledSelectField<T extends Record<string, unknown>>({
testId, testId,
defaultValue, defaultValue,
readOnly, readOnly,
className,
...props ...props
}: Props<T>) { }: Props<T>) {
const { control, t, readOnly: formReadonly, formId } = useScmFormContext(); const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
@@ -65,6 +67,7 @@ function ControlledSelectField<T extends Record<string, unknown>>({
form={formId} form={formId}
readOnly={readOnly ?? formReadonly} readOnly={readOnly ?? formReadonly}
required={rules?.required as boolean} required={rules?.required as boolean}
className={classNames("column", className)}
{...props} {...props}
{...field} {...field}
label={labelTranslation} label={labelTranslation}

View File

@@ -51,7 +51,7 @@ const SelectField = React.forwardRef<HTMLSelectElement, Props>(
{helpText ? <Help className="ml-1" text={helpText} /> : null} {helpText ? <Help className="ml-1" text={helpText} /> : null}
</Label> </Label>
<Control> <Control>
<Select id={selectId} variant={variant} ref={ref} {...props}></Select> <Select id={selectId} variant={variant} ref={ref} className="is-full-width" {...props}></Select>
</Control> </Control>
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null} {error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
</Field> </Field>

View File

@@ -300,3 +300,7 @@ $danger-25: scale-color($danger, $lightness: -75%);
.has-hover-visible:hover { .has-hover-visible:hover {
color: $grey !important; color: $grey !important;
} }
input[type="date"].input::-webkit-calendar-picker-indicator {
filter: invert(100%);
}

View File

@@ -372,3 +372,7 @@ td:first-child.diff-gutter-conflict:before {
border: 1px solid $info; border: 1px solid $info;
box-shadow: none; box-shadow: none;
} }
input[type="date"].input::-webkit-calendar-picker-indicator {
filter: invert(100%);
}

View File

@@ -1,6 +1,8 @@
{ {
"form": { "form": {
"submit": "Speichern", "submit": "Speichern",
"reset": "Leeren",
"discardChanges": "Änderungen verwerfen",
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
"table": { "table": {
"headers": { "headers": {

View File

@@ -1,6 +1,8 @@
{ {
"form": { "form": {
"submit": "Submit", "submit": "Submit",
"reset": "Clear",
"discardChanges": "Discard changes",
"submit-success-notification": "Configuration changed successfully!", "submit-success-notification": "Configuration changed successfully!",
"table": { "table": {
"headers": { "headers": {