Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1249,6 +1250,7 @@ export {
DisableableMenuItem,
DiscardConsumedSamplesPanel,
Discussions,
dividedOptionsRenderer,
DOMAIN_FIELD_REQUIRED,
DOMAIN_FIELD_TYPE,
DOMAIN_RANGE_VALIDATOR,
Expand Down Expand Up @@ -1305,6 +1307,7 @@ export {
FilterAction,
filterArrayToString,
FilterCriteriaRenderer,
filterDividedOptions,
FilterStatus,
FIND_BY_IDS_QUERY_PARAM,
FindByIdsModal,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'}
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { FC, memo } from 'react';

interface DividedOptionsRendererProps {
isDivider: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would isDivider also be optional? (i.e. isDivider?: boolean). minor since this is a boolean field

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that, but it seemed unnecessary.

label?: string;
}

// export for jest testing
export const DividedOptionsRenderer: FC<DividedOptionsRendererProps> = memo(({ label, isDivider }) => {
if (isDivider) {
return <hr className="select-options-divider"/>;
}
return <div>{label}</div>
});
DividedOptionsRenderer.displayName = 'DividedOptionsRenderer';

export function dividedOptionsRenderer(option) {
return <DividedOptionsRenderer isDivider={option.data.isDivider} label={option.data.label} />;
}

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ export function updateSampleStorageData(
containerPath?: string,
userComment?: string,
isDiscard = false,
editMethod?: EDIT_METHOD
editMethod?: EDIT_METHOD,
jobActionId?: number
): Promise<any> {
if (sampleStorageData.length === 0) {
return Promise.resolve();
Expand All @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/theme/form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading