diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 60a2646b52..442dd5c57b 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.3.0", + "version": "7.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.3.0", + "version": "7.3.1", "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 b5e284d630..94c8910ad6 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.3.0", + "version": "7.3.1", "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 ece69808c2..eebfc5ab91 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 7.3.1 +*Released*: 13 December 2025 +- Remove LSID column from provisioned sample tables +- Update `getUpdatedData()` utility method to only check for primary keys actually used in data iteration. + ### version 7.3.0 *Released*: 10 December 2025 - CharBuilderModal: add UI for legend position diff --git a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.test.tsx b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.test.tsx index ffb35ca3cb..a281da1a7c 100644 --- a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.test.tsx +++ b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.test.tsx @@ -17,7 +17,13 @@ import { renderWithAppContext } from '../../../test/reactTestLibraryHelpers'; import { TEST_LKS_STARTER_MODULE_CONTEXT } from '../../../productFixtures'; -import { SampleTypeDesigner, SampleTypeDesignerImpl } from './SampleTypeDesigner'; +import { + SampleTypeDesigner, + SampleTypeDesignerImpl, + SampleTypeDesignerImplProps, + SampleTypeDesignerProps, +} from './SampleTypeDesigner'; +import { getQueryTestAPIWrapper } from '../../../query/APIWrapper'; const SERVER_CONTEXT = { moduleContext: { @@ -57,65 +63,50 @@ const PARENT_OPTIONS = [ }, ]; -const BASE_PROPS = { - appPropertiesOnly: true, - onComplete: jest.fn(), - onCancel: jest.fn(), +const DESIGNER_PROPS: SampleTypeDesignerProps = { api: getTestAPIWrapper(jest.fn, { entity: getEntityTestAPIWrapper(jest.fn, { initParentOptionsSelects: jest.fn().mockResolvedValue({ parentOptions: PARENT_OPTIONS, parentAliases: Map(), }), + loadNameExpressionOptions: jest.fn().mockResolvedValue({}), + }), + query: getQueryTestAPIWrapper(jest.fn, { + selectRows: jest.fn().mockResolvedValue({ rows: [] }), }), }), + appPropertiesOnly: true, + onCancel: jest.fn(), + onComplete: jest.fn(), +}; + +const DESIGNER_IMPL_PROPS: SampleTypeDesignerImplProps = { + currentPanelIndex: 0, + firstState: true, + onFinish: jest.fn(), + onTogglePanel: jest.fn(), + setSubmitting: jest.fn(), + submitting: false, + validatePanel: 0, + visitedPanels: List(), + ...DESIGNER_PROPS, }; describe('SampleTypeDesigner', () => { test('default properties', async () => { - const form = ( - - ); - - renderWithAppContext(form, { - serverContext: SERVER_CONTEXT, - }); + renderWithAppContext(, { serverContext: SERVER_CONTEXT }); await waitFor(() => { expect(document.getElementsByClassName('domain-form-panel')).toHaveLength(2); }); const panelTitles = document.querySelectorAll('.domain-panel-title'); - expect(panelTitles[0].textContent).toBe('Sample Type Properties'); - expect(panelTitles[1].textContent).toBe('Fields'); + expect(panelTitles[0]).toHaveTextContent('Sample Type Properties'); + expect(panelTitles[1]).toHaveTextContent('Fields'); }); test('allowFolderExclusion', async () => { - const form = ( - - ); - - renderWithAppContext(form, { + renderWithAppContext(, { serverContext: SERVER_CONTEXT, }); @@ -123,15 +114,15 @@ describe('SampleTypeDesigner', () => { expect(document.getElementsByClassName('domain-form-panel')).toHaveLength(3); }); const panelTitles = document.querySelectorAll('.domain-panel-title'); - expect(panelTitles[0].textContent).toBe('Sample Type Properties'); - expect(panelTitles[1].textContent).toBe('Fields'); - expect(panelTitles[2].textContent).toBe('Folders'); + expect(panelTitles[0]).toHaveTextContent('Sample Type Properties'); + expect(panelTitles[1]).toHaveTextContent('Fields'); + expect(panelTitles[2]).toHaveTextContent('Folders'); }); test('initModel with name URL props', async () => { const form = ( { nameReadOnly: true, }) )} - currentPanelIndex={0} - firstState={true} - onFinish={jest.fn()} - onTogglePanel={jest.fn()} - setSubmitting={jest.fn()} - submitting={false} - validatePanel={0} - visitedPanels={List()} /> ); - renderWithAppContext(form, { - serverContext: SERVER_CONTEXT, - }); + renderWithAppContext(form, { serverContext: SERVER_CONTEXT }); await waitFor(() => { expect(document.querySelectorAll('.domain-form-panel')).toHaveLength(2); }); const panelTitles = document.querySelectorAll('.domain-panel-title'); - expect(panelTitles[0].textContent).toBe('Sample Type Properties'); - expect(panelTitles[1].textContent).toBe('Fields'); + expect(panelTitles[0]).toHaveTextContent('Sample Type Properties'); + expect(panelTitles[1]).toHaveTextContent('Fields'); expect(document.getElementsByClassName('translator--toggle__wizard')).toHaveLength(1); }); test('open fields panel, with barcodes', async () => { - renderWithAppContext(, { + // NOTE: Here we are calling the full designer, SampleTypeDesigner, not the SampleTypeDesignerImpl + renderWithAppContext(, { serverContext: { moduleContext: { ...TEST_LKS_STARTER_MODULE_CONTEXT, @@ -187,8 +169,7 @@ describe('SampleTypeDesigner', () => { const alerts = document.getElementsByClassName('alert'); // still expect to have only two alerts. We don't show the Barcode header in the file import panel. // Jest doesn't want to switch to that panel. - expect(alerts).toHaveLength(2); - expect(alerts[0].textContent).toEqual(PROPERTIES_PANEL_ERROR_MSG); - expect(alerts[1].textContent).toEqual('Please correct errors in the properties panel before saving.'); + expect(alerts[0]).toHaveTextContent(PROPERTIES_PANEL_ERROR_MSG); + expect(alerts[1]).toHaveTextContent('Please correct errors in the properties panel before saving.'); }); }); diff --git a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx index 464a7ee44f..eb3544cf58 100644 --- a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx +++ b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx @@ -1,5 +1,5 @@ import React, { FC, memo, ReactNode } from 'react'; -import { List, Map } from 'immutable'; +import { List } from 'immutable'; import { Domain, getServerContext } from '@labkey/api'; import { @@ -90,7 +90,8 @@ const AliquotOptionsHelp: FC<{ helpTopic: string }> = memo(({ helpTopic }) => { }); AliquotOptionsHelp.displayName = 'AliquotOptionsHelp'; -interface Props { +// Exported for testing +export interface SampleTypeDesignerProps { aliquotNamePatternProps?: AliquotNamePatternProps; allowFolderExclusion?: boolean; api?: ComponentsAPIWrapper; @@ -137,8 +138,12 @@ interface State { showUniqueIdConfirmation: boolean; uniqueIdsConfirmed: boolean; } + +// Exported for testing +export type SampleTypeDesignerImplProps = InjectedBaseDomainDesignerProps & SampleTypeDesignerProps; + // Exported for testing -export class SampleTypeDesignerImpl extends React.PureComponent { +export class SampleTypeDesignerImpl extends React.PureComponent { static defaultProps = { api: getDefaultAPIWrapper(), defaultSampleFieldConfig: DEFAULT_SAMPLE_FIELD_CONFIG, @@ -156,7 +161,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent this.saveDomain(false, comment ?? auditUserComment)); @@ -385,8 +390,8 @@ export class SampleTypeDesignerImpl extends React.PureComponent 0) { exception = 'Duplicate parent alias header found: ' + getDuplicateAlias(model.parentAliases, true).join(', '); - } else if (!model.isMetricUnitValid()) { - exception = metricUnitProps?.metricUnitLabel + ' field is required.'; + } else if (!model.isMetricUnitValid(metricUnitProps)) { + exception = (metricUnitProps?.metricUnitLabel ?? 'Units') + ' field is required.'; } else { exception = model.domain.getFirstFieldError(); } @@ -838,4 +843,4 @@ export class SampleTypeDesignerImpl extends React.PureComponent(SampleTypeDesignerImpl); +export const SampleTypeDesigner = withBaseDomainDesigner(SampleTypeDesignerImpl); diff --git a/packages/components/src/internal/components/domainproperties/samples/SampleTypePropertiesPanel.tsx b/packages/components/src/internal/components/domainproperties/samples/SampleTypePropertiesPanel.tsx index 3d9ea635f0..8e8ef2ed02 100644 --- a/packages/components/src/internal/components/domainproperties/samples/SampleTypePropertiesPanel.tsx +++ b/packages/components/src/internal/components/domainproperties/samples/SampleTypePropertiesPanel.tsx @@ -292,20 +292,16 @@ class SampleTypePropertiesPanelImpl extends PureComponent { - const { model, updateModel, metricUnitProps } = this.props; - - const updatedModel = newModel || model; - const isValid = updatedModel?.hasValidProperties() && updatedModel?.isMetricUnitValid(); - - this.setState( - () => ({ isValid }), - () => { - // Issue 39918: only consider the model changed if there is a newModel param - if (newModel) { - updateModel(updatedModel); - } + const { metricUnitProps, model, updateModel } = this.props; + const updatedModel = newModel ?? model; + const isValid = updatedModel.hasValidProperties() && updatedModel.isMetricUnitValid(metricUnitProps); + + this.setState({ isValid }, () => { + // Issue 39918: only consider the model changed if there is a newModel param + if (newModel) { + updateModel(newModel); } - ); + }); }; onFormChange = (evt: any): void => { diff --git a/packages/components/src/internal/components/domainproperties/samples/models.ts b/packages/components/src/internal/components/domainproperties/samples/models.ts index cabd917bde..1107160f14 100644 --- a/packages/components/src/internal/components/domainproperties/samples/models.ts +++ b/packages/components/src/internal/components/domainproperties/samples/models.ts @@ -1,4 +1,4 @@ -import { fromJS, Map, OrderedMap, Record } from 'immutable'; +import { OrderedMap, Record } from 'immutable'; import { DomainDesign, DomainDetails, IDomainField } from '../models'; import { IImportAlias, IParentAlias } from '../../entities/models'; @@ -74,18 +74,18 @@ export class SampleTypeModel extends Record({ return !this.rowId; } - isValid(defaultNameFieldConfig?: Partial) { + isValid(defaultNameFieldConfig?: Partial, metricUnitProps?: MetricUnitProps): boolean { return ( this.hasValidProperties() && !this.hasInvalidNameField(defaultNameFieldConfig) && getDuplicateAlias(this.parentAliases, true).size === 0 && !this.domain.hasInvalidFields() && - this.isMetricUnitValid() + this.isMetricUnitValid(metricUnitProps) ); } - isMetricUnitValid() { - return this.metricUnit != null; + isMetricUnitValid(metricUnitProps?: MetricUnitProps): boolean { + return !metricUnitProps?.includeMetricUnitProperty || this.metricUnit != null; } hasValidProperties(): boolean { diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index b30e0fa5cb..b70f0bd311 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -306,9 +306,10 @@ export function isSameWithStringCompare(value1: any, value2: any): boolean { } /** - * Constructs an array of objects (suitable for the rows parameter of updateRows) where each object contains the - * values that are different from the ones in originalData object as well as the primary key values for that row. - * If updatedValues is empty or all of the originalData values are the same as the updatedValues, returns an empty array. + * Constructs an array of objects, suitable for the "rows" parameter of updateRows, where each object contains the + * values that are different from the ones in the originalData object as well as the primary key values for that row. + * If updatedValues are empty, or all the originalData values are the same as the updatedValues, then it returns an + * empty array. * * @param originalData a map from an id field to a Map from fieldKeys to an object with a 'value' field * @param updatedValues an object mapping fieldKeys to values that are being updated @@ -323,10 +324,11 @@ export function getUpdatedData( ): any[] { const updateValuesMap = Map(updatedValues); const pkColsLc = new Set(); + const pkColsInUse = new Set(); queryInfo.pkCols.forEach(key => pkColsLc.add(key.toLowerCase())); additionalCols?.forEach(col => pkColsLc.add(col.toLowerCase())); - // if the originalData has the container/folder values, keep those as well (i.e. treat it as a primary key) + // if the originalData has the container/folder values, keep those as well (i.e., treat it as a primary key) const folderKey = originalData .first() .keySeq() @@ -353,6 +355,7 @@ export function getUpdatedData( if (fieldValueMap?.has('value')) { if (isPKCol) { + pkColsInUse.add(key.toLowerCase()); return m.set(key, fieldValueMap.get('value')); } @@ -399,7 +402,7 @@ export function getUpdatedData( }); // we want the rows that contain more than just the primaryKeys return updatedData - .filter(rowData => rowData.size > pkColsLc.size) + .filter(rowData => rowData.size > pkColsInUse.size) .map(rowData => rowData.toJS()) .toArray(); }