Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIBULKED-586 Support multiple edits to statistical codes in one bulk edit job #685

Merged
merged 5 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* [UIBULKED-605](https://folio-org.atlassian.net/browse/UIBULKED-605) Enabling Confirm changes button based on forms state.
* [UIBULKED-606](https://folio-org.atlassian.net/browse/UIBULKED-606) Update upload-artifact actions from v3 to v4.
* [UIBULKED-584](https://folio-org.atlassian.net/browse/UIBULKED-584) Handling instances with sources other than MARC.
* [UIBULKED-685](https://folio-org.atlassian.net/browse/UIBULKED-685) Support multiple edits to statistical codes in one bulk edit job.

## [4.2.2](https://github.com/folio-org/ui-bulk-edit/tree/v4.2.2) (2024-11-15)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ import { AdditionalActionParameters } from './AdditionalActionParameters';
import { sortAlphabeticallyComponentLabels } from '../../../../../utils/sortAlphabetically';
import css from '../../../BulkEditPane.css';

export const ActionsRow = ({ option, actions, onChange }) => {
export const ActionsRow = ({ fieldsRules, option, actions, onChange }) => {
const { formatMessage } = useIntl();

return actions.map((action, actionIndex) => {
if (!action) return null;

const sortedActions = sortAlphabeticallyComponentLabels(action.actionsList, formatMessage);
const filteredActions = fieldsRules
? action.actionsList.filter(({ value }) => fieldsRules.availableActions.includes(value))
: action.actionsList;
const sortedActions = sortAlphabeticallyComponentLabels(filteredActions, formatMessage);

return (
<Fragment key={actionIndex}>
Expand All @@ -29,7 +32,7 @@ export const ActionsRow = ({ option, actions, onChange }) => {
dataOptions={sortedActions}
value={action.name}
onChange={(e) => onChange({ actionIndex, value: e.target.value, fieldName: ACTION_VALUE_KEY })}
disabled={action.actionsList?.length === 1}
disabled={filteredActions?.length === 1}
data-testid={`select-actions-${actionIndex}`}
aria-label={formatMessage({ id: 'ui-bulk-edit.ariaLabel.actionsSelect' })}
marginBottom0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getFilteredFields,
getExtraActions,
getFieldTemplate,
getFieldsWithRules,
getNormalizedFieldsRules,
} from './helpers';
import {
customFilter,
Expand All @@ -35,6 +37,7 @@ import css from '../../../BulkEditPane.css';
export const ContentUpdatesForm = ({ fields, setFields, options }) => {
const { formatMessage } = useIntl();
const { currentRecordType, approach } = useSearchParams();
const normalizedFieldsRules = getNormalizedFieldsRules(fields);

useEffect(() => {
setFields([getFieldTemplate(options, currentRecordType, approach)]);
Expand Down Expand Up @@ -119,10 +122,11 @@ export const ContentUpdatesForm = ({ fields, setFields, options }) => {
return finalActions;
};

const handleChange = ({ rowIndex, actionIndex, value, fieldName, tenants = [] }) => {
setFields(fieldsArr => fieldsArr.map((field, i) => {
const handleChange = ({ rowIndex, actionIndex, value, fieldName, tenants = [], option }) => {
const hasActionChanged = fieldName === ACTION_VALUE_KEY;

const mappedFields = fields.map((field, i) => {
if (i === rowIndex) {
const hasActionChanged = fieldName === ACTION_VALUE_KEY;
const hasValueChanged = fieldName === FIELD_VALUE_KEY && actionIndex === 0 && field.option === OPTIONS.ELECTRONIC_ACCESS_URL_RELATIONSHIP;

const sharedArgs = {
Expand Down Expand Up @@ -159,7 +163,14 @@ export const ContentUpdatesForm = ({ fields, setFields, options }) => {
}

return field;
}));
});

// Check if there are rules should be applied based on if action changed and option value
const fieldsWithRules = hasActionChanged
? getFieldsWithRules({ fields: mappedFields, option, value, rowIndex })
: mappedFields;

setFields(fieldsWithRules);
};

const handleRemove = (index) => {
Expand Down Expand Up @@ -211,11 +222,13 @@ export const ContentUpdatesForm = ({ fields, setFields, options }) => {
className={css.row}
onAdd={noop}
renderField={(field, index) => {
const filteredOptions = field.options.filter(o => !o.hidden);

return (
<Row data-testid={`row-${index}`} className={css.marcFieldRow}>
<Col xs={2} sm={2} className={css.column}>
<Selection
dataOptions={groupByCategory(field.options)}
dataOptions={groupByCategory(filteredOptions)}
value={field.option}
onChange={(value) => handleOptionChange(value, index, getTenantsById(field.options, value))}
placeholder={formatMessage({ id:'ui-bulk-edit.options.placeholder' })}
Expand All @@ -227,9 +240,10 @@ export const ContentUpdatesForm = ({ fields, setFields, options }) => {
/>
</Col>
<ActionsRow
fieldsRules={normalizedFieldsRules[index]}
option={field.option}
actions={field.actionsDetails.actions}
onChange={(values) => handleChange({ ...values, rowIndex: index })}
onChange={(values) => handleChange({ ...values, rowIndex: index, option: field.option })}
/>
<div className={css.actionButtonsWrapper}>
{isAddButtonShown(index, fields, options) && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ export const getFormattedDate = (value) => {
return `${moment(date).format(format)}.000Z`;
};

export const isAddButtonShown = (index, fields, options) => {
return index === fields.length - 1 && fields.length !== options.length;
};

export const getContentUpdatesBody = ({ bulkOperationId, contentUpdates, totalRecords }) => {
const bulkOperationRules = contentUpdates.reduce((acc, item) => {
const formattedItem = () => {
Expand Down Expand Up @@ -402,19 +398,154 @@ export const isMarcContentUpdatesFormValid = (errors) => {
return Object.keys(errors).length === 0;
};

export const getActionIndex = (fields, action) => fields.findIndex(({ option, actionsDetails }) => {
return option === OPTIONS.STATISTICAL_CODE && actionsDetails.actions.filter(Boolean)
.some(({ name }) => name === action);
});

/**
* Checks if there is at least one field with the STATISTICAL_CODE option that contains
* either an ADD_TO_EXISTING or REMOVE_SOME action.
*/
const hasAddOrRemoveFieldAction = (fields) => {
const addActionIndex = getActionIndex(fields, ACTIONS.ADD_TO_EXISTING);
const removeActionIndex = getActionIndex(fields, ACTIONS.REMOVE_SOME);

return addActionIndex !== -1 || removeActionIndex !== -1;
};

/**
* Determines the additional options that should be applied based on the provided fields.
* If the number of STATISTICAL_CODE fields is not exactly 2 and at least one STATISTICAL_CODE field
* has an ADD_TO_EXISTING or REMOVE_SOME action, this function returns an array containing STATISTICAL_CODE.
* It will be used to show/hide the extra STATISTICAl_CODE option in options list.
*/
export const getAdditionalOptions = (fields) => {
const statisticalCodeFields = fields.filter(({ option }) => option === OPTIONS.STATISTICAL_CODE);
const hasAddOrRemove = hasAddOrRemoveFieldAction(fields);

return statisticalCodeFields.length !== 2 && hasAddOrRemove ? [OPTIONS.STATISTICAL_CODE] : [];
};

/**
* Filters and updates the provided fields by marking options as hidden based on other fields’ options.
* For each field, it computes the unique set of options available among all fields and then hides
* options that are present in other fields – except for those that are returned by getAdditionalOptions.
*/
export const getFilteredFields = (initialFields) => {
return initialFields.map(f => {
const uniqOptions = new Set(initialFields.map(i => i.option));

const optionsExceptCurrent = [...uniqOptions].filter(u => u !== f.option);
const optionsExceptCurrent = [...uniqOptions].filter(u => u !== f.option && !getAdditionalOptions(initialFields).includes(u));

return {
...f,
options: f.options.filter(o => !optionsExceptCurrent.includes(o.value)),
options: f.options.map(o => ({ ...o, hidden: optionsExceptCurrent.includes(o.value) })),
};
});
};

/**
* Determines whether an "add" button should be shown for fields.
* The add button is shown if:
* - The provided index corresponds to the last field in the array, and
* - The total number of fields is less than the allowed maximum.
* The allowed maximum is defined as the length of the base options plus 1 if any STATISTICAL_CODE field
* contains an ADD_TO_EXISTING or REMOVE_SOME action as exceptional case.
*/
export const isAddButtonShown = (index, fields, options) => {
const additionalFieldsCount = hasAddOrRemoveFieldAction(fields) ? 1 : 0;
const maxFieldsLength = options.length + additionalFieldsCount;
return index === fields.length - 1 && fields.length < maxFieldsLength;
};

/**
* Processes an array of fields using rules for STATISTICAL_CODE.
* - Returns fields unchanged if `option` is not STATISTICAL_CODE.
* - When `value` is REMOVE_ALL:
* - Removes any STATISTICAL_CODE field not at `rowIndex`.
* - Sets `hidden` to true for STATISTICAL_CODE options.
*/
export const getFieldsWithRules = ({ fields, option, value, rowIndex }) => {
if (option !== OPTIONS.STATISTICAL_CODE) return fields;

return fields.map((field, i) => {
const isCurrentRow = i === rowIndex;
const isStatisticalCode = field.option === OPTIONS.STATISTICAL_CODE;

if (value === ACTIONS.REMOVE_ALL && isStatisticalCode && !isCurrentRow) {
return null; // Remove this item
}

return {
...field,
options: field.options.map(o => ({
...o,
hidden: o.value === OPTIONS.STATISTICAL_CODE
? (value === ACTIONS.REMOVE_ALL)
: o.hidden,
})),
};
}).filter(Boolean);
};

/**
* Normalizes the rules for fields having the STATISTICAL_CODE option.
* This function extracts each STATISTICAL_CODE field’s selected actions and available actions
* (from its actionsDetails), then adjusts the available actions for fields that are not the primary
* (first) field having either an ADD_TO_EXISTING or REMOVE_SOME action.
* The result is returned as an object keyed by the original field indices.
*/
export const getNormalizedFieldsRules = (fields) => {
const statisticalCodeFields = fields.reduce((acc, field, index) => {
const actions = field.actionsDetails.actions.filter(Boolean);

if (field.option === OPTIONS.STATISTICAL_CODE) {
acc.push({
selectedActions: actions.map(({ name }) => name),
availableActions: actions[0]?.actionsList.map(({ value }) => value),
index,
});
}

return acc;
}, []);

const addActionIndex = statisticalCodeFields.findIndex(({ selectedActions }) => {
return selectedActions.includes(ACTIONS.ADD_TO_EXISTING);
});

const removeActionIndex = statisticalCodeFields.findIndex(({ selectedActions }) => {
return selectedActions.includes(ACTIONS.REMOVE_SOME);
});

return statisticalCodeFields.reduce((acc, elem, index) => {
let item = elem;

if (addActionIndex !== -1 && addActionIndex !== index) {
item = {
...item,
availableActions: item.availableActions.filter((action) => {
return action === ACTIONS.REMOVE_SOME || !action;
})
};
}

if (removeActionIndex !== -1 && removeActionIndex !== index) {
item = {
...item,
availableActions: item.availableActions.filter((action) => {
return action === ACTIONS.ADD_TO_EXISTING || !action;
})
};
}

acc[elem.index] = item;

return acc;
}, {});
};

export const getExtraActions = (option, action) => {
switch (`${option}-${action}`) {
case `${OPTIONS.ITEM_NOTE}-${ACTIONS.FIND}`:
Expand Down
Loading
Loading