Skip to content

Commit b09b17c

Browse files
joaquincasalFBanfiJuliRossi
authored
Bulk Edit v2 [MAPS-22] (#10226)
* Bulk-Edit: field editors for initial default appearances [MAPS-53] (#10159) * Keyboard accesibility for bulk edit [MAPS-29] (#10110) * Bulk-Edit-App: Fix sorting error and edit button with no padding [INTEG-3103] (#10090) * Bulk-Edit-App: Freeze top row with Field Names [INTEG-2953] (#10076) * freeze top row with Field Names * removing unused import * making the status column sticky too (#10082) * not showing the edit button when loading entries * fix error when changing sorting * Bulk edit: Filter columns [INTEG-3089] (#10089) * Bulk-Edit-App: Freeze top row with Field Names [INTEG-2953] (#10076) * freeze top row with Field Names * removing unused import * making the status column sticky too (#10082) * wip * select all * Refactor FilterColumns and SortMenu components for improved layout and functionality * Fixing states and enhancing performance in the process by not calling the getContentType each time * Fix box issue * Renaming and fixing warnings * sticky * corrections PR comments * Fixing rebase conflicts --------- Co-authored-by: Franco Banfi <62450599+FBanfi@users.noreply.github.com> Co-authored-by: francobanfi <franco.banfi@external.contentful.com> * wip * Added new useKeyboardNavigation hook to encapsulate keyboard navigation logic. * Simplifying a bit * Refactor keyboard navigation logic in useKeyboardNavigation hook for improved readability and performance. Simplified moveFocus and extendFocusToEdge functions by removing unnecessary useCallback and enhancing selection handling. * Refactor EntryTable and TableHeader components to improve keyboard navigation and selection handling. Updated focus logic to use HEADERS_ROW constant for better readability and maintainability. Enhanced checkbox toggle functionality for header and row selections. * fixing issues * changing styles for keyboard accessibility * Refactors and tests * Fixing focus on first cell and edge navigation * Readding column selection * Refactoring styles * Refactor Table components to centralize cell focus and selection logic. * Fixing checked disable checkboxes * Refactor EntryTable, TableHeader, and TableRow components to unify cell focus and selection logic. Updated function signatures to use FocusPosition for better clarity and maintainability. Enhanced keyboard navigation handling in useKeyboardNavigation hook and corresponding tests. * Simplifying a few things. Enter doesn't do that much anymore * fix merge --------- Co-authored-by: Franco Banfi <62450599+FBanfi@users.noreply.github.com> Co-authored-by: francobanfi <franco.banfi@external.contentful.com> * Tooltip fix for bulk edit [MAPS-57] (#10148) * fix * wip * Workaround to make both things work * changing some icons to match field editors versions + adding initial editors * changing icon + modifying tests * adding tests for FieldEditor component * adding more field editors * adding test for entry utils content type mapper * renaming method * removing unused variable * moving methods to entry utils file * removing any * removing unnecessary logic for field editor setter * changing disabled logic * refactors + removing createLocales * using field locale instead of default * removing manual numeric validations * removing mock from BulkEditModal.test.tsx * fixing tests between conflicts * Bulk-Edit: Parsing the value for the different editors [MAPS-53] (#10173) * Keyboard accesibility for bulk edit [MAPS-29] (#10110) * Bulk-Edit-App: Fix sorting error and edit button with no padding [INTEG-3103] (#10090) * Bulk-Edit-App: Freeze top row with Field Names [INTEG-2953] (#10076) * freeze top row with Field Names * removing unused import * making the status column sticky too (#10082) * not showing the edit button when loading entries * fix error when changing sorting * Bulk edit: Filter columns [INTEG-3089] (#10089) * Bulk-Edit-App: Freeze top row with Field Names [INTEG-2953] (#10076) * freeze top row with Field Names * removing unused import * making the status column sticky too (#10082) * wip * select all * Refactor FilterColumns and SortMenu components for improved layout and functionality * Fixing states and enhancing performance in the process by not calling the getContentType each time * Fix box issue * Renaming and fixing warnings * sticky * corrections PR comments * Fixing rebase conflicts --------- Co-authored-by: Franco Banfi <62450599+FBanfi@users.noreply.github.com> Co-authored-by: francobanfi <franco.banfi@external.contentful.com> * wip * Added new useKeyboardNavigation hook to encapsulate keyboard navigation logic. * Simplifying a bit * Refactor keyboard navigation logic in useKeyboardNavigation hook for improved readability and performance. Simplified moveFocus and extendFocusToEdge functions by removing unnecessary useCallback and enhancing selection handling. * Refactor EntryTable and TableHeader components to improve keyboard navigation and selection handling. Updated focus logic to use HEADERS_ROW constant for better readability and maintainability. Enhanced checkbox toggle functionality for header and row selections. * fixing issues * changing styles for keyboard accessibility * Refactors and tests * Fixing focus on first cell and edge navigation * Readding column selection * Refactoring styles * Refactor Table components to centralize cell focus and selection logic. * Fixing checked disable checkboxes * Refactor EntryTable, TableHeader, and TableRow components to unify cell focus and selection logic. Updated function signatures to use FocusPosition for better clarity and maintainability. Enhanced keyboard navigation handling in useKeyboardNavigation hook and corresponding tests. * Simplifying a few things. Enter doesn't do that much anymore * fix merge --------- Co-authored-by: Franco Banfi <62450599+FBanfi@users.noreply.github.com> Co-authored-by: francobanfi <franco.banfi@external.contentful.com> * Tooltip fix for bulk edit [MAPS-57] (#10148) * fix * wip * Workaround to make both things work * adding more field editors * adding test for entry utils content type mapper * refactors + removing createLocales * parsing the value for the different editors * setting truncation to display fields * adding more tests * pr comments * format changes between conflicts resolution * changing utils file name --------- Co-authored-by: JuliRossi <juliana.rossi@external.contentful.com> --------- Co-authored-by: JuliRossi <juliana.rossi@external.contentful.com> * Bulk edit: Refactor display names [MAPS-78] (#10205) * refactor to reuse existing display value method * refactor for getFieldDisplayValue method * refactor in the getEntryFieldValue method * removing test * Bulk Edit: Customize the Boolean editor label with user settings [MAPS-86] (#10210) * first version of the customizable boolean labels * refactor to use useSdk hook * adding tests and some refactors * fixing pr comments * removing unused imports * Bulk Edit: Alternative appearances [MAPS-54] (#10219) * first version of the customizable boolean labels * refactor to use useSdk hook * fixing pr comments * removing unused imports * adding alternative appearances for field editors * Bulk edit: Add field validations [MAPS-52] (#10221) * Add field validations * Address PR comments - Remove duplicated debounced value - Updated the way error messages are displayed so they're consistent with errors from the field editors - Removed redundant isValid property * Bulk edit v2 fixes [MAPS-22] (#10229) * Run validations for items * Remove value from update if empty --------- Co-authored-by: Franco Banfi <62450599+FBanfi@users.noreply.github.com> Co-authored-by: JuliRossi <juliana.rossi@external.contentful.com>
1 parent 05317bd commit b09b17c

41 files changed

Lines changed: 13986 additions & 454 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/bulk-edit/package-lock.json

Lines changed: 11638 additions & 185 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/bulk-edit/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,21 @@
55
"dependencies": {
66
"@contentful/app-sdk": "^4.29.1",
77
"@contentful/f36-components": "4.79.1",
8+
"@contentful/f36-icons": "^5.6.0",
89
"@contentful/f36-multiselect": "^4.81.1",
910
"@contentful/f36-navlist": "^4.1.0-alpha.1",
1011
"@contentful/f36-tokens": "4.2.0",
11-
"@contentful/field-editor-json": "^3.3.38",
12+
"@contentful/field-editor-boolean": "^1.7.16",
13+
"@contentful/field-editor-checkbox": "^1.7.0",
14+
"@contentful/field-editor-date": "^1.9.16",
15+
"@contentful/field-editor-dropdown": "^1.8.19",
16+
"@contentful/field-editor-json": "^3.5.16",
17+
"@contentful/field-editor-list": "^1.7.0",
18+
"@contentful/field-editor-multiple-line": "^1.3.9",
19+
"@contentful/field-editor-number": "^1.5.16",
20+
"@contentful/field-editor-radio": "^1.8.0",
21+
"@contentful/field-editor-single-line": "^1.3.9",
22+
"@contentful/field-editor-tags": "^1.7.16",
1223
"@contentful/react-apps-toolkit": "1.2.16",
1324
"@contentful/rich-text-html-renderer": "^17.1.0",
1425
"@phosphor-icons/react": "^2.1.10",

apps/bulk-edit/src/App.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useMemo } from 'react';
1+
import { useMemo, useEffect } from 'react';
22
import { locations, AppExtensionSDK } from '@contentful/app-sdk';
33
import Page from './locations/Page';
44
import { useSDK } from '@contentful/react-apps-toolkit';
5+
import { i18n } from '@lingui/core';
56

67
const ComponentLocationSettings = {
78
[locations.LOCATION_PAGE]: Page,
@@ -10,6 +11,16 @@ const ComponentLocationSettings = {
1011
const App = () => {
1112
const sdk = useSDK<AppExtensionSDK>();
1213

14+
// Initialize Lingui i18n for field editors that require it
15+
useEffect(() => {
16+
const defaultLocale = sdk.locales.default;
17+
if (!i18n.locale) {
18+
// Initialize with a default locale - field editors will use their own locales
19+
i18n.load(defaultLocale, {});
20+
i18n.activate(defaultLocale);
21+
}
22+
}, []);
23+
1324
const Component = useMemo(() => {
1425
for (const [location, component] of Object.entries(ComponentLocationSettings)) {
1526
if (sdk.location.is(location)) {

apps/bulk-edit/src/locations/Page/components/BulkEditModal.tsx

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
import React, { useEffect, useState } from 'react';
2-
import {
3-
Modal,
4-
Button,
5-
TextInput,
6-
Text,
7-
Flex,
8-
FormControl,
9-
Note,
10-
} from '@contentful/f36-components';
11-
import type { Entry, ContentTypeField } from '../types';
12-
import { getEntryFieldValue, truncate } from '../utils/entryUtils';
2+
import { Button, Flex, FormControl, Modal, Note, Text } from '@contentful/f36-components';
3+
import type { ContentTypeField, Entry } from '../types';
4+
import { getEntryFieldValue, getFieldDisplayValue } from '../utils/entryUtils';
135
import { ClockIcon } from '@contentful/f36-icons';
6+
import { FieldEditor } from './FieldEditor';
7+
import { FieldValidation } from './FieldValidation';
8+
import type { LocalesAPI } from '@contentful/field-editor-shared';
149

1510
interface BulkEditModalProps {
1611
isOpen: boolean;
1712
onClose: () => void;
1813
onSave: (newValue: string | number) => void;
1914
selectedEntries: Entry[];
2015
selectedField: ContentTypeField | null;
21-
defaultLocale: string;
16+
locales: LocalesAPI;
2217
isSaving: boolean;
2318
totalUpdateCount: number;
2419
editionCount: number;
@@ -30,25 +25,24 @@ export const BulkEditModal: React.FC<BulkEditModalProps> = ({
3025
onSave,
3126
selectedEntries,
3227
selectedField,
33-
defaultLocale,
28+
locales,
3429
isSaving,
3530
totalUpdateCount,
3631
editionCount,
3732
}) => {
38-
const [value, setValue] = useState('');
33+
const [value, setValue] = useState<any>('');
34+
const [hasValidationErrors, setHasValidationErrors] = useState(false);
3935
const entryCount = selectedEntries.length;
4036
const firstEntry = selectedEntries[0];
4137
const firstValueToUpdate =
42-
firstEntry && selectedField && defaultLocale
43-
? getEntryFieldValue(firstEntry, selectedField, defaultLocale)
38+
firstEntry && selectedField && locales.default
39+
? getEntryFieldValue(firstEntry, selectedField, locales.default)
4440
: '';
4541
const title = entryCount === 1 ? 'Edit' : 'Bulk edit';
4642

47-
const isNumber = selectedField?.type === 'Number' || selectedField?.type === 'Integer';
48-
const isInvalid = selectedField?.type === 'Integer' && !Number.isInteger(Number(value));
49-
5043
useEffect(() => {
5144
setValue('');
45+
setHasValidationErrors(false);
5246
}, [isOpen]);
5347

5448
return (
@@ -66,26 +60,28 @@ export const BulkEditModal: React.FC<BulkEditModalProps> = ({
6660
</Text>
6761
<Flex>
6862
<Text>
69-
<Text fontWeight="fontWeightDemiBold">{truncate(firstValueToUpdate, 100)}</Text>{' '}
63+
<Text fontWeight="fontWeightDemiBold">
64+
{getFieldDisplayValue(selectedField, firstValueToUpdate, 30)}
65+
</Text>{' '}
7066
{entryCount === 1 ? 'selected' : `selected and ${entryCount - 1} more`}
7167
</Text>
7268
</Flex>
73-
<FormControl isInvalid={isInvalid}>
74-
<TextInput
75-
name="bulk-edit-value"
76-
value={value}
77-
onChange={(e) => setValue(e.target.value)}
78-
placeholder="Enter your new value"
79-
type={isNumber ? 'number' : 'text'}
80-
isInvalid={isInvalid}
81-
autoFocus
82-
/>
83-
{isInvalid && (
84-
<FormControl.ValidationMessage>
85-
Integer field does not allow decimal
86-
</FormControl.ValidationMessage>
87-
)}
88-
</FormControl>
69+
{selectedField && (
70+
<>
71+
<FieldEditor
72+
field={selectedField}
73+
value={value}
74+
onChange={setValue}
75+
locales={locales}
76+
datatest-id="field-editor"
77+
/>
78+
<FieldValidation
79+
field={selectedField}
80+
value={value}
81+
onValidationChange={setHasValidationErrors}
82+
/>
83+
</>
84+
)}
8985
</Flex>
9086
{totalUpdateCount > 0 && isSaving && (
9187
<Note title="Updating entries" variant="neutral" icon={<ClockIcon variant="muted" />}>
@@ -103,12 +99,8 @@ export const BulkEditModal: React.FC<BulkEditModalProps> = ({
10399
</Button>
104100
<Button
105101
variant="primary"
106-
onClick={() => {
107-
if (isInvalid) return;
108-
const finalValue = isNumber ? Number(value) : value;
109-
onSave(finalValue);
110-
}}
111-
isDisabled={!value || isInvalid}
102+
onClick={() => onSave(value)}
103+
isDisabled={hasValidationErrors || isSaving}
112104
testId="bulk-edit-save"
113105
isLoading={isSaving}>
114106
Save

apps/bulk-edit/src/locations/Page/components/EntryTable.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';
2-
import { Table, Box, Pagination } from '@contentful/f36-components';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2+
import { Box, Pagination, Table } from '@contentful/f36-components';
33
import { useVirtualizer } from '@tanstack/react-virtual';
4-
import { Entry, ContentTypeField } from '../types';
4+
import { ContentTypeField, Entry } from '../types';
55
import { ContentTypeProps } from 'contentful-management';
66
import { styles } from '../styles';
77
import { TableHeader } from './TableHeader';
88
import { TableRow } from './TableRow';
9-
import { isCheckboxAllowed as isBulkEditable, getEntryUrl } from '../utils/entryUtils';
9+
import { getEntryUrl, isCheckboxAllowed as isBulkEditable } from '../utils/entryUtils';
1010
import {
1111
DISPLAY_NAME_COLUMN,
1212
DISPLAY_NAME_INDEX,
1313
ENTRY_STATUS_COLUMN,
1414
ESTIMATED_ROW_HEIGHT,
1515
HEADERS_ROW,
1616
} from '../utils/constants';
17-
import { useKeyboardNavigation, FocusPosition } from '../hooks/useKeyboardNavigation';
17+
import { FocusPosition, useKeyboardNavigation } from '../hooks/useKeyboardNavigation';
1818
import { tableStyles } from './EntryTable.styles';
1919

2020
interface EntryTableProps {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { SingleLineEditor } from '@contentful/field-editor-single-line';
3+
import { MultipleLineEditor } from '@contentful/field-editor-multiple-line';
4+
import { NumberEditor } from '@contentful/field-editor-number';
5+
import { DateEditor } from '@contentful/field-editor-date';
6+
import { TagsEditor } from '@contentful/field-editor-tags';
7+
import { BooleanEditor } from '@contentful/field-editor-boolean';
8+
import { JsonEditor } from '@contentful/field-editor-json';
9+
import { DropdownEditor } from '@contentful/field-editor-dropdown';
10+
import { RadioEditor } from '@contentful/field-editor-radio';
11+
import { ListEditor } from '@contentful/field-editor-list';
12+
import { CheckboxEditor } from '@contentful/field-editor-checkbox';
13+
import type { ContentTypeField } from '../types';
14+
import { Note } from '@contentful/f36-components';
15+
import {
16+
createFieldAPI,
17+
getCustomBooleanLabels,
18+
getBooleanEditorParameters,
19+
} from '../utils/fieldEditorUtils';
20+
import type { LocalesAPI } from '@contentful/field-editor-shared';
21+
22+
interface FieldEditorProps {
23+
field: ContentTypeField;
24+
value: string;
25+
onChange: (value: string) => void;
26+
locales: LocalesAPI;
27+
}
28+
29+
const ERROR_MESSAGE = 'Failed to initialize field editor. Please try again.';
30+
export const FieldEditor: React.FC<FieldEditorProps> = ({ field, value, onChange, locales }) => {
31+
const [error, setError] = useState('');
32+
const locale = field.locale ? field.locale : locales.default;
33+
34+
const fieldApi = useMemo(() => {
35+
return createFieldAPI(field, value, onChange, locale);
36+
}, [field, value, onChange, locale]);
37+
38+
const renderEditor = () => {
39+
try {
40+
switch (field.fieldControl?.widgetId) {
41+
case 'singleLine':
42+
return (
43+
<SingleLineEditor
44+
isInitiallyDisabled={false}
45+
withCharValidation={true}
46+
field={fieldApi}
47+
locales={locales}
48+
/>
49+
);
50+
case 'dropdown':
51+
return (
52+
<DropdownEditor
53+
isInitiallyDisabled={false}
54+
field={fieldApi}
55+
locales={locales}></DropdownEditor>
56+
);
57+
case 'radio':
58+
return (
59+
<RadioEditor
60+
isInitiallyDisabled={false}
61+
field={fieldApi}
62+
locales={locales}></RadioEditor>
63+
);
64+
case 'multipleLine':
65+
return (
66+
<MultipleLineEditor field={fieldApi} locales={locales} isInitiallyDisabled={false} />
67+
);
68+
case 'numberEditor':
69+
return <NumberEditor field={fieldApi} isInitiallyDisabled={false} />;
70+
case 'datePicker':
71+
return <DateEditor field={fieldApi} isInitiallyDisabled={false} />;
72+
case 'listInput':
73+
return (
74+
<ListEditor field={fieldApi} locales={locales} isInitiallyDisabled={false}></ListEditor>
75+
);
76+
case 'checkbox':
77+
return (
78+
<CheckboxEditor
79+
field={fieldApi}
80+
locales={locales}
81+
isInitiallyDisabled={false}></CheckboxEditor>
82+
);
83+
case 'tagEditor':
84+
return <TagsEditor field={fieldApi} isInitiallyDisabled={false} />;
85+
case 'boolean':
86+
const { trueLabel, falseLabel } = getCustomBooleanLabels(field.fieldControl);
87+
88+
return (
89+
<BooleanEditor
90+
field={fieldApi}
91+
isInitiallyDisabled={false}
92+
parameters={getBooleanEditorParameters(trueLabel, falseLabel)}
93+
/>
94+
);
95+
96+
case 'objectEditor':
97+
return <JsonEditor field={fieldApi} isInitiallyDisabled={false} />;
98+
default:
99+
return <Note variant="negative">{ERROR_MESSAGE}</Note>;
100+
}
101+
} catch (error) {
102+
setError(ERROR_MESSAGE);
103+
console.error('Error: ', error);
104+
}
105+
};
106+
107+
if (error) {
108+
return <Note variant="negative">{error}</Note>;
109+
}
110+
111+
return <>{renderEditor()}</>;
112+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
import { Flex, Text, Icon } from '@contentful/f36-components';
3+
import { ValidationExecutor } from '../../../validations';
4+
import type { ContentTypeField } from '../types';
5+
import { WarningOctagonIcon } from '@contentful/f36-icons';
6+
7+
interface FieldValidationProps {
8+
field: ContentTypeField;
9+
value: any;
10+
onValidationChange?: (hasErrors: boolean) => void;
11+
}
12+
13+
export const FieldValidation: React.FC<FieldValidationProps> = ({
14+
field,
15+
value,
16+
onValidationChange,
17+
}) => {
18+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
19+
20+
const validationExecutor = useMemo(() => {
21+
return new ValidationExecutor(field);
22+
}, [field]);
23+
24+
useEffect(() => {
25+
const result = validationExecutor.validate(value);
26+
const errorMessages = result.errors.map((err) => err.message);
27+
setValidationErrors(errorMessages);
28+
29+
const hasErrors = result.errors.length > 0;
30+
31+
if (onValidationChange) {
32+
onValidationChange(hasErrors);
33+
}
34+
}, [value, validationExecutor, onValidationChange]);
35+
36+
if (validationErrors.length === 0) {
37+
return null;
38+
}
39+
40+
return (
41+
<>
42+
{validationErrors.map((error, index) => (
43+
<Flex key={index} alignItems="center" gap="spacingXs">
44+
<Icon as={WarningOctagonIcon} variant="negative" size="tiny"></Icon>
45+
<Text as="p" fontColor="red600">
46+
{error}
47+
</Text>
48+
</Flex>
49+
))}
50+
</>
51+
);
52+
};

apps/bulk-edit/src/locations/Page/components/SearchBar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect, useRef } from 'react';
22
import { Flex, Text, TextInput } from '@contentful/f36-components';
3-
import { SearchIcon } from '@contentful/f36-icons';
3+
import { MagnifyingGlassIcon } from '@contentful/f36-icons';
44
import { useDebounce } from 'use-debounce';
55
import { styles } from './SearchBar.styles';
66

@@ -41,7 +41,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
4141
placeholder="Search"
4242
value={inputValue}
4343
onChange={(e) => setInputValue(e.target.value)}
44-
icon={<SearchIcon />}
44+
icon={<MagnifyingGlassIcon />}
4545
isDisabled={isDisabled}
4646
/>
4747
</Flex>

0 commit comments

Comments
 (0)