Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / components / Form / Field / ExtraFieldGroup / ExtraField.tsx
blob11a1c2ed20e0e5d05deece2521ca8a950aa36284
1 import type { ChangeEvent, FC } from 'react';
3 import type { FieldInputProps, FormikErrors } from 'formik';
4 import { c } from 'ttag';
6 import { Button, type ButtonProps } from '@proton/atoms';
7 import type { IconName } from '@proton/components';
8 import { Icon } from '@proton/components';
9 import type { ExtraFieldType, UnsafeItemExtraField } from '@proton/pass/types';
10 import { partialMerge } from '@proton/pass/utils/object/merge';
11 import clsx from '@proton/utils/clsx';
13 import type { FieldBoxProps } from '../Layout/FieldBox';
14 import { FieldBox } from '../Layout/FieldBox';
15 import type { BaseTextFieldProps } from '../TextField';
16 import { BaseTextField } from '../TextField';
17 import type { BaseTextAreaFieldProps } from '../TextareaField';
18 import { BaseMaskedTextAreaField, BaseTextAreaField } from '../TextareaField';
20 type ExtraFieldOption = {
21     icon: IconName;
22     label: string;
23     placeholder: string;
26 type ExtraFieldError<T extends ExtraFieldType> = FormikErrors<UnsafeItemExtraField<T>>;
28 export type ExtraFieldProps = FieldBoxProps &
29     Omit<BaseTextFieldProps & BaseTextAreaFieldProps, 'field' | 'placeholder' | 'error'> & {
30         type: ExtraFieldType;
31         field: FieldInputProps<UnsafeItemExtraField>;
32         error?: ExtraFieldError<ExtraFieldType>;
33         touched?: boolean;
34         showIcon?: boolean;
35         autoFocus?: boolean;
36         onDelete: () => void;
37     };
39 export const getExtraFieldOptions = (): Record<ExtraFieldType, ExtraFieldOption> => ({
40     text: {
41         icon: 'text-align-left',
42         label: c('Label').t`Text`,
43         placeholder: c('Placeholder').t`Add text`,
44     },
45     totp: {
46         icon: 'lock',
47         label: c('Label').t`2FA secret key (TOTP)`,
48         placeholder: c('Placeholder').t`Add 2FA secret key`,
49     },
50     hidden: {
51         icon: 'eye-slash',
52         // translator: label for a field that is hidden. Singular only.
53         label: c('Label').t`Hidden`,
54         placeholder: c('Placeholder').t`Add hidden text`,
55     },
56 });
58 export const getExtraFieldOption = (type: ExtraFieldType) => getExtraFieldOptions()[type];
60 type DeleteButtonProps = ButtonProps & { onDelete: () => void };
61 export const DeleteButton: FC<DeleteButtonProps> = ({ onDelete, size = 'medium' }) => (
62     <Button icon pill color="weak" onClick={onDelete} shape="solid" size={size} title={c('Action').t`Delete`}>
63         <Icon name="cross" size={5} />
64     </Button>
67 export const ExtraFieldComponent: FC<ExtraFieldProps> = ({
68     autoFocus,
69     className,
70     error,
71     field,
72     onDelete,
73     showIcon = true,
74     touched,
75     type,
76     ...rest
77 }) => {
78     const { icon, placeholder } = getExtraFieldOption(type);
80     const onChangeHandler =
81         (merge: (evt: ChangeEvent<HTMLInputElement>, field: UnsafeItemExtraField) => UnsafeItemExtraField) =>
82         (evt: ChangeEvent<HTMLInputElement>) => {
83             void rest.form.setFieldValue(field.name, merge(evt, field.value));
84         };
86     const fieldValueEmpty = Object.values(field.value.data).every((value) => !value);
88     return (
89         <FieldBox
90             actions={[<DeleteButton onDelete={onDelete} />]}
91             className={className}
92             icon={showIcon ? icon : undefined}
93         >
94             <BaseTextField
95                 inputClassName={clsx(
96                     'text-sm',
97                     !fieldValueEmpty && 'color-weak',
98                     touched && error?.fieldName && 'placeholder-danger'
99                 )}
100                 placeholder={c('Label').t`Field name`}
101                 autoFocus={autoFocus}
102                 field={{
103                     ...field,
104                     value: field.value.fieldName,
105                     onChange: onChangeHandler((evt, values) => partialMerge(values, { fieldName: evt.target.value })),
106                 }}
107                 {...rest}
108             />
110             {(() => {
111                 switch (field.value.type) {
112                     case 'text':
113                     case 'hidden': {
114                         const FieldComponent =
115                             field.value.type === 'hidden' ? BaseMaskedTextAreaField : BaseTextAreaField;
116                         const fieldError = error as ExtraFieldError<'text' | 'hidden'>;
117                         return (
118                             <FieldComponent
119                                 placeholder={placeholder}
120                                 error={touched && (error?.fieldName || fieldError?.data?.content)}
121                                 field={{
122                                     ...field,
123                                     value: field.value.data.content,
124                                     onChange: onChangeHandler((evt, values) =>
125                                         partialMerge(values, { data: { content: evt.target.value } })
126                                     ),
127                                 }}
128                                 {...rest}
129                             />
130                         );
131                     }
132                     case 'totp': {
133                         const fieldError = error as ExtraFieldError<'totp'>;
134                         return (
135                             <BaseTextField
136                                 hidden
137                                 placeholder={placeholder}
138                                 error={touched && (error?.fieldName || fieldError?.data?.totpUri)}
139                                 field={{
140                                     ...field,
141                                     value: field.value.data.totpUri,
142                                     onChange: onChangeHandler((evt, values) =>
143                                         partialMerge(values, { data: { totpUri: evt.target.value } })
144                                     ),
145                                 }}
146                                 {...rest}
147                             />
148                         );
149                     }
150                 }
151             })()}
152         </FieldBox>
153     );