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 = {
26 type ExtraFieldError<T extends ExtraFieldType> = FormikErrors<UnsafeItemExtraField<T>>;
28 export type ExtraFieldProps = FieldBoxProps &
29 Omit<BaseTextFieldProps & BaseTextAreaFieldProps, 'field' | 'placeholder' | 'error'> & {
31 field: FieldInputProps<UnsafeItemExtraField>;
32 error?: ExtraFieldError<ExtraFieldType>;
39 export const getExtraFieldOptions = (): Record<ExtraFieldType, ExtraFieldOption> => ({
41 icon: 'text-align-left',
42 label: c('Label').t`Text`,
43 placeholder: c('Placeholder').t`Add text`,
47 label: c('Label').t`2FA secret key (TOTP)`,
48 placeholder: c('Placeholder').t`Add 2FA secret key`,
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`,
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} />
67 export const ExtraFieldComponent: FC<ExtraFieldProps> = ({
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));
86 const fieldValueEmpty = Object.values(field.value.data).every((value) => !value);
90 actions={[<DeleteButton onDelete={onDelete} />]}
92 icon={showIcon ? icon : undefined}
97 !fieldValueEmpty && 'color-weak',
98 touched && error?.fieldName && 'placeholder-danger'
100 placeholder={c('Label').t`Field name`}
101 autoFocus={autoFocus}
104 value: field.value.fieldName,
105 onChange: onChangeHandler((evt, values) => partialMerge(values, { fieldName: evt.target.value })),
111 switch (field.value.type) {
114 const FieldComponent =
115 field.value.type === 'hidden' ? BaseMaskedTextAreaField : BaseTextAreaField;
116 const fieldError = error as ExtraFieldError<'text' | 'hidden'>;
119 placeholder={placeholder}
120 error={touched && (error?.fieldName || fieldError?.data?.content)}
123 value: field.value.data.content,
124 onChange: onChangeHandler((evt, values) =>
125 partialMerge(values, { data: { content: evt.target.value } })
133 const fieldError = error as ExtraFieldError<'totp'>;
137 placeholder={placeholder}
138 error={touched && (error?.fieldName || fieldError?.data?.totpUri)}
141 value: field.value.data.totpUri,
142 onChange: onChangeHandler((evt, values) =>
143 partialMerge(values, { data: { totpUri: evt.target.value } })