From a98fb70370820d0e5e2a5b40936767c102dc9306 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Mon, 20 Jan 2025 14:32:23 -0300 Subject: [PATCH 01/44] Add multiline paste event to input --- .../components/Input/index.tsx | 19 +++++++++++++++++++ .../SelectFieldOption.tsx | 9 +++++++++ .../ConfigSelectWithLocaleSwitcher/index.tsx | 5 ++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/front/app/component-library/components/Input/index.tsx b/front/app/component-library/components/Input/index.tsx index 6e331d3fb5f7..65b1346f3399 100644 --- a/front/app/component-library/components/Input/index.tsx +++ b/front/app/component-library/components/Input/index.tsx @@ -80,6 +80,7 @@ export interface InputProps { onBlur?: (arg: FormEvent) => void; setRef?: (arg: HTMLInputElement) => void | undefined; onKeyDown?: (event: KeyboardEvent) => void; + onMultilinePaste?: (lines: string[]) => void; autoFocus?: boolean; min?: string; max?: string; @@ -154,6 +155,8 @@ class Input extends PureComponent { autocomplete, size = 'medium', 'data-testid': dataTestId, + onChange, + onMultilinePaste, } = this.props; const hasError = !isNil(this.props.error) && !isEmpty(this.props.error); const optionalProps = isBoolean(spellCheck) ? { spellCheck } : null; @@ -204,6 +207,22 @@ class Input extends PureComponent { required={required} autoComplete={autocomplete} onKeyDown={onKeyDown} + onPaste={ + onMultilinePaste + ? (e) => { + e.preventDefault(); + navigator.clipboard.readText().then((text) => { + const split = text.split('\n'); + + if (split.length === 1) { + onChange?.(split[0], this.props.locale); + } else { + onMultilinePaste(split); + } + }); + } + : undefined + } {...optionalProps} /> diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/SelectFieldOption.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/SelectFieldOption.tsx index f4ed6d4e5adf..f92ac9f82026 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/SelectFieldOption.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/SelectFieldOption.tsx @@ -30,6 +30,7 @@ interface Props { locale: SupportedLocale; removeOption: (index: number) => void; onChoiceUpdate: (choice: IOptionsType, index: number) => void; + onMultilinePaste?: (lines: string[], index: number) => void; optionImages: OptionImageType | undefined; } @@ -42,6 +43,7 @@ const SelectFieldOption = memo( locale, removeOption, onChoiceUpdate, + onMultilinePaste, optionImages, }: Props) => { const [isUploading, setIsUploading] = useState(false); @@ -107,6 +109,13 @@ const SelectFieldOption = memo( onChoiceUpdate(choice, index); }} autoFocus={false} + onMultilinePaste={ + onMultilinePaste + ? (lines) => { + onMultilinePaste(lines, index); + } + : undefined + } /> {showImageSettings && ( diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx index 4a8e4be27d04..9c8580984b95 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx @@ -311,9 +311,12 @@ const ConfigSelectWithLocaleSwitcher = ({ locale={selectedLocale} inputType={inputType} canDeleteLastOption={canDeleteLastOption} + optionImages={optionImages} removeOption={removeOption} onChoiceUpdate={updateChoice} - optionImages={optionImages} + onMultilinePaste={(lines, index) => { + console.log({ lines, index }); + }} /> )} From 23c47429be7c2a7ca6e052f8827435de39c42a47 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Mon, 20 Jan 2025 20:51:59 -0300 Subject: [PATCH 02/44] Add allowMultilinePaste check + test --- .../ConfigSelectWithLocaleSwitcher/index.tsx | 18 ++++++-- .../utils.test.ts | 25 +++++++++++ .../ConfigSelectWithLocaleSwitcher/utils.ts | 44 +++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts create mode 100644 front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx index 9c8580984b95..40c6896a1f46 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx @@ -194,6 +194,13 @@ const ConfigSelectWithLocaleSwitcher = ({ [update] ); + const handleMultilinePaste = useCallback( + (lines, index) => { + console.log({ lines, index }); + }, + [update] + ); + const defaultOptionValues = [{}]; const errors = get(formContextErrors, name) as RHFErrors; const apiError = errors?.error && ([errors] as CLError[]); @@ -276,6 +283,9 @@ const ConfigSelectWithLocaleSwitcher = ({ return aValue - bValue; }) .map((choice, index) => { + const isLast = index === choices.length - 1; + const isEmpty = !!choice.title_multiloc[selectedLocale]; + return ( {choice.other === true ? ( @@ -314,9 +324,11 @@ const ConfigSelectWithLocaleSwitcher = ({ optionImages={optionImages} removeOption={removeOption} onChoiceUpdate={updateChoice} - onMultilinePaste={(lines, index) => { - console.log({ lines, index }); - }} + onMultilinePaste={ + isLast && isEmpty + ? handleMultilinePaste + : undefined + } /> )} diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts new file mode 100644 index 000000000000..4a796e5c3f4e --- /dev/null +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts @@ -0,0 +1,25 @@ +import { allowMultilinePaste } from './utils'; + +describe('allowMultilinePaste', () => { + it('should return true if all options from the given index are empty', () => { + const options = [ + { title_multiloc: { en: 'Text' } }, + { title_multiloc: { en: '' } }, + { title_multiloc: { en: undefined } }, + ]; + + const result = allowMultilinePaste({ options, index: 1, locale: 'en' }); + expect(result).toBe(true); + }); + + it('should return false if not all options from the given index are empty', () => { + const options = [ + { title_multiloc: { en: 'Text' } }, + { title_multiloc: { en: '' } }, + { title_multiloc: { en: 'Something' } }, + ]; + + const result = allowMultilinePaste({ options, index: 1, locale: 'en' }); + expect(result).toBe(false); + }); +}); diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts new file mode 100644 index 000000000000..23dd6b8f7feb --- /dev/null +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts @@ -0,0 +1,44 @@ +import { SupportedLocale } from 'typings'; + +import { IOptionsType } from 'api/custom_fields/types'; + +interface AllowMultilinePasteParams { + options: IOptionsType[]; + index: number; + locale: SupportedLocale; +} + +// Ensure that for the given locale, +// the option at the given index and all +// subsequent indices are empty +export const allowMultilinePaste = ({ + options, + index, + locale, +}: AllowMultilinePasteParams) => { + for (let i = index; i < options.length; i++) { + if (options[i].title_multiloc[locale]) { + return false; + } + } + + return true; +}; + +// interface UpdateFormOnMultilinePasteParams { +// update: (index: number, newOption: IOptionsType) => void; +// locale: SupportedLocale; +// lines: string[]; +// index: number; +// options: IOptionsType[]; +// } + +// export const updateFormOnMultlinePaste = ({ +// update, +// locale, +// lines, +// index, +// options +// }: UpdateFormOnMultilinePasteParams) => { + +// } From 763e1bf56cbf28d7e75823ff38186de561dca176 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Mon, 20 Jan 2025 21:10:26 -0300 Subject: [PATCH 03/44] Implement logic to paste multilines --- .../ConfigSelectWithLocaleSwitcher/index.tsx | 23 +++++-- .../ConfigSelectWithLocaleSwitcher/utils.ts | 60 +++++++++++++------ 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx index 40c6896a1f46..c15eb1640028 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx @@ -36,6 +36,7 @@ import { isNilOrError } from 'utils/helperUtils'; import messages from './messages'; import SelectFieldOption, { OptionImageType } from './SelectFieldOption'; +import { allowMultilinePaste, updateFormOnMultlinePaste } from './utils'; interface Props { name: string; @@ -196,9 +197,18 @@ const ConfigSelectWithLocaleSwitcher = ({ const handleMultilinePaste = useCallback( (lines, index) => { - console.log({ lines, index }); + if (!selectedLocale) return; + + updateFormOnMultlinePaste({ + update, + append, + locale: selectedLocale, + lines, + index, + options: selectOptions, + }); }, - [update] + [update, append, selectOptions, selectedLocale] ); const defaultOptionValues = [{}]; @@ -283,8 +293,11 @@ const ConfigSelectWithLocaleSwitcher = ({ return aValue - bValue; }) .map((choice, index) => { - const isLast = index === choices.length - 1; - const isEmpty = !!choice.title_multiloc[selectedLocale]; + const multilinePasteAllowed = allowMultilinePaste({ + options, + index, + locale: selectedLocale, + }); return ( @@ -325,7 +338,7 @@ const ConfigSelectWithLocaleSwitcher = ({ removeOption={removeOption} onChoiceUpdate={updateChoice} onMultilinePaste={ - isLast && isEmpty + multilinePasteAllowed ? handleMultilinePaste : undefined } diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts index 23dd6b8f7feb..7cfc87400c7b 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts @@ -2,6 +2,8 @@ import { SupportedLocale } from 'typings'; import { IOptionsType } from 'api/custom_fields/types'; +import { generateTempId } from 'components/FormBuilder/utils'; + interface AllowMultilinePasteParams { options: IOptionsType[]; index: number; @@ -25,20 +27,44 @@ export const allowMultilinePaste = ({ return true; }; -// interface UpdateFormOnMultilinePasteParams { -// update: (index: number, newOption: IOptionsType) => void; -// locale: SupportedLocale; -// lines: string[]; -// index: number; -// options: IOptionsType[]; -// } - -// export const updateFormOnMultlinePaste = ({ -// update, -// locale, -// lines, -// index, -// options -// }: UpdateFormOnMultilinePasteParams) => { - -// } +interface UpdateFormOnMultilinePasteParams { + update: (index: number, newOption: IOptionsType) => void; + append: (newOption: IOptionsType) => void; + locale: SupportedLocale; + lines: string[]; + index: number; + options: IOptionsType[]; +} + +export const updateFormOnMultlinePaste = ({ + update, + append, + locale, + lines, + index, + options, +}: UpdateFormOnMultilinePasteParams) => { + lines.forEach((line, i) => { + const optionIndex = index + i; + const option = + optionIndex >= options.length ? undefined : options[optionIndex]; + + if (option) { + update(optionIndex, { + ...option, + title_multiloc: { + ...option.title_multiloc, + [locale]: line, + }, + ...(!option.id && !option.temp_id ? { temp_id: generateTempId() } : {}), + }); + } else { + append({ + title_multiloc: { + [locale]: line, + }, + temp_id: generateTempId(), + }); + } + }); +}; From 0a74aa5677726d0048e6eb7926877d72489f9f93 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Mon, 20 Jan 2025 21:16:39 -0300 Subject: [PATCH 04/44] Sanitize lines on paste --- .../ConfigSelectWithLocaleSwitcher/utils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts index 7cfc87400c7b..32021af25fbb 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts @@ -54,17 +54,25 @@ export const updateFormOnMultlinePaste = ({ ...option, title_multiloc: { ...option.title_multiloc, - [locale]: line, + [locale]: sanitizeLine(line), }, ...(!option.id && !option.temp_id ? { temp_id: generateTempId() } : {}), }); } else { append({ title_multiloc: { - [locale]: line, + [locale]: sanitizeLine(line), }, temp_id: generateTempId(), }); } }); }; + +const sanitizeLine = (line: string) => { + const trimmedLine = line.trim(); + const cleanedLine = trimmedLine.startsWith('-') + ? trimmedLine.slice(1) + : trimmedLine; + return cleanedLine.trim(); +}; From bdf5179ffeba057d201e29f14f320987bfe47db2 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Mon, 20 Jan 2025 21:37:15 -0300 Subject: [PATCH 05/44] Sanitize pasted lines --- .../ConfigSelectWithLocaleSwitcher/index.tsx | 3 +- .../utils.test.ts | 38 ++++++++++++++++++- .../ConfigSelectWithLocaleSwitcher/utils.ts | 6 ++- .../FieldTypeSwitcher/index.tsx | 3 +- .../components/FormBuilderToolbox/index.tsx | 7 +--- .../components/FormFields/FormField/index.tsx | 2 +- front/app/components/FormBuilder/utils.tsx | 5 --- .../components/HookForm/OptionList/index.tsx | 3 +- .../Fields/FieldSelectionModal/index.tsx | 2 +- front/app/utils/helperUtils.ts | 5 +++ 10 files changed, 53 insertions(+), 21 deletions(-) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx index c15eb1640028..83c6f7047307 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx @@ -27,12 +27,11 @@ import usePrevious from 'hooks/usePrevious'; import { List, Row } from 'components/admin/ResourceList'; import SortableRow from 'components/admin/ResourceList/SortableRow'; import { SectionField } from 'components/admin/Section'; -import { generateTempId } from 'components/FormBuilder/utils'; import Error, { TFieldName } from 'components/UI/Error'; import { useIntl } from 'utils/cl-intl'; import { convertUrlToUploadFile } from 'utils/fileUtils'; -import { isNilOrError } from 'utils/helperUtils'; +import { generateTempId, isNilOrError } from 'utils/helperUtils'; import messages from './messages'; import SelectFieldOption, { OptionImageType } from './SelectFieldOption'; diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts index 4a796e5c3f4e..b688751c8c17 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.test.ts @@ -1,4 +1,4 @@ -import { allowMultilinePaste } from './utils'; +import { allowMultilinePaste, updateFormOnMultlinePaste } from './utils'; describe('allowMultilinePaste', () => { it('should return true if all options from the given index are empty', () => { @@ -23,3 +23,39 @@ describe('allowMultilinePaste', () => { expect(result).toBe(false); }); }); + +describe('updateFormOnMultlinePaste', () => { + it('correctly sanitizes line with summarization points', () => { + const lines = ['• One', '• Two', '• Three']; + + const options = [{ title_multiloc: { en: '' } }]; + + const update = jest.fn(); + const append = jest.fn(); + + updateFormOnMultlinePaste({ + update, + append, + locale: 'en', + lines, + index: 0, + options, + }); + + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith(0, { + title_multiloc: { + en: 'One', + }, + temp_id: expect.any(String), + }); + + expect(append).toHaveBeenCalledTimes(2); + expect(append).toHaveBeenCalledWith({ + title_multiloc: { + en: 'Two', + }, + temp_id: expect.any(String), + }); + }); +}); diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts index 32021af25fbb..b9b57d47b7c0 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/utils.ts @@ -2,7 +2,7 @@ import { SupportedLocale } from 'typings'; import { IOptionsType } from 'api/custom_fields/types'; -import { generateTempId } from 'components/FormBuilder/utils'; +import { generateTempId } from 'utils/helperUtils'; interface AllowMultilinePasteParams { options: IOptionsType[]; @@ -69,9 +69,11 @@ export const updateFormOnMultlinePaste = ({ }); }; +const REMOVABLE_PREFIXES = new Set(['•', '-']); + const sanitizeLine = (line: string) => { const trimmedLine = line.trim(); - const cleanedLine = trimmedLine.startsWith('-') + const cleanedLine = REMOVABLE_PREFIXES.has(trimmedLine[0]) ? trimmedLine.slice(1) : trimmedLine; return cleanedLine.trim(); diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/FieldTypeSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/FieldTypeSwitcher/index.tsx index 7109d2e38001..9fd82514de32 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/FieldTypeSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/FieldTypeSwitcher/index.tsx @@ -5,9 +5,8 @@ import { useFormContext } from 'react-hook-form'; import { IFlatCustomFieldWithIndex } from 'api/custom_fields/types'; -import { generateTempId } from 'components/FormBuilder/utils'; - import { useIntl } from 'utils/cl-intl'; +import { generateTempId } from 'utils/helperUtils'; import messages from './messages'; import { getFieldSwitchOptions } from './utils'; diff --git a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx index 7d84de692620..d5f8457bfda8 100644 --- a/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderToolbox/index.tsx @@ -19,13 +19,10 @@ import { import useFeatureFlag from 'hooks/useFeatureFlag'; import useLocale from 'hooks/useLocale'; -import { - generateTempId, - FormBuilderConfig, -} from 'components/FormBuilder/utils'; +import { FormBuilderConfig } from 'components/FormBuilder/utils'; import { FormattedMessage, useIntl } from 'utils/cl-intl'; -import { isNilOrError } from 'utils/helperUtils'; +import { generateTempId, isNilOrError } from 'utils/helperUtils'; import messages from '../messages'; diff --git a/front/app/components/FormBuilder/components/FormFields/FormField/index.tsx b/front/app/components/FormBuilder/components/FormFields/FormField/index.tsx index 27d9af7457bd..3d0f03147e31 100644 --- a/front/app/components/FormBuilder/components/FormFields/FormField/index.tsx +++ b/front/app/components/FormBuilder/components/FormFields/FormField/index.tsx @@ -22,12 +22,12 @@ import useDuplicateMapConfig from 'api/map_config/useDuplicateMapConfig'; import { FormBuilderConfig, builtInFieldKeys, - generateTempId, } from 'components/FormBuilder/utils'; import Modal from 'components/UI/Modal'; import MoreActionsMenu from 'components/UI/MoreActionsMenu'; import { useIntl } from 'utils/cl-intl'; +import { generateTempId } from 'utils/helperUtils'; import { FlexibleRow } from '../../FlexibleRow'; import { getFieldBackgroundColor } from '../utils'; diff --git a/front/app/components/FormBuilder/utils.tsx b/front/app/components/FormBuilder/utils.tsx index 8bb8a4c7dba4..a865a8e4bfff 100644 --- a/front/app/components/FormBuilder/utils.tsx +++ b/front/app/components/FormBuilder/utils.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { uuid4 } from '@sentry/utils'; import { MessageDescriptor } from 'react-intl'; import { RouteType } from 'routes'; import { SupportedLocale } from 'typings'; @@ -81,10 +80,6 @@ export const getIsPostingEnabled = ( return false; }; -export function generateTempId() { - return `TEMP-ID-${uuid4()}`; -} - // TODO: BE key for survey end options should be replaced with form_end, then we can update this value. export const formEndOption = 'survey_end'; diff --git a/front/app/components/HookForm/OptionList/index.tsx b/front/app/components/HookForm/OptionList/index.tsx index 4e84802e29e6..9979ac761e8b 100644 --- a/front/app/components/HookForm/OptionList/index.tsx +++ b/front/app/components/HookForm/OptionList/index.tsx @@ -19,10 +19,9 @@ import { IOptionsType } from 'api/custom_fields/types'; import { List } from 'components/admin/ResourceList'; import SortableRow from 'components/admin/ResourceList/SortableRow'; import { SectionField } from 'components/admin/Section'; -import { generateTempId } from 'components/FormBuilder/utils'; import Error, { TFieldName } from 'components/UI/Error'; -import { isNilOrError } from 'utils/helperUtils'; +import { isNilOrError, generateTempId } from 'utils/helperUtils'; export type Option = { id?: string; diff --git a/front/app/components/admin/ActionForm/Fields/FieldSelectionModal/index.tsx b/front/app/components/admin/ActionForm/Fields/FieldSelectionModal/index.tsx index 7d7de4f3cb2a..e81c3d5d8417 100644 --- a/front/app/components/admin/ActionForm/Fields/FieldSelectionModal/index.tsx +++ b/front/app/components/admin/ActionForm/Fields/FieldSelectionModal/index.tsx @@ -8,10 +8,10 @@ import useUserCustomFields from 'api/user_custom_fields/useUserCustomFields'; import useLocale from 'hooks/useLocale'; -import { generateTempId } from 'components/FormBuilder/utils'; import Modal from 'components/UI/Modal'; import { FormattedMessage, useIntl } from 'utils/cl-intl'; +import { generateTempId } from 'utils/helperUtils'; import { AddFieldScreen } from './AddFieldScreen'; import messages from './messages'; diff --git a/front/app/utils/helperUtils.ts b/front/app/utils/helperUtils.ts index 098634132414..6d0d02c8d38d 100644 --- a/front/app/utils/helperUtils.ts +++ b/front/app/utils/helperUtils.ts @@ -1,3 +1,4 @@ +import { uuid4 } from '@sentry/utils'; import { trim, isUndefined } from 'lodash-es'; import { SupportedLocale, Multiloc, GraphqlLocale } from 'typings'; @@ -223,3 +224,7 @@ export function hexToRGBA(hex: string, alpha: number) { export const classNames = (...classes: (string | undefined)[]) => { return classes.filter(Boolean).join(' '); }; + +export function generateTempId() { + return `TEMP-ID-${uuid4()}`; +} From 6c34e1a8324963617527a670d246a9718de735ae Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Tue, 21 Jan 2025 09:17:05 -0300 Subject: [PATCH 06/44] Limit paste to 20 lines --- .../FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx index 83c6f7047307..71b1c86546f0 100644 --- a/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx +++ b/front/app/components/FormBuilder/components/FormBuilderSettings/ConfigSelectWithLocaleSwitcher/index.tsx @@ -197,6 +197,7 @@ const ConfigSelectWithLocaleSwitcher = ({ const handleMultilinePaste = useCallback( (lines, index) => { if (!selectedLocale) return; + if (lines.length > 20) return; updateFormOnMultlinePaste({ update, From 68d497e9678ee727c997d42ca2cd5276160b1481 Mon Sep 17 00:00:00 2001 From: Iva Date: Tue, 21 Jan 2025 14:39:32 +0200 Subject: [PATCH 07/44] Add cypress axe to events test --- .../components/Button/index.tsx | 13 ++- .../components/IconTooltip/index.tsx | 2 +- .../components/Tooltip/index.tsx | 110 ++++++------------ .../EventAttendanceButton/index.tsx | 41 ++++--- .../components/FilterSelector/Combobox.tsx | 5 +- .../FilterSelector/MultiSelectDropdown.tsx | 2 +- front/app/components/Pagination/index.tsx | 6 + front/app/components/Pagination/messages.ts | 12 ++ .../components/UI/ButtonWithLink/index.tsx | 86 +++++++------- .../components/admin/SubmitWrapper/index.tsx | 8 +- .../SubmitWrapper/index.tsx | 6 +- front/cypress/e2e/events/events_page.cy.ts | 14 ++- 12 files changed, 146 insertions(+), 159 deletions(-) create mode 100644 front/app/components/Pagination/messages.ts diff --git a/front/app/component-library/components/Button/index.tsx b/front/app/component-library/components/Button/index.tsx index e853e79682e2..b8e279fc2881 100644 --- a/front/app/component-library/components/Button/index.tsx +++ b/front/app/component-library/components/Button/index.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, ButtonHTMLAttributes } from 'react'; +import React, { MouseEvent, ButtonHTMLAttributes, forwardRef } from 'react'; import { isNil, get } from 'lodash-es'; import { darken, transparentize, opacify, rgba } from 'polished'; @@ -514,7 +514,6 @@ export interface Props extends ButtonContainerProps { hiddenText?: string | JSX.Element; icon?: IconProps['name']; iconPos?: 'left' | 'right'; - setSubmitButtonRef?: (value: any) => void; text?: string | JSX.Element; theme?: MainThemeProps | undefined; type?: ButtonHTMLAttributes['type']; @@ -526,11 +525,13 @@ export interface Props extends ButtonContainerProps { ariaExpanded?: boolean; ariaPressed?: boolean; ariaDescribedby?: string; + ariaControls?: string; as?: React.ElementType; tabIndex?: number; } +export type Ref = HTMLButtonElement; -const Button = (props: Props) => { +const Button = forwardRef((props, ref) => { const handleOnClick = (event: MouseEvent) => { const { onClick, processing, disabled } = props; @@ -594,6 +595,7 @@ const Button = (props: Props) => { ariaExpanded, ariaPressed, ariaDescribedby, + ariaControls, opacityDisabled, className, onClick: _onClick, @@ -698,8 +700,8 @@ const Button = (props: Props) => { aria-expanded={ariaExpanded} aria-pressed={ariaPressed} aria-describedby={ariaDescribedby} + aria-controls={ariaControls} aria-disabled={disabled || processing} - ref={props.setSubmitButtonRef} className={buttonClassNames} form={form} type={buttonType} @@ -707,11 +709,12 @@ const Button = (props: Props) => { autoFocus={autoFocus} as={as} tabIndex={tabIndex} + ref={ref} > {childContent} ); -}; +}); export default Button; diff --git a/front/app/component-library/components/IconTooltip/index.tsx b/front/app/component-library/components/IconTooltip/index.tsx index 338efed78664..d435b126b85e 100644 --- a/front/app/component-library/components/IconTooltip/index.tsx +++ b/front/app/component-library/components/IconTooltip/index.tsx @@ -105,7 +105,7 @@ const IconTooltip: FC = memo( placement={placement || 'right-end'} theme={theme || ''} maxWidth={maxTooltipWidth || 350} - useWrapper={false} + useContentWrapper={false} content={ {content} diff --git a/front/app/component-library/components/Tooltip/index.tsx b/front/app/component-library/components/Tooltip/index.tsx index 3eaecb7a3b73..722890c85efa 100644 --- a/front/app/component-library/components/Tooltip/index.tsx +++ b/front/app/component-library/components/Tooltip/index.tsx @@ -10,7 +10,7 @@ export type TooltipProps = Omit< 'interactive' | 'plugins' | 'role' > & { width?: string; - useWrapper?: boolean; + useContentWrapper?: boolean; }; const useActiveElement = () => { @@ -64,63 +64,12 @@ const PLUGINS = [ }, ]; -const TippyComponent = ({ - children, - theme, - width, - componentKey, - isFocused, - setIsFocused, - setKey, - tooltipId, - onHidden, - ...rest -}: { - children: React.ReactNode; - theme: string; - width: string | undefined; - componentKey: number; - isFocused: boolean | undefined; - setIsFocused: React.Dispatch>; - setKey: React.Dispatch>; - tooltipId: React.MutableRefObject; -} & TooltipProps) => { - // This component sometimes crashes because of re-renders. - // This useCallback slightly improves the situation (i.e. it makes it - // slightly less likely for the component to crash). - // But in the end we just need to completely rewrite this whole component - // to fix the issue properly. - // https://www.notion.so/govocal/Fix-Tooltip-component-16f9663b7b2680a48aebdf2ace15d1f8 - const handleOnHidden = useCallback(() => { - setIsFocused(undefined); - setKey((prev) => prev + 1); - }, [setIsFocused, setKey]); - - return ( - - - {children} - - - ); -}; - const Tooltip = ({ children, theme = 'light', width, - // This prop is used to determine if the native Tippy component should be wrapped in a Box component - useWrapper = true, + // This prop is used to determine if the native Tippy component content should be wrapped in a Box component + useContentWrapper = true, ...rest }: TooltipProps) => { const tooltipId = useRef( @@ -145,37 +94,52 @@ const Tooltip = ({ } }, [activeElement, isFocused]); - if (useWrapper) { + // This component sometimes crashes because of re-renders. + // This useCallback slightly improves the situation (i.e. it makes it + // slightly less likely for the component to crash). + // But in the end we just need to completely rewrite this whole component + // to fix the issue properly. + // https://www.notion.so/govocal/Fix-Tooltip-component-16f9663b7b2680a48aebdf2ace15d1f8 + const handleOnHidden = useCallback(() => { + setIsFocused(undefined); + setKey((prev) => prev + 1); + }, [setIsFocused, setKey]); + + if (useContentWrapper) { return ( - - {children} - + + {children} + + ); } else { return ( - // This option is used for more accessible tooltips when useWrapper is false + // This Box is used for more accessible tooltips when useContentWrapper is false - {children} - + ); } diff --git a/front/app/components/EventAttendanceButton/index.tsx b/front/app/components/EventAttendanceButton/index.tsx index 73b558595f33..3d5bfd5b30f5 100644 --- a/front/app/components/EventAttendanceButton/index.tsx +++ b/front/app/components/EventAttendanceButton/index.tsx @@ -182,28 +182,27 @@ const EventAttendanceButton = ({ event }: EventAttendanceButtonProps) => { disabled={!disabled_reason} placement="bottom" content={disabledMessage} + useContentWrapper={false} > -
- -
+ {currentTitle} diff --git a/front/app/components/FilterSelector/MultiSelectDropdown.tsx b/front/app/components/FilterSelector/MultiSelectDropdown.tsx index 7ae3c281fc01..5dd741441a8c 100644 --- a/front/app/components/FilterSelector/MultiSelectDropdown.tsx +++ b/front/app/components/FilterSelector/MultiSelectDropdown.tsx @@ -166,7 +166,7 @@ const MultiSelectDropdown = ({ minWidth={minWidth} onKeyDown={handleKeyDown} ariaExpanded={opened} - aria-controls={baseID} + ariaControls={baseID} > {currentTitle} diff --git a/front/app/components/Pagination/index.tsx b/front/app/components/Pagination/index.tsx index 6dace4498f01..2b6e7872a0a4 100644 --- a/front/app/components/Pagination/index.tsx +++ b/front/app/components/Pagination/index.tsx @@ -9,8 +9,11 @@ import { import { rgba } from 'polished'; import styled from 'styled-components'; +import { useIntl } from 'utils/cl-intl'; import { removeFocusAfterMouseClick } from 'utils/helperUtils'; +import messages from './messages'; + const ContainerInner = styled.div` display: flex; align-items: center; @@ -124,6 +127,7 @@ const Pagination = ({ useColorsTheme, loadPage, }: Props) => { + const { formatMessage } = useIntl(); const calculateMenuItems = (currentPage: number, totalPages: number) => { const current = currentPage; const last = totalPages; @@ -183,6 +187,7 @@ const Pagination = ({ onClick={goTo(currentPage - 1)} disabled={currentPage === 1} className={currentPage === 1 ? 'disabled' : ''} + aria-label={formatMessage(messages.back)} > @@ -209,6 +214,7 @@ const Pagination = ({ onClick={goTo(currentPage + 1)} disabled={currentPage === totalPages} className={currentPage === totalPages ? 'disabled' : ''} + aria-label={formatMessage(messages.next)} > diff --git a/front/app/components/Pagination/messages.ts b/front/app/components/Pagination/messages.ts new file mode 100644 index 000000000000..35f42d64d777 --- /dev/null +++ b/front/app/components/Pagination/messages.ts @@ -0,0 +1,12 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + next: { + id: 'app.components.Pagination.next', + defaultMessage: 'Next page', + }, + back: { + id: 'app.components.Pagination.back', + defaultMessage: 'Previous page', + }, +}); diff --git a/front/app/components/UI/ButtonWithLink/index.tsx b/front/app/components/UI/ButtonWithLink/index.tsx index c691226b491b..182f62e94f39 100644 --- a/front/app/components/UI/ButtonWithLink/index.tsx +++ b/front/app/components/UI/ButtonWithLink/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { Button, @@ -21,51 +21,49 @@ interface ButtonContainerProps extends ComponentLibraryButtonContainerProps { 'data-cy'?: string; } -const ButtonWithLink = ({ - linkTo, - openLinkInNewTab, - disabled, - scrollToTop, - ...rest -}: Props) => { - const isExternalLink = - linkTo && (linkTo.startsWith('http') || linkTo.startsWith('www')); +type Ref = HTMLButtonElement; - const link = - linkTo && !disabled - ? isExternalLink - ? ({ - children, - ...rest - }: ButtonProps & React.HTMLAttributes) => ( - - {children} - - ) - : ({ - children, - ...rest - }: Omit & - React.HTMLAttributes) => ( - - {children} - - ) - : undefined; +const ButtonWithLink = forwardRef( + ({ linkTo, openLinkInNewTab, disabled, scrollToTop, ...rest }, ref) => { + const isExternalLink = + linkTo && (linkTo.startsWith('http') || linkTo.startsWith('www')); - return