diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 1f1f183540..1a4403e246 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.29.4", + "version": "7.29.5-workflowStorageActions.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.29.4", + "version": "7.29.5-workflowStorageActions.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 40d54e6d32..8d4076a8e4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.29.4", + "version": "7.29.5-workflowStorageActions.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 5e7639e679..e2609e6e91 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,11 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version TBD +*Released*: TBD +- Add optional `jobActionId` parameter to `updateSampleStorageData` +- Add `dividedOptionsRenderer` and `filterDividedOptions` for rendering selectInputs with dividers between groups of options + ### version 7.29.4 *Released*: 9 April 2026 - GitHub Issue 954: Add error for duplicate values for parent inputs diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 4c01381ea3..3fa5e576c2 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -308,6 +308,7 @@ import { import { QueryFormInputs } from './internal/components/forms/QueryFormInputs'; import { LookupSelectInput } from './internal/components/forms/input/LookupSelectInput'; import { SelectInput } from './internal/components/forms/input/SelectInput'; +import { dividedOptionsRenderer, filterDividedOptions } from './internal/components/forms/input/DividedOptionsRenderer'; import { DatePickerInput } from './internal/components/forms/input/DatePickerInput'; import { FileInput } from './internal/components/forms/input/FileInput'; import { TextInput } from './internal/components/forms/input/TextInput'; @@ -1249,6 +1250,7 @@ export { DisableableMenuItem, DiscardConsumedSamplesPanel, Discussions, + dividedOptionsRenderer, DOMAIN_FIELD_REQUIRED, DOMAIN_FIELD_TYPE, DOMAIN_RANGE_VALIDATOR, @@ -1305,6 +1307,7 @@ export { FilterAction, filterArrayToString, FilterCriteriaRenderer, + filterDividedOptions, FilterStatus, FIND_BY_IDS_QUERY_PARAM, FindByIdsModal, diff --git a/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.test.tsx b/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.test.tsx new file mode 100644 index 0000000000..7ccf16fbe2 --- /dev/null +++ b/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.test.tsx @@ -0,0 +1,74 @@ +import { filterDividedOptions } from './DividedOptionsRenderer'; + +describe('filterDividedOptions', () => { + test('no options presented', () => { + expect(filterDividedOptions(undefined, undefined)).toStrictEqual([]); + }); + test('no dividers', () => { + expect(filterDividedOptions([{label: 'option1', value: 'option1'}], [])).toStrictEqual([{label: 'option1', value: 'option1'}]); + expect(filterDividedOptions([{label: 'option1', value: 'option1'}, {label: 'option2', value: 'option2'}], ['option2'])).toStrictEqual([{label: 'option1', value: 'option1'}]); + }); + test('all selected', () => { + expect(filterDividedOptions([{label: 'option1', value: 'o1'}, {label: 'option2', value: 'o2'}], ['o2', 'o1'])).toStrictEqual([]); + + }); + test('none selected', () => { + expect(filterDividedOptions([{label: 'option1', value: 'o1'}, {label: undefined, isDivider: true}, {label: 'option2', value: 'o2'}], [])) + .toStrictEqual([ + {label: 'option1', value: 'o1'}, {label: undefined, isDivider: true}, {label: 'option2', value: 'o2'} + ]); + }); + test('remove last divider', () => { + expect(filterDividedOptions([{label: 'option1', value: 'o1'}, {isDivider: true}, {label: 'option2', value: 'o2'}], ['o2'])) + .toStrictEqual([ + {label: 'option1', value: 'o1'} + ]); + expect(filterDividedOptions([{label: 'option1', value: 'o1'}, {isDivider: true}, {label: 'option2', value: 'o2'}, {isDivider: true}, {label: 'option3', value: 'o3'}], ['o2', 'o3'])) + .toStrictEqual([ + {label: 'option1', value: 'o1'} + ]); + }); + test('remove middle divider', () => { + expect(filterDividedOptions([ + {label: 'option1', value: 'o1'}, + {label: undefined, isDivider: true}, + {label: 'option2', value: 'o2'}, + {label: undefined, isDivider: true}, + {label: 'option3', value: 'o3'}, + ], + ['o2'])) + .toStrictEqual([ + {label: 'option1', value: 'o1'}, + {label: undefined, isDivider: true}, + {label: 'option3', value: 'o3'} + ]); + }); + test('remove first divider', () => { + expect(filterDividedOptions([ + {label: 'option1', value: 'o1'}, + {label: undefined, isDivider: true}, + {label: 'option2', value: 'o2'}, + {label: undefined, isDivider: true}, + {label: 'option3', value: 'o3'}, + ], + ['o1'])) + .toStrictEqual([ + {label: 'option2', value: 'o2'}, + {label: undefined, isDivider: true}, + {label: 'option3', value: 'o3'} + ]); + }); + test('remove multiple dividers', () => { + expect(filterDividedOptions([ + {label: 'option1', value: 'o1'}, + {label: 'd1', isDivider: true}, + {label: 'option2', value: 'o2'}, + {label: 'd2', isDivider: true}, + {label: 'option3', value: 'o3'}, + ], + ['o1', 'o2'])) + .toStrictEqual([ + {label: 'option3', value: 'o3'} + ]); + }); +}); diff --git a/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.tsx b/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.tsx new file mode 100644 index 0000000000..39f2797299 --- /dev/null +++ b/packages/components/src/internal/components/forms/input/DividedOptionsRenderer.tsx @@ -0,0 +1,48 @@ +import React, { FC, memo } from 'react'; + +interface DividedOptionsRendererProps { + isDivider: boolean; + label?: string; +} + +// export for jest testing +export const DividedOptionsRenderer: FC = memo(({ label, isDivider }) => { + if (isDivider) { + return
; + } + return
{label}
+}); +DividedOptionsRenderer.displayName = 'DividedOptionsRenderer'; + +export function dividedOptionsRenderer(option) { + return ; +} + +export function filterDividedOptions(allOptions, selectedOptions): any[] { + if (!allOptions) + return []; + + const notSelected = selectedOptions ? allOptions + // remove options already selected + .filter(option => selectedOptions.indexOf(option.value) === -1) : allOptions; + const options = []; + // remove dividers that are no longer dividing anything + let hasPreviousSection = false; + let pendingDivider; + notSelected.forEach((option) => { + if (!option.isDivider) { + if (pendingDivider) { + options.push(pendingDivider); + pendingDivider = undefined; + } + options.push(option); + hasPreviousSection = true; + } else { + if (hasPreviousSection) { + pendingDivider = option; + hasPreviousSection = false; + } + } + }); + return options; +} diff --git a/packages/components/src/internal/components/samples/actions.ts b/packages/components/src/internal/components/samples/actions.ts index 836a2cc7fb..d86e6c4bf5 100644 --- a/packages/components/src/internal/components/samples/actions.ts +++ b/packages/components/src/internal/components/samples/actions.ts @@ -559,7 +559,8 @@ export function updateSampleStorageData( containerPath?: string, userComment?: string, isDiscard = false, - editMethod?: EDIT_METHOD + editMethod?: EDIT_METHOD, + jobActionId?: number ): Promise { if (sampleStorageData.length === 0) { return Promise.resolve(); @@ -569,6 +570,7 @@ export function updateSampleStorageData( return Ajax.request({ url: ActionURL.buildURL('inventory', 'updateSampleStorageData.api', containerPath), jsonData: { + jobActionId, sampleRows: sampleStorageData, [STORED_AMOUNT_FIELDS.AUDIT_COMMENT]: userComment, ...getRequestAuditDetail(editMethod), diff --git a/packages/components/src/theme/form.scss b/packages/components/src/theme/form.scss index b0cef0890f..53bd0795d0 100644 --- a/packages/components/src/theme/form.scss +++ b/packages/components/src/theme/form.scss @@ -465,3 +465,7 @@ textarea.form-control { .has-warning .select-input__control:hover { border-color: $brand-warning; } + +.select-options-divider { + margin: 0 2px 0 2px; +}