Skip to content

Commit 02a8168

Browse files
authored
GitHub Issue 73: Field editor Advanced Settings to allow for non-unique single-field index (#1915)
### version 7.11.0 *Released*: 6 January 2026 - GitHub Issue 73: Field editor Advanced Settings to allow for non-unique constraint / index - update UI to allow for single field non-unique index and unique constraint via select dropdown - update DomainField model to support nonUniqueConstraint in addition to uniqueConstraint
1 parent 96eb767 commit 02a8168

File tree

8 files changed

+164
-56
lines changed

8 files changed

+164
-56
lines changed

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.10.0",
3+
"version": "7.11.0",
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.11.0
5+
*Released*: 6 January 2026
6+
- GitHub Issue 73: Field editor Advanced Settings to allow for non-unique constraint / index
7+
- update UI to allow for single field non-unique index and unique constraint via select dropdown
8+
- update DomainField model to support nonUniqueConstraint in addition to uniqueConstraint
9+
410
### version 7.10.0
511
*Released*: 6 January 2026
612
- GridColumn: remove width, fixedWidth properties

packages/components/src/internal/components/domainproperties/AdvancedSettings.test.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createFormInputId } from './utils';
77
import {
88
CALCULATED_CONCEPT_URI,
99
DOMAIN_EDITABLE_DEFAULT,
10+
DOMAIN_FIELD_CONSTRAINT,
1011
DOMAIN_FIELD_DEFAULT_VALUE_TYPE,
1112
DOMAIN_FIELD_DIMENSION,
1213
DOMAIN_FIELD_HIDDEN,
@@ -128,9 +129,14 @@ describe('AdvancedSettings', () => {
128129
expect(recommendedVariable.getAttribute('checked')).toEqual('');
129130

130131
// Verify uniqueConstraint
131-
id = createFormInputId(DOMAIN_FIELD_UNIQUECONSTRAINT, _domainIndex, _index);
132-
const uniqueConstraint = document.querySelector('#' + id);
133-
expect(uniqueConstraint.getAttribute('checked')).toEqual('');
132+
id = createFormInputId(DOMAIN_FIELD_CONSTRAINT, _domainIndex, _index);
133+
const singleFieldIndex = document.querySelector('#' + id);
134+
expect(singleFieldIndex.getAttribute('disabled')).toBeNull();
135+
let options = singleFieldIndex.querySelectorAll('option');
136+
expect(options).toHaveLength(3);
137+
expect(options[0].textContent).toBe('No Index');
138+
expect(options[1].textContent).toBe('Index');
139+
expect(options[2].textContent).toBe('Index and require unique values');
134140

135141
// Verify default type
136142
id = createFormInputId(DOMAIN_FIELD_DEFAULT_VALUE_TYPE, _domainIndex, _index);
@@ -148,8 +154,7 @@ describe('AdvancedSettings', () => {
148154
id = createFormInputId(DOMAIN_FIELD_PHI, _domainIndex, _index);
149155
const phi = document.querySelector('#' + id);
150156
expect(phi.getAttribute('disabled')).toBeNull();
151-
152-
const options = phi.querySelectorAll('option');
157+
options = phi.querySelectorAll('option');
153158
expect(options).toHaveLength(3);
154159
expect(options[0].textContent).toBe('Not PHI');
155160
expect(options[1].textContent).toBe('Limited PHI');

packages/components/src/internal/components/domainproperties/AdvancedSettings.tsx

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import {
2525
DEFAULT_DOMAIN_FORM_DISPLAY_OPTIONS,
2626
DOMAIN_DEFAULT_TYPES,
2727
DOMAIN_EDITABLE_DEFAULT,
28+
DOMAIN_FIELD_CONSTRAINT,
2829
DOMAIN_FIELD_DEFAULT_VALUE_TYPE,
2930
DOMAIN_FIELD_DIMENSION,
3031
DOMAIN_FIELD_EXCLUDE_FROM_SHIFTING,
3132
DOMAIN_FIELD_HIDDEN,
3233
DOMAIN_FIELD_MEASURE,
3334
DOMAIN_FIELD_MVENABLED,
35+
DOMAIN_FIELD_NONUNIQUECONSTRAINT,
3436
DOMAIN_FIELD_PHI,
3537
DOMAIN_FIELD_RECOMMENDEDVARIABLE,
3638
DOMAIN_FIELD_SHOWNINDETAILSVIEW,
@@ -67,6 +69,7 @@ interface AdvancedSettingsState {
6769
hidden?: boolean;
6870
measure?: boolean;
6971
mvEnabled?: boolean;
72+
nonUniqueConstraint?: boolean;
7073
PHI?: string;
7174
phiLevels?: { label: string; value: string }[];
7275
recommendedVariable?: boolean;
@@ -117,6 +120,7 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
117120
recommendedVariable: field.recommendedVariable,
118121
excludeFromShifting: field.excludeFromShifting,
119122
uniqueConstraint: field.uniqueConstraint,
123+
nonUniqueConstraint: field.nonUniqueConstraint,
120124
PHI: field.PHI,
121125
phiLevels,
122126
};
@@ -178,6 +182,27 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
178182
});
179183
};
180184

185+
handleSingleFieldIndexChange = evt => {
186+
// only one of uniqueConstraint or nonUniqueConstraint can be true at a time
187+
const value = evt.target.value;
188+
if (value === DOMAIN_FIELD_UNIQUECONSTRAINT) {
189+
this.setState({
190+
uniqueConstraint: true,
191+
nonUniqueConstraint: false,
192+
});
193+
} else if (value === DOMAIN_FIELD_NONUNIQUECONSTRAINT) {
194+
this.setState({
195+
uniqueConstraint: false,
196+
nonUniqueConstraint: true,
197+
});
198+
} else {
199+
this.setState({
200+
uniqueConstraint: false,
201+
nonUniqueConstraint: false,
202+
});
203+
}
204+
};
205+
181206
hasValidDomainId(): boolean {
182207
const { domainId } = this.props;
183208
return !(domainId === undefined || domainId === null || domainId === 0);
@@ -217,6 +242,15 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
217242
);
218243
};
219244

245+
getSingleFieldIndexHelpText = () => {
246+
return (
247+
<div>
248+
<p>Add a single-field database index for this field.</p>
249+
<p>Optionally, also require all values to be unique for this field.</p>
250+
</div>
251+
);
252+
};
253+
220254
getDefaultTypeHelpText = () => {
221255
return (
222256
<div>
@@ -364,10 +398,17 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
364398
mvEnabled,
365399
recommendedVariable,
366400
uniqueConstraint,
401+
nonUniqueConstraint,
367402
PHI,
368403
excludeFromShifting,
369404
phiLevels,
370405
} = this.state;
406+
const singleFieldConstraintType =
407+
uniqueConstraint || field.isPrimaryKey
408+
? DOMAIN_FIELD_UNIQUECONSTRAINT
409+
: nonUniqueConstraint
410+
? DOMAIN_FIELD_NONUNIQUECONSTRAINT
411+
: '';
371412
const currentValueExists = phiLevels?.find(level => level.value === PHI) !== undefined;
372413
const disablePhiSelect =
373414
domainFormDisplayOptions.phiLevelDisabled ||
@@ -378,8 +419,8 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
378419
<>
379420
<div className="domain-adv-misc-options">Miscellaneous Options</div>
380421
{!field.isCalculatedField() && (
381-
<div className="row">
382-
<div className="col-xs-3">
422+
<div className="row domain-adv-thick-row">
423+
<div className="col-xs-4">
383424
<DomainFieldLabel helpTipBody={this.getPhiHelpText()} label="PHI Level" />
384425
</div>
385426
<div className="col-xs-6">
@@ -403,7 +444,39 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
403444
))}
404445
</select>
405446
</div>
406-
<div className="col-xs-3" />
447+
<div className="col-xs-2" />
448+
</div>
449+
)}
450+
451+
{allowUniqueConstraintProperties && !field.isCalculatedField() && (
452+
<div className="row">
453+
<div className="col-xs-4">
454+
<DomainFieldLabel
455+
helpTipBody={this.getSingleFieldIndexHelpText()}
456+
label="Uniqueness & Index"
457+
/>
458+
</div>
459+
<div className="col-xs-6">
460+
<select
461+
className="form-control"
462+
disabled={field.isPrimaryKey}
463+
id={createFormInputId(DOMAIN_FIELD_CONSTRAINT, domainIndex, index)}
464+
name={createFormInputName(DOMAIN_FIELD_CONSTRAINT)}
465+
onChange={this.handleSingleFieldIndexChange}
466+
value={singleFieldConstraintType}
467+
>
468+
<option key="None" value="">
469+
No Index
470+
</option>
471+
<option key="Non-Unique" value={DOMAIN_FIELD_NONUNIQUECONSTRAINT}>
472+
Index
473+
</option>
474+
<option key="Unique" value={DOMAIN_FIELD_UNIQUECONSTRAINT}>
475+
Index and require unique values
476+
</option>
477+
</select>
478+
</div>
479+
<div className="col-xs-2" />
407480
</div>
408481
)}
409482
{field.dataType === DATETIME_TYPE && (
@@ -508,20 +581,6 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
508581
</LabelHelpTip>
509582
</CheckboxLK>
510583
)}
511-
{allowUniqueConstraintProperties && !field.isCalculatedField() && (
512-
<CheckboxLK
513-
checked={uniqueConstraint || field.isPrimaryKey}
514-
disabled={field.isPrimaryKey}
515-
id={createFormInputId(DOMAIN_FIELD_UNIQUECONSTRAINT, domainIndex, index)}
516-
name={createFormInputName(DOMAIN_FIELD_UNIQUECONSTRAINT)}
517-
onChange={this.handleCheckbox}
518-
>
519-
Require all values to be unique
520-
<LabelHelpTip title="Unique Constraint">
521-
<div>Add a unique constraint via a database-level index for this field.</div>
522-
</LabelHelpTip>
523-
</CheckboxLK>
524-
)}
525584
</>
526585
);
527586
};

packages/components/src/internal/components/domainproperties/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export const DOMAIN_FIELD_DIMENSION = 'dimension';
4040
export const DOMAIN_FIELD_HIDDEN = 'hidden';
4141
export const DOMAIN_FIELD_MVENABLED = 'mvEnabled';
4242
export const DOMAIN_FIELD_PHI = 'PHI';
43+
export const DOMAIN_FIELD_CONSTRAINT = 'singleFieldConstraint';
4344
export const DOMAIN_FIELD_UNIQUECONSTRAINT = 'uniqueConstraint';
45+
export const DOMAIN_FIELD_NONUNIQUECONSTRAINT = 'nonUniqueConstraint';
4446
export const DOMAIN_FIELD_RECOMMENDEDVARIABLE = 'recommendedVariable';
4547
export const DOMAIN_FIELD_SHOWNINDETAILSVIEW = 'shownInDetailsView';
4648
export const DOMAIN_FIELD_SHOWNININSERTVIEW = 'shownInInsertView';

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -827,17 +827,17 @@ describe('DomainDesign', () => {
827827
{ columnNames: ['a', 'b', 'c'], unique: true },
828828
{ columnNames: ['a'], unique: true },
829829
{ columnNames: ['b'], unique: false },
830-
{ columnNames: ['c'], unique: true },
830+
{ columnNames: ['c'], unique: true }, // should be omitted since 'c' is not a field
831831
],
832832
});
833833
const ddJson = DomainDesign.serialize(dd);
834834
expect(ddJson.indices.length).toBe(3);
835835
expect(ddJson.indices[0].columnNames).toStrictEqual(['a', 'b', 'c']);
836836
expect(ddJson.indices[0].unique).toBe(true);
837-
expect(ddJson.indices[1].columnNames).toStrictEqual(['b']);
838-
expect(ddJson.indices[1].unique).toBe(false);
839-
expect(ddJson.indices[2].columnNames).toStrictEqual(['a']);
840-
expect(ddJson.indices[2].unique).toBe(true);
837+
expect(ddJson.indices[1].columnNames).toStrictEqual(['a']);
838+
expect(ddJson.indices[1].unique).toBe(true);
839+
expect(ddJson.indices[2].columnNames).toStrictEqual(['b']);
840+
expect(ddJson.indices[2].unique).toBe(false);
841841
});
842842
});
843843

@@ -1211,8 +1211,26 @@ describe('DomainField', () => {
12111211
List.of('A', 'b', 'd')
12121212
);
12131213
expect(fields.get(0).uniqueConstraint).toBe(true); // field a
1214+
expect(fields.get(0).nonUniqueConstraint).toBe(false); // field a
1215+
expect(fields.get(1).uniqueConstraint).toBe(true); // field b
1216+
expect(fields.get(1).nonUniqueConstraint).toBe(false); // field b
1217+
expect(fields.get(2).uniqueConstraint).toBe(false); // field c
1218+
expect(fields.get(2).nonUniqueConstraint).toBe(false); // field c
1219+
});
1220+
1221+
test('nonUniqueConstraintFieldNames in fromJS', () => {
1222+
const fields = DomainField.fromJS(
1223+
[{ name: 'a' } as IDomainField, { name: 'b' } as IDomainField, { name: 'c' } as IDomainField],
1224+
undefined,
1225+
List.of('A', 'b', 'd'),
1226+
List.of('c', 'd')
1227+
);
1228+
expect(fields.get(0).uniqueConstraint).toBe(true); // field a
1229+
expect(fields.get(0).nonUniqueConstraint).toBe(false); // field a
12141230
expect(fields.get(1).uniqueConstraint).toBe(true); // field b
1231+
expect(fields.get(1).nonUniqueConstraint).toBe(false); // field b
12151232
expect(fields.get(2).uniqueConstraint).toBe(false); // field c
1233+
expect(fields.get(2).nonUniqueConstraint).toBe(true); // field c
12161234
});
12171235

12181236
// TODO add other test cases for DomainField.serialize code
@@ -1228,17 +1246,13 @@ describe('DomainIndex', () => {
12281246
expect(index.isSingleFieldUniqueConstraint()).toBe(false);
12291247
});
12301248

1231-
test('isMSSQLHashedSingleFieldUniqueConstraint', () => {
1232-
let index = DomainIndex.fromJS([{ columnNames: ['a'], unique: true } as IDomainIndex]).get(0);
1233-
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
1234-
index = DomainIndex.fromJS([{ columnNames: ['a'], unique: false } as IDomainIndex]).get(0);
1235-
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
1236-
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a'], unique: true } as IDomainIndex]).get(0);
1237-
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
1238-
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a'], unique: false } as IDomainIndex]).get(0);
1239-
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(true);
1240-
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a', 'b'], unique: false } as IDomainIndex]).get(0);
1241-
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
1249+
test('isSingleFieldNonUniqueConstraint', () => {
1250+
let index = DomainIndex.fromJS([{ columnNames: ['a'], unique: false } as IDomainIndex]).get(0);
1251+
expect(index.isSingleFieldNonUniqueConstraint()).toBe(true);
1252+
index = DomainIndex.fromJS([{ columnNames: ['a'], unique: true } as IDomainIndex]).get(0);
1253+
expect(index.isSingleFieldNonUniqueConstraint()).toBe(false);
1254+
index = DomainIndex.fromJS([{ columnNames: ['a', 'b'], unique: false } as IDomainIndex]).get(0);
1255+
expect(index.isSingleFieldNonUniqueConstraint()).toBe(false);
12421256
});
12431257
});
12441258

0 commit comments

Comments
 (0)