Skip to content
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
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": "6.54.1",
"version": "6.54.2",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
19 changes: 19 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 6.54.2
*Released*: 9 July 2025
- Merge from release25.7-SNAPSHOT to develop
- includes changes from 6.53.1 #1824
- includes changes from 6.53.2 #1823
- includes changes from 6.53.3 #1825

### version 6.54.1
*Released*: 8 July 2025
- Issue 53134: Set dirty bit after adding uniqueId field
Expand All @@ -10,6 +17,18 @@ Components, models, actions, and utility functions for LabKey applications and p
- ProductMenu shows 'Dashboard' instead of 'Storage' as subtitle in FM /home route
- Issue 53371: Find Samples by ID JS error when clicking "Find Samples" button without entering anything into the text area

### version 6.53.3
*Released*: 7 July 2025
- Issue 53394: FileInput revert removal of the "-fileUpload" suffix from inputId

### version 6.53.2
*Released*: 3 July 2025
* Issue 53141: Should set a dirty bit when setting or updating the hit selection criteria for an assay

### version 6.53.1
*Released*: 3 July 2025
- Issue 53153: Disable value validation for expInput, aliquotParent columns

### version 6.53.0
*Released*: 1 July 2025
- Remove AssayResultsForSamplesButton, AssayResultsForSamplesMenuItem
Expand Down
21 changes: 17 additions & 4 deletions packages/components/src/internal/FilterCriteriaModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { useLoadableState } from './useLoadableState';
import { LoadingSpinner } from './components/base/LoadingSpinner';
import { ChoicesListItem } from './components/base/ChoicesListItem';
import { FilterExpressionView } from './components/search/FilterExpressionView';
import { useAppContext } from './AppContext';
import { FilterCriteriaColumns } from './components/assay/models';
import { AssayProtocolModel } from './components/domainproperties/assay/models';
import { Alert } from './components/base/Alert';
import { ComponentsAPIWrapper, getDefaultAPIWrapper } from './APIWrapper';

type BaseFilterCriteriaField = Omit<FilterCriteria, 'op' | 'value'>;
interface FilterCriteriaField extends BaseFilterCriteriaField {
Expand Down Expand Up @@ -100,16 +100,21 @@ FilterCriteriaChoice.displayName = 'FilterCriteriaChoice';
* openTo: The propertyId of the domain field you want to open the modal to
*/
interface Props {
api?: ComponentsAPIWrapper;
onClose: () => void;
onSave: (filterCriteria: FilterCriteriaMap) => void;
openTo?: number;
protocolModel: AssayProtocolModel;
}

export const FilterCriteriaModal: FC<Props> = memo(({ onClose, onSave, openTo, protocolModel }) => {
const { api } = useAppContext();
export const FilterCriteriaModal: FC<Props> = memo(props => {
// Note: we cannot fetch api from useAppContext, because this component can be rendered in LKS, which does not set
// up an AppContext
const { api = getDefaultAPIWrapper(), onClose, onSave, openTo, protocolModel } = props;
const { protocolId, container } = protocolModel;
const domain = useMemo(() => protocolModel.getDomainByNameSuffix('Data'), [protocolModel]);
// Intentionally not using withRouteLeave, that is handled above this component, after onSave is called
const [isDirty, setIsDirty] = useState(false);
const [filterCriteria, setFilterCriteria] = useState<FilterCriteriaMap>(() => {
return domain.fields.reduce((result, field) => {
if (field.filterCriteria) result.set(field.propertyId, [...field.filterCriteria]);
Expand All @@ -135,6 +140,7 @@ export const FilterCriteriaModal: FC<Props> = memo(({ onClose, onSave, openTo, p

const onFieldFilterUpdate = useCallback(
(newFilters: Filter.IFilter[]) => {
setIsDirty(true);
setFilterCriteria(current => {
const filterCriteriaField = filterCriteriaFields.find(field => field.propertyId === selectedFieldId);
// Use the referencePropertyId if it exists, because all filterCriteria are stored on the parent field
Expand Down Expand Up @@ -186,7 +192,14 @@ export const FilterCriteriaModal: FC<Props> = memo(({ onClose, onSave, openTo, p
const hasFields = fieldsToRender !== undefined && fieldsToRender.length > 0;

return (
<Modal bsSize="lg" title="Hit Selection Criteria" onCancel={onClose} onConfirm={onConfirm} confirmText="Apply">
<Modal
bsSize="lg"
canConfirm={isDirty}
confirmText="Apply"
onCancel={onClose}
onConfirm={onConfirm}
title="Hit Selection Criteria"
>
{loading && <LoadingSpinner />}
{!loading && !error && (
<div className="filter-criteria-modal-body field-modal__container row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,27 @@ const AssayDomainForm: FC<AssayDomainFormProps> = memo(props => {
]);
return (
<DomainForm
key={domain.domainId || index}
api={api}
index={domain.domainId || index}
domainIndex={index}
appDomainHeaderRenderer={appDomainHeaderRenderer}
appPropertiesOnly={hideAdvancedProperties}
controlledCollapse
domain={domain}
domainFormDisplayOptions={displayOptions}
domainIndex={index}
headerPrefix={headerPrefix}
controlledCollapse
helpTopic={null} // null so that we don't show the "learn more about this tool" link for these domains
index={domain.domainId || index}
initCollapsed={currentPanelIndex !== index + DOMAIN_PANEL_INDEX}
validate={validatePanel === index + DOMAIN_PANEL_INDEX}
key={domain.domainId || index}
modelDomains={protocolModel.domains}
onChange={onChange}
onToggle={onToggle}
panelStatus={
protocolModel.isNew()
? getDomainPanelStatus(index + DOMAIN_PANEL_INDEX, currentPanelIndex, visitedPanels, firstState)
: 'COMPLETE'
}
helpTopic={null} // null so that we don't show the "learn more about this tool" link for these domains
onChange={onChange}
onToggle={onToggle}
appDomainHeaderRenderer={appDomainHeaderRenderer}
modelDomains={protocolModel.domains}
appPropertiesOnly={hideAdvancedProperties}
domainFormDisplayOptions={displayOptions}
validate={validatePanel === index + DOMAIN_PANEL_INDEX}
>
<div>{domain.description}</div>
</DomainForm>
Expand Down Expand Up @@ -336,40 +336,43 @@ export class AssayDesignerPanelsImpl extends React.PureComponent<Props, State> {
};

saveFilterCriteria = (filterCriteria: FilterCriteriaMap) => {
this.setState(current => {
const protocolModel = current.protocolModel;
const resultsIndex = current.protocolModel.domains.findIndex((domain: DomainDesign): boolean =>
domain.isNameSuffixMatch('Data')
);
const domains = current.protocolModel.domains;
let resultsDomain = domains.get(resultsIndex);
// Clear the existing values first
let fields = resultsDomain.fields.map(f => f.set('filterCriteria', []) as DomainField).toList();

filterCriteria.forEach((fieldCriteria, propertyId) => {
const domainFieldIdx = fields.findIndex(d => d.propertyId === propertyId);

if (domainFieldIdx < 0) {
console.warn(`Unable to find domain field with property id ${propertyId}`);
return;
}
this.setState(
current => {
const protocolModel = current.protocolModel;
const resultsIndex = current.protocolModel.domains.findIndex((domain: DomainDesign): boolean =>
domain.isNameSuffixMatch('Data')
);
const domains = current.protocolModel.domains;
let resultsDomain = domains.get(resultsIndex);
// Clear the existing values first
let fields = resultsDomain.fields.map(f => f.set('filterCriteria', []) as DomainField).toList();

let domainField = fields.get(domainFieldIdx);
domainField = domainField.set('filterCriteria', fieldCriteria) as DomainField;
fields = fields.set(domainFieldIdx, domainField);
});
filterCriteria.forEach((fieldCriteria, propertyId) => {
const domainFieldIdx = fields.findIndex(d => d.propertyId === propertyId);

resultsDomain = resultsDomain.set('fields', fields) as DomainDesign;
if (domainFieldIdx < 0) {
console.warn(`Unable to find domain field with property id ${propertyId}`);
return;
}

return {
modalOpen: false,
openTo: undefined,
protocolModel: protocolModel.set(
'domains',
protocolModel.domains.set(resultsIndex, resultsDomain)
) as AssayProtocolModel,
};
});
let domainField = fields.get(domainFieldIdx);
domainField = domainField.set('filterCriteria', fieldCriteria) as DomainField;
fields = fields.set(domainFieldIdx, domainField);
});

resultsDomain = resultsDomain.set('fields', fields) as DomainDesign;

return {
modalOpen: false,
openTo: undefined,
protocolModel: protocolModel.set(
'domains',
protocolModel.domains.set(resultsIndex, resultsDomain)
) as AssayProtocolModel,
};
},
() => this.props.onChange?.(this.state.protocolModel)
);
};

togglePropertiesPanel = (collapsed, callback): void => {
Expand Down Expand Up @@ -409,32 +412,32 @@ export class AssayDesignerPanelsImpl extends React.PureComponent<Props, State> {

return (
<BaseDomainDesigner
name={protocolModel.name}
exception={protocolModel.exception}
domains={protocolModel.domains}
exception={protocolModel.exception}
hasValidProperties={protocolModel.hasValidProperties()}
visitedPanels={visitedPanels}
submitting={submitting}
name={protocolModel.name}
onCancel={onCancel}
onFinish={this.onFinish}
saveBtnText={saveBtnText}
showUserComment={!initModel.isNew() && appPropertiesOnly}
submitting={submitting}
visitedPanels={visitedPanels}
>
<FilterCriteriaContext.Provider value={filterCriteriaState}>
<AssayPropertiesPanel
model={protocolModel}
onChange={this.onAssayPropertiesChange}
controlledCollapse
initCollapsed={currentPanelIndex !== PROPERTIES_PANEL_INDEX}
panelStatus={panelStatus}
validate={validatePanel === PROPERTIES_PANEL_INDEX}
appPropertiesOnly={appPropertiesOnly}
canRename={isGpat}
controlledCollapse
hideAdvancedProperties={hideAdvancedProperties}
hideStudyProperties={
!!domainFormDisplayOptions && domainFormDisplayOptions.hideStudyPropertyTypes
}
initCollapsed={currentPanelIndex !== PROPERTIES_PANEL_INDEX}
model={protocolModel}
onChange={this.onAssayPropertiesChange}
onToggle={this.togglePropertiesPanel}
canRename={isGpat}
panelStatus={panelStatus}
validate={validatePanel === PROPERTIES_PANEL_INDEX}
/>
{/* Note: We cannot filter this array because onChange needs the correct index for each domain */}
{protocolModel.domains.toArray().map((domain, i) => {
Expand All @@ -445,16 +448,16 @@ export class AssayDesignerPanelsImpl extends React.PureComponent<Props, State> {
<AssayDomainForm
api={api}
appDomainHeaders={appDomainHeaders}
currentPanelIndex={currentPanelIndex}
domain={domain}
domainFormDisplayOptions={domainFormDisplayOptions}
firstState={firstState}
headerPrefix={initModel?.name}
index={i}
key={domain.name}
onDomainChange={this.onDomainChange}
protocolModel={protocolModel}
currentPanelIndex={currentPanelIndex}
firstState={firstState}
onTogglePanel={onTogglePanel}
protocolModel={protocolModel}
validatePanel={validatePanel}
visitedPanels={visitedPanels}
/>
Expand All @@ -463,17 +466,17 @@ export class AssayDesignerPanelsImpl extends React.PureComponent<Props, State> {
{modalOpen && (
<FilterCriteriaModal
onClose={this.closeModal}
openTo={openTo}
onSave={this.saveFilterCriteria}
openTo={openTo}
protocolModel={protocolModel}
/>
)}
</FilterCriteriaContext.Provider>
{appPropertiesOnly && allowFolderExclusion && (
<DataTypeFoldersPanel
controlledCollapse
dataTypeRowId={protocolModel?.protocolId}
dataTypeName={protocolModel?.name}
dataTypeRowId={protocolModel?.protocolId}
entityDataType={AssayRunDataType}
initCollapsed={currentPanelIndex !== protocolModel.domains.size + 1}
onToggle={this.toggleFoldersPanel}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FC, useMemo } from 'react';
import { List, Map } from 'immutable';
import { List } from 'immutable';

import { Operation } from '../../../public/QueryColumn';

Expand All @@ -18,9 +18,9 @@ type BaseProps = Omit<
| 'hideButtons'
| 'includeCountField'
| 'initiallyDisableFields'
| 'queryInfo'
| 'showLabelAsterisk'
| 'title'
| 'queryInfo'
>;

interface BulkAddUpdateFormProps extends BaseProps {
Expand Down
Loading