Skip to content

Commit 3cc0954

Browse files
authored
Remove LSID column from provisioned sample tables (#1901)
1 parent 0a1387e commit 3cc0954

8 files changed

Lines changed: 85 additions & 95 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "7.3.0",
3+
"version": "7.3.1",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.3.1
5+
*Released*: 13 December 2025
6+
- Remove LSID column from provisioned sample tables
7+
- Update `getUpdatedData()` utility method to only check for primary keys actually used in data iteration.
8+
49
### version 7.3.0
510
*Released*: 10 December 2025
611
- CharBuilderModal: add UI for legend position

packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.test.tsx

Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import { renderWithAppContext } from '../../../test/reactTestLibraryHelpers';
1717

1818
import { TEST_LKS_STARTER_MODULE_CONTEXT } from '../../../productFixtures';
1919

20-
import { SampleTypeDesigner, SampleTypeDesignerImpl } from './SampleTypeDesigner';
20+
import {
21+
SampleTypeDesigner,
22+
SampleTypeDesignerImpl,
23+
SampleTypeDesignerImplProps,
24+
SampleTypeDesignerProps,
25+
} from './SampleTypeDesigner';
26+
import { getQueryTestAPIWrapper } from '../../../query/APIWrapper';
2127

2228
const SERVER_CONTEXT = {
2329
moduleContext: {
@@ -57,81 +63,66 @@ const PARENT_OPTIONS = [
5763
},
5864
];
5965

60-
const BASE_PROPS = {
61-
appPropertiesOnly: true,
62-
onComplete: jest.fn(),
63-
onCancel: jest.fn(),
66+
const DESIGNER_PROPS: SampleTypeDesignerProps = {
6467
api: getTestAPIWrapper(jest.fn, {
6568
entity: getEntityTestAPIWrapper(jest.fn, {
6669
initParentOptionsSelects: jest.fn().mockResolvedValue({
6770
parentOptions: PARENT_OPTIONS,
6871
parentAliases: Map(),
6972
}),
73+
loadNameExpressionOptions: jest.fn().mockResolvedValue({}),
74+
}),
75+
query: getQueryTestAPIWrapper(jest.fn, {
76+
selectRows: jest.fn().mockResolvedValue({ rows: [] }),
7077
}),
7178
}),
79+
appPropertiesOnly: true,
80+
onCancel: jest.fn(),
81+
onComplete: jest.fn(),
82+
};
83+
84+
const DESIGNER_IMPL_PROPS: SampleTypeDesignerImplProps = {
85+
currentPanelIndex: 0,
86+
firstState: true,
87+
onFinish: jest.fn(),
88+
onTogglePanel: jest.fn(),
89+
setSubmitting: jest.fn(),
90+
submitting: false,
91+
validatePanel: 0,
92+
visitedPanels: List(),
93+
...DESIGNER_PROPS,
7294
};
7395

7496
describe('SampleTypeDesigner', () => {
7597
test('default properties', async () => {
76-
const form = (
77-
<SampleTypeDesignerImpl
78-
{...BASE_PROPS}
79-
currentPanelIndex={0}
80-
firstState={true}
81-
onFinish={jest.fn()}
82-
onTogglePanel={jest.fn()}
83-
setSubmitting={jest.fn()}
84-
submitting={false}
85-
validatePanel={0}
86-
visitedPanels={List()}
87-
/>
88-
);
89-
90-
renderWithAppContext(form, {
91-
serverContext: SERVER_CONTEXT,
92-
});
98+
renderWithAppContext(<SampleTypeDesignerImpl {...DESIGNER_IMPL_PROPS} />, { serverContext: SERVER_CONTEXT });
9399

94100
await waitFor(() => {
95101
expect(document.getElementsByClassName('domain-form-panel')).toHaveLength(2);
96102
});
97103
const panelTitles = document.querySelectorAll('.domain-panel-title');
98-
expect(panelTitles[0].textContent).toBe('Sample Type Properties');
99-
expect(panelTitles[1].textContent).toBe('Fields');
104+
expect(panelTitles[0]).toHaveTextContent('Sample Type Properties');
105+
expect(panelTitles[1]).toHaveTextContent('Fields');
100106
});
101107

102108
test('allowFolderExclusion', async () => {
103-
const form = (
104-
<SampleTypeDesignerImpl
105-
{...BASE_PROPS}
106-
currentPanelIndex={0}
107-
firstState={true}
108-
onFinish={jest.fn()}
109-
onTogglePanel={jest.fn()}
110-
setSubmitting={jest.fn()}
111-
submitting={false}
112-
validatePanel={0}
113-
visitedPanels={List()}
114-
allowFolderExclusion
115-
/>
116-
);
117-
118-
renderWithAppContext(form, {
109+
renderWithAppContext(<SampleTypeDesignerImpl {...DESIGNER_IMPL_PROPS} allowFolderExclusion />, {
119110
serverContext: SERVER_CONTEXT,
120111
});
121112

122113
await waitFor(() => {
123114
expect(document.getElementsByClassName('domain-form-panel')).toHaveLength(3);
124115
});
125116
const panelTitles = document.querySelectorAll('.domain-panel-title');
126-
expect(panelTitles[0].textContent).toBe('Sample Type Properties');
127-
expect(panelTitles[1].textContent).toBe('Fields');
128-
expect(panelTitles[2].textContent).toBe('Folders');
117+
expect(panelTitles[0]).toHaveTextContent('Sample Type Properties');
118+
expect(panelTitles[1]).toHaveTextContent('Fields');
119+
expect(panelTitles[2]).toHaveTextContent('Folders');
129120
});
130121

131122
test('initModel with name URL props', async () => {
132123
const form = (
133124
<SampleTypeDesignerImpl
134-
{...BASE_PROPS}
125+
{...DESIGNER_IMPL_PROPS}
135126
domainFormDisplayOptions={{
136127
hideConditionalFormatting: true,
137128
}}
@@ -146,31 +137,22 @@ describe('SampleTypeDesigner', () => {
146137
nameReadOnly: true,
147138
})
148139
)}
149-
currentPanelIndex={0}
150-
firstState={true}
151-
onFinish={jest.fn()}
152-
onTogglePanel={jest.fn()}
153-
setSubmitting={jest.fn()}
154-
submitting={false}
155-
validatePanel={0}
156-
visitedPanels={List()}
157140
/>
158141
);
159-
renderWithAppContext(form, {
160-
serverContext: SERVER_CONTEXT,
161-
});
142+
renderWithAppContext(form, { serverContext: SERVER_CONTEXT });
162143

163144
await waitFor(() => {
164145
expect(document.querySelectorAll('.domain-form-panel')).toHaveLength(2);
165146
});
166147
const panelTitles = document.querySelectorAll('.domain-panel-title');
167-
expect(panelTitles[0].textContent).toBe('Sample Type Properties');
168-
expect(panelTitles[1].textContent).toBe('Fields');
148+
expect(panelTitles[0]).toHaveTextContent('Sample Type Properties');
149+
expect(panelTitles[1]).toHaveTextContent('Fields');
169150
expect(document.getElementsByClassName('translator--toggle__wizard')).toHaveLength(1);
170151
});
171152

172153
test('open fields panel, with barcodes', async () => {
173-
renderWithAppContext(<SampleTypeDesigner {...BASE_PROPS} />, {
154+
// NOTE: Here we are calling the full designer, SampleTypeDesigner, not the SampleTypeDesignerImpl
155+
renderWithAppContext(<SampleTypeDesigner {...DESIGNER_PROPS} />, {
174156
serverContext: {
175157
moduleContext: {
176158
...TEST_LKS_STARTER_MODULE_CONTEXT,
@@ -187,8 +169,7 @@ describe('SampleTypeDesigner', () => {
187169
const alerts = document.getElementsByClassName('alert');
188170
// still expect to have only two alerts. We don't show the Barcode header in the file import panel.
189171
// Jest doesn't want to switch to that panel.
190-
expect(alerts).toHaveLength(2);
191-
expect(alerts[0].textContent).toEqual(PROPERTIES_PANEL_ERROR_MSG);
192-
expect(alerts[1].textContent).toEqual('Please correct errors in the properties panel before saving.');
172+
expect(alerts[0]).toHaveTextContent(PROPERTIES_PANEL_ERROR_MSG);
173+
expect(alerts[1]).toHaveTextContent('Please correct errors in the properties panel before saving.');
193174
});
194175
});

packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { FC, memo, ReactNode } from 'react';
2-
import { List, Map } from 'immutable';
2+
import { List } from 'immutable';
33
import { Domain, getServerContext } from '@labkey/api';
44

55
import {
@@ -90,7 +90,8 @@ const AliquotOptionsHelp: FC<{ helpTopic: string }> = memo(({ helpTopic }) => {
9090
});
9191
AliquotOptionsHelp.displayName = 'AliquotOptionsHelp';
9292

93-
interface Props {
93+
// Exported for testing
94+
export interface SampleTypeDesignerProps {
9495
aliquotNamePatternProps?: AliquotNamePatternProps;
9596
allowFolderExclusion?: boolean;
9697
api?: ComponentsAPIWrapper;
@@ -137,8 +138,12 @@ interface State {
137138
showUniqueIdConfirmation: boolean;
138139
uniqueIdsConfirmed: boolean;
139140
}
141+
142+
// Exported for testing
143+
export type SampleTypeDesignerImplProps = InjectedBaseDomainDesignerProps & SampleTypeDesignerProps;
144+
140145
// Exported for testing
141-
export class SampleTypeDesignerImpl extends React.PureComponent<InjectedBaseDomainDesignerProps & Props, State> {
146+
export class SampleTypeDesignerImpl extends React.PureComponent<SampleTypeDesignerImplProps, State> {
142147
static defaultProps = {
143148
api: getDefaultAPIWrapper(),
144149
defaultSampleFieldConfig: DEFAULT_SAMPLE_FIELD_CONFIG,
@@ -156,7 +161,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent<InjectedBaseDoma
156161
validateNameExpressions: true,
157162
};
158163

159-
constructor(props: InjectedBaseDomainDesignerProps & Props) {
164+
constructor(props: SampleTypeDesignerImplProps) {
160165
super(props);
161166

162167
let domainDetails = this.props.initModel || DomainDetails.create();
@@ -369,7 +374,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent<InjectedBaseDoma
369374
return;
370375
}
371376

372-
const isValid = model.isValid(defaultSampleFieldConfig);
377+
const isValid = model.isValid(defaultSampleFieldConfig, metricUnitProps);
373378

374379
this.props.onFinish(isValid, () => this.saveDomain(false, comment ?? auditUserComment));
375380

@@ -385,8 +390,8 @@ export class SampleTypeDesignerImpl extends React.PureComponent<InjectedBaseDoma
385390
} else if (getDuplicateAlias(model.parentAliases, true).size > 0) {
386391
exception =
387392
'Duplicate parent alias header found: ' + getDuplicateAlias(model.parentAliases, true).join(', ');
388-
} else if (!model.isMetricUnitValid()) {
389-
exception = metricUnitProps?.metricUnitLabel + ' field is required.';
393+
} else if (!model.isMetricUnitValid(metricUnitProps)) {
394+
exception = (metricUnitProps?.metricUnitLabel ?? 'Units') + ' field is required.';
390395
} else {
391396
exception = model.domain.getFirstFieldError();
392397
}
@@ -838,4 +843,4 @@ export class SampleTypeDesignerImpl extends React.PureComponent<InjectedBaseDoma
838843
}
839844
}
840845

841-
export const SampleTypeDesigner = withBaseDomainDesigner<Props>(SampleTypeDesignerImpl);
846+
export const SampleTypeDesigner = withBaseDomainDesigner<SampleTypeDesignerProps>(SampleTypeDesignerImpl);

packages/components/src/internal/components/domainproperties/samples/SampleTypePropertiesPanel.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,16 @@ class SampleTypePropertiesPanelImpl extends PureComponent<InjectedDomainProperti
292292
};
293293

294294
updateValidStatus = (newModel?: SampleTypeModel): void => {
295-
const { model, updateModel, metricUnitProps } = this.props;
296-
297-
const updatedModel = newModel || model;
298-
const isValid = updatedModel?.hasValidProperties() && updatedModel?.isMetricUnitValid();
299-
300-
this.setState(
301-
() => ({ isValid }),
302-
() => {
303-
// Issue 39918: only consider the model changed if there is a newModel param
304-
if (newModel) {
305-
updateModel(updatedModel);
306-
}
295+
const { metricUnitProps, model, updateModel } = this.props;
296+
const updatedModel = newModel ?? model;
297+
const isValid = updatedModel.hasValidProperties() && updatedModel.isMetricUnitValid(metricUnitProps);
298+
299+
this.setState({ isValid }, () => {
300+
// Issue 39918: only consider the model changed if there is a newModel param
301+
if (newModel) {
302+
updateModel(newModel);
307303
}
308-
);
304+
});
309305
};
310306

311307
onFormChange = (evt: any): void => {

packages/components/src/internal/components/domainproperties/samples/models.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fromJS, Map, OrderedMap, Record } from 'immutable';
1+
import { OrderedMap, Record } from 'immutable';
22

33
import { DomainDesign, DomainDetails, IDomainField } from '../models';
44
import { IImportAlias, IParentAlias } from '../../entities/models';
@@ -74,18 +74,18 @@ export class SampleTypeModel extends Record({
7474
return !this.rowId;
7575
}
7676

77-
isValid(defaultNameFieldConfig?: Partial<IDomainField>) {
77+
isValid(defaultNameFieldConfig?: Partial<IDomainField>, metricUnitProps?: MetricUnitProps): boolean {
7878
return (
7979
this.hasValidProperties() &&
8080
!this.hasInvalidNameField(defaultNameFieldConfig) &&
8181
getDuplicateAlias(this.parentAliases, true).size === 0 &&
8282
!this.domain.hasInvalidFields() &&
83-
this.isMetricUnitValid()
83+
this.isMetricUnitValid(metricUnitProps)
8484
);
8585
}
8686

87-
isMetricUnitValid() {
88-
return this.metricUnit != null;
87+
isMetricUnitValid(metricUnitProps?: MetricUnitProps): boolean {
88+
return !metricUnitProps?.includeMetricUnitProperty || this.metricUnit != null;
8989
}
9090

9191
hasValidProperties(): boolean {

packages/components/src/internal/util/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,10 @@ export function isSameWithStringCompare(value1: any, value2: any): boolean {
306306
}
307307

308308
/**
309-
* Constructs an array of objects (suitable for the rows parameter of updateRows) where each object contains the
310-
* values that are different from the ones in originalData object as well as the primary key values for that row.
311-
* If updatedValues is empty or all of the originalData values are the same as the updatedValues, returns an empty array.
309+
* Constructs an array of objects, suitable for the "rows" parameter of updateRows, where each object contains the
310+
* values that are different from the ones in the originalData object as well as the primary key values for that row.
311+
* If updatedValues are empty, or all the originalData values are the same as the updatedValues, then it returns an
312+
* empty array.
312313
*
313314
* @param originalData a map from an id field to a Map from fieldKeys to an object with a 'value' field
314315
* @param updatedValues an object mapping fieldKeys to values that are being updated
@@ -323,10 +324,11 @@ export function getUpdatedData(
323324
): any[] {
324325
const updateValuesMap = Map<any, any>(updatedValues);
325326
const pkColsLc = new Set<string>();
327+
const pkColsInUse = new Set<string>();
326328
queryInfo.pkCols.forEach(key => pkColsLc.add(key.toLowerCase()));
327329
additionalCols?.forEach(col => pkColsLc.add(col.toLowerCase()));
328330

329-
// if the originalData has the container/folder values, keep those as well (i.e. treat it as a primary key)
331+
// if the originalData has the container/folder values, keep those as well (i.e., treat it as a primary key)
330332
const folderKey = originalData
331333
.first()
332334
.keySeq()
@@ -353,6 +355,7 @@ export function getUpdatedData(
353355

354356
if (fieldValueMap?.has('value')) {
355357
if (isPKCol) {
358+
pkColsInUse.add(key.toLowerCase());
356359
return m.set(key, fieldValueMap.get('value'));
357360
}
358361

@@ -399,7 +402,7 @@ export function getUpdatedData(
399402
});
400403
// we want the rows that contain more than just the primaryKeys
401404
return updatedData
402-
.filter(rowData => rowData.size > pkColsLc.size)
405+
.filter(rowData => rowData.size > pkColsInUse.size)
403406
.map(rowData => rowData.toJS())
404407
.toArray();
405408
}

0 commit comments

Comments
 (0)