mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
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:
committed by
SCM-Manager
parent
026ffa18fd
commit
b53f8bcf12
2
gradle/changelog/form_reset.yaml
Normal file
2
gradle/changelog/form_reset.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Optional reset button for forms
|
||||
@@ -32,7 +32,7 @@ type Result<C extends HalRepresentation> = {
|
||||
configuration: C;
|
||||
};
|
||||
|
||||
type MutationVariables<C extends HalRepresentation> = {
|
||||
type MutationVariables<C> = {
|
||||
configuration: C;
|
||||
contentType: string;
|
||||
link: string;
|
||||
@@ -53,8 +53,8 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
|
||||
error: mutationError,
|
||||
mutateAsync,
|
||||
data: updateResponse,
|
||||
} = useMutation<Response, Error, MutationVariables<C>>(
|
||||
(vars: MutationVariables<C>) => apiClient.put(vars.link, vars.configuration, vars.contentType),
|
||||
} = useMutation<Response, Error, MutationVariables<Omit<C, keyof HalRepresentation>>>(
|
||||
(vars) => apiClient.put(vars.link, vars.configuration, vars.contentType),
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(queryKey);
|
||||
@@ -65,7 +65,7 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
|
||||
const isReadOnly = !data?.configuration._links.update;
|
||||
|
||||
const update = useCallback(
|
||||
(configuration: C) => {
|
||||
(configuration: Omit<C, keyof HalRepresentation>) => {
|
||||
if (data && !isReadOnly) {
|
||||
return mutateAsync({
|
||||
configuration,
|
||||
|
||||
@@ -102,7 +102,7 @@ function AddListEntryForm<FormType extends Record<string, unknown>, DefaultValue
|
||||
return (
|
||||
<ScmFormContextProvider {...form} t={translateWithExtraPrefix} formId={nameWithPrefix}>
|
||||
<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}
|
||||
<div className="level-right">
|
||||
<Button
|
||||
|
||||
@@ -21,13 +21,18 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useConfigLink } from "@scm-manager/ui-api";
|
||||
import { Loading } from "@scm-manager/ui-components";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import Form from "./Form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props<T extends HalRepresentation> = Pick<ComponentProps<typeof Form<T, T>>, "translationPath" | "children"> & {
|
||||
type Props<T extends HalRepresentation> =
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
Omit<ComponentProps<typeof Form<T>>, "onSubmit" | "defaultValues" | "readOnly" | "successMessageFallback">
|
||||
& {
|
||||
link: string;
|
||||
};
|
||||
|
||||
@@ -35,20 +40,17 @@ type Props<T extends HalRepresentation> = Pick<ComponentProps<typeof Form<T, T>>
|
||||
* @beta
|
||||
* @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 [t] = useTranslation("commons", { keyPrefix: "form" });
|
||||
|
||||
if (isLoading || !initialConfiguration) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form<T, T>
|
||||
onSubmit={update}
|
||||
translationPath={translationPath}
|
||||
defaultValues={initialConfiguration}
|
||||
readOnly={isReadOnly}
|
||||
>
|
||||
<Form onSubmit={update} defaultValues={initialConfiguration} readOnly={isReadOnly}
|
||||
successMessageFallback={t("submit-success-notification")} {...formProps}>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Form from "./Form";
|
||||
import FormRow from "./FormRow";
|
||||
@@ -35,6 +35,7 @@ import ControlledColumn from "./table/ControlledColumn";
|
||||
import ControlledTable from "./table/ControlledTable";
|
||||
import AddListEntryForm from "./AddListEntryForm";
|
||||
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
|
||||
export type SimpleWebHookConfiguration = {
|
||||
urlPattern: string;
|
||||
@@ -62,7 +63,7 @@ storiesOf("Forms", module)
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
<ControlledInputField rules={{ required: true }} name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
@@ -79,6 +80,7 @@ storiesOf("Forms", module)
|
||||
defaultValues={{
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
passwordConfirmation: "",
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
@@ -93,6 +95,77 @@ storiesOf("Forms", module)
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Reset", () => {
|
||||
type FormType = {
|
||||
name: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
active: boolean;
|
||||
date: string;
|
||||
};
|
||||
const [defaultValues, setValues] = useState({
|
||||
name: "trillian",
|
||||
password: "secret",
|
||||
passwordConfirmation: "secret",
|
||||
active: true,
|
||||
date: "",
|
||||
});
|
||||
const resetValues = useRef({
|
||||
name: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
active: false,
|
||||
date: "",
|
||||
});
|
||||
return (
|
||||
<Form<FormType>
|
||||
onSubmit={(vals) => {
|
||||
console.log(vals);
|
||||
setValues(vals);
|
||||
}}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={defaultValues as HalRepresentation & FormType}
|
||||
withResetTo={resetValues.current}
|
||||
withDiscardChanges
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledSecretConfirmationField name="password" rules={{ required: true }} />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="active" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledInputField name="date" type="date" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
})
|
||||
.add("Layout", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
name: "",
|
||||
password: "",
|
||||
method: "",
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
<ControlledSelectField name="method">
|
||||
{["", "post", "get", "put"].map((value) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</ControlledSelectField>
|
||||
<ControlledSecretConfirmationField name="password" />
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("GlobalConfiguration", () => (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
@@ -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">
|
||||
<ControlledList withDelete>
|
||||
@@ -273,6 +376,7 @@ storiesOf("Forms", module)
|
||||
<ControlledColumn name="concealed">{(value) => (value ? <b>Hallo</b> : null)}</ControlledColumn>
|
||||
</ControlledTable>
|
||||
<AddListEntryForm defaultValues={{ key: "", value: "", concealed: false }}>
|
||||
<FormRow>
|
||||
<ControlledInputField
|
||||
name="key"
|
||||
rules={{
|
||||
@@ -281,8 +385,13 @@ storiesOf("Forms", module)
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledInputField name="value" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledCheckboxField name="concealed" />
|
||||
</FormRow>
|
||||
</AddListEntryForm>
|
||||
</ScmFormListContextProvider>
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,19 @@ 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 { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import styled from "styled-components";
|
||||
import { setValues } from "./helpers";
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||
UseFormReturn<T>,
|
||||
"register" | "unregister" | "handleSubmit" | "control"
|
||||
>;
|
||||
|
||||
const ButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
`;
|
||||
|
||||
const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label, hide }) => {
|
||||
if (!label) {
|
||||
return null;
|
||||
@@ -52,28 +58,60 @@ type Props<FormType extends Record<string, unknown>, DefaultValues extends FormT
|
||||
children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
|
||||
translationPath: [namespace: string, prefix: string];
|
||||
onSubmit: SubmitHandler<FormType>;
|
||||
defaultValues: Omit<DefaultValues, keyof HalRepresentation>;
|
||||
defaultValues: DefaultValues;
|
||||
readOnly?: boolean;
|
||||
submitButtonTestId?: string;
|
||||
/**
|
||||
* Renders a button which resets the form to its default.
|
||||
* This reflects the default browser behavior for a form *reset*.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
withDiscardChanges?: boolean;
|
||||
/**
|
||||
* Renders a button which acts as if a user manually updated all fields to supplied values.
|
||||
* The default use-case for this is to clear forms and this is also how the button is labelled.
|
||||
* You can also use it to reset the form to an original state, but it is then advised to change the button label
|
||||
* to *Reset to Defaults* by defining the *reset* translation in the form's translation object's root.
|
||||
*
|
||||
* > *Important Note:* This mechanism cannot be used to change the number of items in lists,
|
||||
* > neither on the root level nor nested.
|
||||
* > It is therefore advised not to use this property when lists or nested forms are involved.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
withResetTo?: DefaultValues;
|
||||
/**
|
||||
* Message to display after a successful submit if no translation key is defined.
|
||||
*
|
||||
* If this is not supplied and the root level `submit-success-notification` translation key is not set,
|
||||
* no message is displayed at all.
|
||||
*
|
||||
* @since 2.43.0
|
||||
*/
|
||||
successMessageFallback?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
*/
|
||||
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType>({
|
||||
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType = FormType>({
|
||||
children,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
translationPath,
|
||||
readOnly,
|
||||
withResetTo,
|
||||
withDiscardChanges,
|
||||
successMessageFallback,
|
||||
submitButtonTestId,
|
||||
}: Props<FormType, DefaultValues>) {
|
||||
const form = useForm<FormType>({
|
||||
mode: "onChange",
|
||||
defaultValues: defaultValues as DeepPartial<FormType>,
|
||||
});
|
||||
const { formState, handleSubmit, reset } = form;
|
||||
const { formState, handleSubmit, reset, setValue } = form;
|
||||
const [ns, prefix] = translationPath;
|
||||
const { t } = useTranslation(ns, { keyPrefix: prefix });
|
||||
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 [showSuccessNotification, setShowSuccessNotification] = useState(false);
|
||||
const submitButtonLabel = t("submit", { defaultValue: defaultTranslate("submit") });
|
||||
const resetButtonLabel = t("reset", { defaultValue: defaultTranslate("reset") });
|
||||
const discardChangesButtonLabel = t("discardChanges", { defaultValue: defaultTranslate("discardChanges") });
|
||||
const successNotification = translateWithFallback("submit-success-notification", {
|
||||
defaultValue: 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/
|
||||
useEffect(() => {
|
||||
@@ -128,7 +173,7 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<SuccessNotification label={successNotification} hide={() => setShowSuccessNotification(false)} />
|
||||
) : null}
|
||||
@@ -137,6 +182,7 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
{!readOnly ? (
|
||||
<Level
|
||||
right={
|
||||
<ButtonsContainer>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
@@ -147,6 +193,17 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
>
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
{withDiscardChanges ? (
|
||||
<Button type="reset" form={prefix} testId={`${prefix}-discard-changes-button`}>
|
||||
{discardChangesButtonLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
{withResetTo ? (
|
||||
<Button form={prefix} onClick={overwriteValues} testId={`${prefix}-reset-button`}>
|
||||
{resetButtonLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
</ButtonsContainer>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -24,26 +24,13 @@
|
||||
|
||||
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 : (
|
||||
<FormRowDiv ref={ref} className={classNames("is-flex is-flex-wrap-wrap", className)} {...rest}>
|
||||
<div ref={ref} className={classNames("columns", className)} {...rest}>
|
||||
{children}
|
||||
</FormRowDiv>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
|
||||
import CheckboxField from "./CheckboxField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof CheckboxField>,
|
||||
@@ -46,6 +47,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
testId,
|
||||
defaultChecked,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
@@ -64,7 +66,8 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
<CheckboxField
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
defaultChecked={field.value}
|
||||
checked={field.value}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
|
||||
@@ -22,6 +22,30 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
export function prefixWithoutIndices(path: string): string {
|
||||
return path.replace(/(\.\d+)/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like {@link setValue} but recursively applies the whole {@link newValues} object.
|
||||
*
|
||||
* > *Important Note:* This deeply overwrites input values of fields in arrays,
|
||||
* > but does **NOT** add or remove items to existing arrays.
|
||||
* > This can therefore not be used to clear lists.
|
||||
*/
|
||||
export function setValues<T>(newValues: T, setValue: UseFormReturn<T>["setValue"], path = "") {
|
||||
for (const [key, val] of Object.entries(newValues)) {
|
||||
if (val !== null && typeof val === "object") {
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((subVal, idx) => setValues(subVal, setValue, path ? `${path}.${key}.${idx}` : `${key}.${idx}`));
|
||||
} else {
|
||||
setValues(val, setValue, path ? `${path}.${key}` : key);
|
||||
}
|
||||
} else {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
setValue(fullPath as any, val, { shouldValidate: !fullPath.endsWith("Confirmation"), shouldDirty: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof InputField>,
|
||||
@@ -46,6 +47,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
@@ -64,6 +66,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
||||
<InputField
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
form={formId}
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import React, { ComponentProps, useCallback, useMemo } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import InputField from "./InputField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof InputField>,
|
||||
@@ -69,6 +70,16 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
|
||||
const confirmationErrorMessageTranslation =
|
||||
confirmationErrorMessage || t(`${prefixedNameWithoutIndices}.confirmation.errorMessage`);
|
||||
const secretValue = watch(nameWithPrefix);
|
||||
const validateConfirmField = useCallback(
|
||||
(value) => secretValue === value || confirmationErrorMessageTranslation,
|
||||
[confirmationErrorMessageTranslation, secretValue]
|
||||
);
|
||||
const confirmFieldRules = useMemo(
|
||||
() => ({
|
||||
validate: validateConfirmField,
|
||||
}),
|
||||
[validateConfirmField]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,7 +93,7 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<InputField
|
||||
className={className}
|
||||
className={classNames("column", className)}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
{...props}
|
||||
{...field}
|
||||
@@ -104,9 +115,9 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
|
||||
control={control}
|
||||
name={`${nameWithPrefix}Confirmation`}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<InputField
|
||||
className={className}
|
||||
className={classNames("column", className)}
|
||||
type="password"
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
disabled={props.disabled}
|
||||
@@ -115,17 +126,12 @@ export default function ControlledSecretConfirmationField<T extends Record<strin
|
||||
label={confirmationLabelTranslation}
|
||||
helpText={confirmationHelpTextTranslation}
|
||||
error={
|
||||
fieldState.error
|
||||
? fieldState.error.message ||
|
||||
t(`${prefixedNameWithoutIndices}.confirmation.error.${fieldState.error.type}`)
|
||||
: undefined
|
||||
error ? error.message || t(`${prefixedNameWithoutIndices}.confirmation.error.${error.type}`) : undefined
|
||||
}
|
||||
testId={confirmationTestId ?? `input-${nameWithPrefix}-confirmation`}
|
||||
/>
|
||||
)}
|
||||
rules={{
|
||||
validate: (value) => secretValue === value || confirmationErrorMessageTranslation,
|
||||
}}
|
||||
rules={confirmFieldRules}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -35,8 +35,7 @@ type InputFieldProps = {
|
||||
label: string;
|
||||
helpText?: string;
|
||||
error?: string;
|
||||
type?: "text" | "password" | "email" | "tel";
|
||||
} & Omit<React.ComponentProps<typeof Input>, "type">;
|
||||
} & React.ComponentProps<typeof Input>;
|
||||
|
||||
/**
|
||||
* @see https://bulma.io/documentation/form/input/
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useScmFormContext } from "../ScmFormContext";
|
||||
import SelectField from "./SelectField";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof SelectField>,
|
||||
@@ -46,6 +47,7 @@ function ControlledSelectField<T extends Record<string, unknown>>({
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
className,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||
@@ -65,6 +67,7 @@ function ControlledSelectField<T extends Record<string, unknown>>({
|
||||
form={formId}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
className={classNames("column", className)}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
|
||||
@@ -51,7 +51,7 @@ const SelectField = React.forwardRef<HTMLSelectElement, Props>(
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<Control>
|
||||
<Select id={selectId} variant={variant} ref={ref} {...props}></Select>
|
||||
<Select id={selectId} variant={variant} ref={ref} className="is-full-width" {...props}></Select>
|
||||
</Control>
|
||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||
</Field>
|
||||
|
||||
@@ -300,3 +300,7 @@ $danger-25: scale-color($danger, $lightness: -75%);
|
||||
.has-hover-visible:hover {
|
||||
color: $grey !important;
|
||||
}
|
||||
|
||||
input[type="date"].input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
@@ -372,3 +372,7 @@ td:first-child.diff-gutter-conflict:before {
|
||||
border: 1px solid $info;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type="date"].input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"form": {
|
||||
"submit": "Speichern",
|
||||
"reset": "Leeren",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"form": {
|
||||
"submit": "Submit",
|
||||
"reset": "Clear",
|
||||
"discardChanges": "Discard changes",
|
||||
"submit-success-notification": "Configuration changed successfully!",
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
Reference in New Issue
Block a user