Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2eea190
feat: enhance EntryTable and TableRow components with selection funct…
JuliRossi Jun 10, 2025
fbd746f
feat: add BulkEditModal component and integrate it into the Page comp…
JuliRossi Jun 10, 2025
0957033
fix: adjust text structure in BulkEditModal for improved readability
JuliRossi Jun 11, 2025
0e3a5a6
wip
JuliRossi Jun 11, 2025
b525bad
feat: update entry fields in BulkEditModal and manage state for updat…
JuliRossi Jun 11, 2025
3939edc
feat: implement success notifications for bulk entry updates and hand…
JuliRossi Jun 11, 2025
34cf563
update now includes localized fields
JuliRossi Jun 11, 2025
aeb4171
feat: enhance BulkEditModal and Page component to utilize getEntryFie…
JuliRossi Jun 11, 2025
7f4cfb3
feat: update BulkEditModal to support number input and validation for…
JuliRossi Jun 11, 2025
49258b6
feat: extend checkbox restrictions in entryUtils to include 'Object' …
JuliRossi Jun 12, 2025
c115d93
feat: add 'RichText' type to checkbox restrictions in entryUtils
JuliRossi Jun 12, 2025
3124a06
move tests
JuliRossi Jun 12, 2025
43c1852
fix tests
JuliRossi Jun 12, 2025
3be438e
refactor: remove unused 'fields' prop from BulkEditModal and related …
JuliRossi Jun 12, 2025
77bf9b7
fix: update success notification to use the first selected entry value
JuliRossi Jun 12, 2025
55a4303
fix: update loading condition and improve error handling in Page comp…
JuliRossi Jun 12, 2025
569462d
fix: enhance loading condition and update mock SDK for entry updates …
JuliRossi Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions apps/bulk-edit/src/locations/Page/components/BulkEditModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { Modal, Button, TextInput, Text, Flex, FormControl } from '@contentful/f36-components';
import type { Entry, ContentTypeField } from '../types';
import { getEntryFieldValue } from '../utils/entryUtils';

interface BulkEditModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (newValue: string | number) => void;
selectedEntries: Entry[];
selectedField: ContentTypeField | null;
defaultLocale: string;
isSaving: boolean;
}

export const BulkEditModal: React.FC<BulkEditModalProps> = ({
isOpen,
onClose,
onSave,
selectedEntries,
selectedField,
defaultLocale,
isSaving,
}) => {
const [value, setValue] = useState('');
const entryCount = selectedEntries.length;
const firstEntry = selectedEntries[0];
const firstValueToUpdate =
firstEntry && selectedField && defaultLocale
? getEntryFieldValue(firstEntry, selectedField, defaultLocale)
: '';
const title = entryCount === 1 ? 'Edit' : 'Bulk edit';

const isNumber = selectedField?.type === 'Number' || selectedField?.type === 'Integer';
const isInvalid = selectedField?.type === 'Integer' && !Number.isInteger(Number(value));

return (
<Modal isShown={isOpen} onClose={onClose} size="medium" aria-label={title}>
<Modal.Header title={title} />
<Modal.Content>
<Flex gap="spacingS" flexDirection="column">
<Text>
Editing field: <Text fontWeight="fontWeightDemiBold">{selectedField?.name}</Text>
</Text>
<Flex>
<Text>
<Text fontWeight="fontWeightDemiBold">{firstValueToUpdate}</Text>{' '}
{entryCount === 1 ? 'selected' : `selected and ${entryCount - 1} more`}
</Text>
</Flex>
<FormControl isInvalid={isInvalid}>
<TextInput
name="bulk-edit-value"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter your new value"
type={isNumber ? 'number' : 'text'}
isInvalid={isInvalid}
autoFocus
/>
{isInvalid && (
<FormControl.ValidationMessage>
Integer field does not allow decimal
</FormControl.ValidationMessage>
)}
</FormControl>
</Flex>
</Modal.Content>
<Modal.Controls>
<Button variant="secondary" onClick={onClose} testId="bulk-edit-cancel">
Cancel
</Button>
<Button
variant="primary"
onClick={() => {
if (isInvalid) return;
const finalValue = isNumber ? Number(value) : value;
onSave(finalValue);
}}
isDisabled={!value || isInvalid}
testId="bulk-edit-save"
isLoading={isSaving}>
Save
</Button>
</Modal.Controls>
</Modal>
);
};
53 changes: 41 additions & 12 deletions apps/bulk-edit/src/locations/Page/components/EntryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ interface EntryTableProps {
onPageChange: (page: number) => void;
onItemsPerPageChange: (itemsPerPage: number) => void;
pageSizeOptions: number[];
onSelectionChange?: (selection: {
selectedEntryIds: string[];
selectedFieldId: string | null;
}) => void;
}

function getColumnIds(fields: ContentTypeField[]): string[] {
Expand Down Expand Up @@ -67,6 +71,7 @@ export const EntryTable: React.FC<EntryTableProps> = ({
onPageChange,
onItemsPerPageChange,
pageSizeOptions,
onSelectionChange,
}) => {
const columnIds = getColumnIds(fields);
const allowedColumns = getBulkEditableColumns(fields);
Expand All @@ -79,11 +84,11 @@ export const EntryTable: React.FC<EntryTableProps> = ({
getInitialRowCheckboxState(entries, columnIds)
);

const checkedColumnId = useMemo(() => {
// Check header first
// Compute selected field (column)
const selectedFieldId = useMemo(() => {
// Only one column can be selected at a time
const checkedHeaderId = columnIds.find((columnId) => headerCheckboxes[columnId]);
if (checkedHeaderId) return checkedHeaderId;
// Then check rows
if (checkedHeaderId && allowedColumns[checkedHeaderId]) return checkedHeaderId;
for (const entryId in rowCheckboxes) {
const row = rowCheckboxes[entryId];
const checkedCellId = columnIds.find((columnId) => allowedColumns[columnId] && row[columnId]);
Expand All @@ -92,12 +97,22 @@ export const EntryTable: React.FC<EntryTableProps> = ({
return null;
}, [headerCheckboxes, rowCheckboxes, allowedColumns, columnIds]);

const checkboxesDisabled = Object.fromEntries(
columnIds.map((columnId) => [
columnId,
allowedColumns[columnId] ? checkedColumnId !== null && checkedColumnId !== columnId : true,
])
);
// Compute selected entry IDs for the selected field
const selectedEntryIds = useMemo(() => {
if (!selectedFieldId) return [];
// If header is checked, all entries are selected
if (headerCheckboxes[selectedFieldId]) {
return entries.map((e) => e.sys.id);
}
// Otherwise, collect entry IDs where the cell is checked
return entries.filter((e) => rowCheckboxes[e.sys.id]?.[selectedFieldId]).map((e) => e.sys.id);
}, [selectedFieldId, headerCheckboxes, rowCheckboxes, entries]);

React.useEffect(() => {
if (onSelectionChange) {
onSelectionChange({ selectedEntryIds, selectedFieldId });
}
}, [selectedEntryIds, selectedFieldId, onSelectionChange]);

function handleHeaderCheckboxChange(columnId: string, checked: boolean) {
setHeaderCheckboxes((previous) => ({ ...previous, [columnId]: checked }));
Expand Down Expand Up @@ -135,7 +150,14 @@ export const EntryTable: React.FC<EntryTableProps> = ({
fields={fields}
headerCheckboxes={headerCheckboxes}
onHeaderCheckboxChange={handleHeaderCheckboxChange}
checkboxesDisabled={checkboxesDisabled}
checkboxesDisabled={Object.fromEntries(
columnIds.map((columnId) => [
columnId,
allowedColumns[columnId]
? selectedFieldId !== null && selectedFieldId !== columnId
: true,
])
)}
/>
<Table.Body>
{entries.map((entry) => (
Expand All @@ -151,7 +173,14 @@ export const EntryTable: React.FC<EntryTableProps> = ({
onCellCheckboxChange={(columnId, checked) =>
handleCellCheckboxChange(entry.sys.id, columnId, checked)
}
cellCheckboxesDisabled={checkboxesDisabled}
cellCheckboxesDisabled={Object.fromEntries(
columnIds.map((columnId) => [
columnId,
allowedColumns[columnId]
? selectedFieldId !== null && selectedFieldId !== columnId
: true,
])
)}
/>
))}
</Table.Body>
Expand Down
Loading