Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2f7ab58
GitHub Issue 73: Field editor Advanced Settings to allow for non-uniq…
cnathe Dec 29, 2025
928cdf8
7.7.1-fb-nonUniqueConst73.0
cnathe Dec 29, 2025
6729b51
change to select input for single-field index options (unique vs non-…
cnathe Dec 29, 2025
3b95a81
7.7.1-fb-nonUniqueConst73.1
cnathe Dec 29, 2025
dfa1fbc
text updates per feedback
cnathe Dec 29, 2025
0445c33
7.7.1-fb-nonUniqueConst73.2
cnathe Dec 29, 2025
7e89d86
Merge remote-tracking branch 'origin/develop' into fb_nonUniqueConst73
cnathe Dec 30, 2025
6a6d578
7.7.1-fb-nonUniqueConst73.3
cnathe Dec 30, 2025
c53df40
update select option wording to be more descriptive
cnathe Dec 30, 2025
08ce72d
jest test updates to match wording change
cnathe Dec 30, 2025
4dc2ade
7.7.1-fb-nonUniqueConst73.4
cnathe Dec 30, 2025
d3ff44c
fix for handling of SQL Server special case for unique constraint _ha…
cnathe Dec 30, 2025
6f889de
7.7.1-fb-nonUniqueConst73.5
cnathe Dec 30, 2025
7ab8132
Merge remote-tracking branch 'origin/develop' into fb_nonUniqueConst73
cnathe Jan 2, 2026
e36e660
7.7.3-fb-nonUniqueConst73.0
cnathe Jan 2, 2026
b9bba16
Merge remote-tracking branch 'origin/develop' into fb_nonUniqueConst73
cnathe Jan 6, 2026
c1b10e9
7.8.1-fb-nonUniqueConst73.0
cnathe Jan 6, 2026
e49c459
Merge remote-tracking branch 'origin/develop' into fb_nonUniqueConst73
cnathe Jan 6, 2026
3d5459f
7.11.0
cnathe Jan 6, 2026
80c822d
Update release notes with version number and release date
cnathe Jan 6, 2026
3149027
npm run lint-branch-fix
cnathe Jan 6, 2026
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.10.0",
"version": "7.11.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
6 changes: 6 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### 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

### version 7.10.0
*Released*: 6 January 2026
- GridColumn: remove width, fixedWidth properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createFormInputId } from './utils';
import {
CALCULATED_CONCEPT_URI,
DOMAIN_EDITABLE_DEFAULT,
DOMAIN_FIELD_CONSTRAINT,
DOMAIN_FIELD_DEFAULT_VALUE_TYPE,
DOMAIN_FIELD_DIMENSION,
DOMAIN_FIELD_HIDDEN,
Expand Down Expand Up @@ -128,9 +129,14 @@ describe('AdvancedSettings', () => {
expect(recommendedVariable.getAttribute('checked')).toEqual('');

// Verify uniqueConstraint
id = createFormInputId(DOMAIN_FIELD_UNIQUECONSTRAINT, _domainIndex, _index);
const uniqueConstraint = document.querySelector('#' + id);
expect(uniqueConstraint.getAttribute('checked')).toEqual('');
id = createFormInputId(DOMAIN_FIELD_CONSTRAINT, _domainIndex, _index);
const singleFieldIndex = document.querySelector('#' + id);
expect(singleFieldIndex.getAttribute('disabled')).toBeNull();
let options = singleFieldIndex.querySelectorAll('option');
expect(options).toHaveLength(3);
expect(options[0].textContent).toBe('No Index');
expect(options[1].textContent).toBe('Index');
expect(options[2].textContent).toBe('Index and require unique values');

// Verify default type
id = createFormInputId(DOMAIN_FIELD_DEFAULT_VALUE_TYPE, _domainIndex, _index);
Expand All @@ -148,8 +154,7 @@ describe('AdvancedSettings', () => {
id = createFormInputId(DOMAIN_FIELD_PHI, _domainIndex, _index);
const phi = document.querySelector('#' + id);
expect(phi.getAttribute('disabled')).toBeNull();

const options = phi.querySelectorAll('option');
options = phi.querySelectorAll('option');
expect(options).toHaveLength(3);
expect(options[0].textContent).toBe('Not PHI');
expect(options[1].textContent).toBe('Limited PHI');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import {
DEFAULT_DOMAIN_FORM_DISPLAY_OPTIONS,
DOMAIN_DEFAULT_TYPES,
DOMAIN_EDITABLE_DEFAULT,
DOMAIN_FIELD_CONSTRAINT,
DOMAIN_FIELD_DEFAULT_VALUE_TYPE,
DOMAIN_FIELD_DIMENSION,
DOMAIN_FIELD_EXCLUDE_FROM_SHIFTING,
DOMAIN_FIELD_HIDDEN,
DOMAIN_FIELD_MEASURE,
DOMAIN_FIELD_MVENABLED,
DOMAIN_FIELD_NONUNIQUECONSTRAINT,
DOMAIN_FIELD_PHI,
DOMAIN_FIELD_RECOMMENDEDVARIABLE,
DOMAIN_FIELD_SHOWNINDETAILSVIEW,
Expand Down Expand Up @@ -67,6 +69,7 @@ interface AdvancedSettingsState {
hidden?: boolean;
measure?: boolean;
mvEnabled?: boolean;
nonUniqueConstraint?: boolean;
PHI?: string;
phiLevels?: { label: string; value: string }[];
recommendedVariable?: boolean;
Expand Down Expand Up @@ -117,6 +120,7 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
recommendedVariable: field.recommendedVariable,
excludeFromShifting: field.excludeFromShifting,
uniqueConstraint: field.uniqueConstraint,
nonUniqueConstraint: field.nonUniqueConstraint,
PHI: field.PHI,
phiLevels,
};
Expand Down Expand Up @@ -178,6 +182,27 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
});
};

handleSingleFieldIndexChange = evt => {
// only one of uniqueConstraint or nonUniqueConstraint can be true at a time
const value = evt.target.value;
if (value === DOMAIN_FIELD_UNIQUECONSTRAINT) {
this.setState({
uniqueConstraint: true,
nonUniqueConstraint: false,
});
} else if (value === DOMAIN_FIELD_NONUNIQUECONSTRAINT) {
this.setState({
uniqueConstraint: false,
nonUniqueConstraint: true,
});
} else {
this.setState({
uniqueConstraint: false,
nonUniqueConstraint: false,
});
}
};

hasValidDomainId(): boolean {
const { domainId } = this.props;
return !(domainId === undefined || domainId === null || domainId === 0);
Expand Down Expand Up @@ -217,6 +242,15 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
);
};

getSingleFieldIndexHelpText = () => {
return (
<div>
<p>Add a single-field database index for this field.</p>
<p>Optionally, also require all values to be unique for this field.</p>
</div>
);
};

getDefaultTypeHelpText = () => {
return (
<div>
Expand Down Expand Up @@ -364,10 +398,17 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
mvEnabled,
recommendedVariable,
uniqueConstraint,
nonUniqueConstraint,
PHI,
excludeFromShifting,
phiLevels,
} = this.state;
const singleFieldConstraintType =
uniqueConstraint || field.isPrimaryKey
? DOMAIN_FIELD_UNIQUECONSTRAINT
: nonUniqueConstraint
? DOMAIN_FIELD_NONUNIQUECONSTRAINT
: '';
const currentValueExists = phiLevels?.find(level => level.value === PHI) !== undefined;
const disablePhiSelect =
domainFormDisplayOptions.phiLevelDisabled ||
Expand All @@ -378,8 +419,8 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
<>
<div className="domain-adv-misc-options">Miscellaneous Options</div>
{!field.isCalculatedField() && (
<div className="row">
<div className="col-xs-3">
<div className="row domain-adv-thick-row">
<div className="col-xs-4">
<DomainFieldLabel helpTipBody={this.getPhiHelpText()} label="PHI Level" />
</div>
<div className="col-xs-6">
Expand All @@ -403,7 +444,39 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
))}
</select>
</div>
<div className="col-xs-3" />
<div className="col-xs-2" />
</div>
)}

{allowUniqueConstraintProperties && !field.isCalculatedField() && (
<div className="row">
<div className="col-xs-4">
<DomainFieldLabel
helpTipBody={this.getSingleFieldIndexHelpText()}
label="Uniqueness & Index"
/>
</div>
<div className="col-xs-6">
<select
className="form-control"
disabled={field.isPrimaryKey}
id={createFormInputId(DOMAIN_FIELD_CONSTRAINT, domainIndex, index)}
name={createFormInputName(DOMAIN_FIELD_CONSTRAINT)}
onChange={this.handleSingleFieldIndexChange}
value={singleFieldConstraintType}
>
<option key="None" value="">
No Index
</option>
<option key="Non-Unique" value={DOMAIN_FIELD_NONUNIQUECONSTRAINT}>
Index
</option>
<option key="Unique" value={DOMAIN_FIELD_UNIQUECONSTRAINT}>
Index and require unique values
</option>
</select>
</div>
<div className="col-xs-2" />
</div>
)}
{field.dataType === DATETIME_TYPE && (
Expand Down Expand Up @@ -508,20 +581,6 @@ export class AdvancedSettings extends React.PureComponent<AdvancedSettingsProps,
</LabelHelpTip>
</CheckboxLK>
)}
{allowUniqueConstraintProperties && !field.isCalculatedField() && (
<CheckboxLK
checked={uniqueConstraint || field.isPrimaryKey}
disabled={field.isPrimaryKey}
id={createFormInputId(DOMAIN_FIELD_UNIQUECONSTRAINT, domainIndex, index)}
name={createFormInputName(DOMAIN_FIELD_UNIQUECONSTRAINT)}
onChange={this.handleCheckbox}
>
Require all values to be unique
<LabelHelpTip title="Unique Constraint">
<div>Add a unique constraint via a database-level index for this field.</div>
</LabelHelpTip>
</CheckboxLK>
)}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export const DOMAIN_FIELD_DIMENSION = 'dimension';
export const DOMAIN_FIELD_HIDDEN = 'hidden';
export const DOMAIN_FIELD_MVENABLED = 'mvEnabled';
export const DOMAIN_FIELD_PHI = 'PHI';
export const DOMAIN_FIELD_CONSTRAINT = 'singleFieldConstraint';
export const DOMAIN_FIELD_UNIQUECONSTRAINT = 'uniqueConstraint';
export const DOMAIN_FIELD_NONUNIQUECONSTRAINT = 'nonUniqueConstraint';
export const DOMAIN_FIELD_RECOMMENDEDVARIABLE = 'recommendedVariable';
export const DOMAIN_FIELD_SHOWNINDETAILSVIEW = 'shownInDetailsView';
export const DOMAIN_FIELD_SHOWNININSERTVIEW = 'shownInInsertView';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -827,17 +827,17 @@ describe('DomainDesign', () => {
{ columnNames: ['a', 'b', 'c'], unique: true },
{ columnNames: ['a'], unique: true },
{ columnNames: ['b'], unique: false },
{ columnNames: ['c'], unique: true },
{ columnNames: ['c'], unique: true }, // should be omitted since 'c' is not a field
],
});
const ddJson = DomainDesign.serialize(dd);
expect(ddJson.indices.length).toBe(3);
expect(ddJson.indices[0].columnNames).toStrictEqual(['a', 'b', 'c']);
expect(ddJson.indices[0].unique).toBe(true);
expect(ddJson.indices[1].columnNames).toStrictEqual(['b']);
expect(ddJson.indices[1].unique).toBe(false);
expect(ddJson.indices[2].columnNames).toStrictEqual(['a']);
expect(ddJson.indices[2].unique).toBe(true);
expect(ddJson.indices[1].columnNames).toStrictEqual(['a']);
expect(ddJson.indices[1].unique).toBe(true);
expect(ddJson.indices[2].columnNames).toStrictEqual(['b']);
expect(ddJson.indices[2].unique).toBe(false);
});
});

Expand Down Expand Up @@ -1211,8 +1211,26 @@ describe('DomainField', () => {
List.of('A', 'b', 'd')
);
expect(fields.get(0).uniqueConstraint).toBe(true); // field a
expect(fields.get(0).nonUniqueConstraint).toBe(false); // field a
expect(fields.get(1).uniqueConstraint).toBe(true); // field b
expect(fields.get(1).nonUniqueConstraint).toBe(false); // field b
expect(fields.get(2).uniqueConstraint).toBe(false); // field c
expect(fields.get(2).nonUniqueConstraint).toBe(false); // field c
});

test('nonUniqueConstraintFieldNames in fromJS', () => {
const fields = DomainField.fromJS(
[{ name: 'a' } as IDomainField, { name: 'b' } as IDomainField, { name: 'c' } as IDomainField],
undefined,
List.of('A', 'b', 'd'),
List.of('c', 'd')
);
expect(fields.get(0).uniqueConstraint).toBe(true); // field a
expect(fields.get(0).nonUniqueConstraint).toBe(false); // field a
expect(fields.get(1).uniqueConstraint).toBe(true); // field b
expect(fields.get(1).nonUniqueConstraint).toBe(false); // field b
expect(fields.get(2).uniqueConstraint).toBe(false); // field c
expect(fields.get(2).nonUniqueConstraint).toBe(true); // field c
});

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

test('isMSSQLHashedSingleFieldUniqueConstraint', () => {
let index = DomainIndex.fromJS([{ columnNames: ['a'], unique: true } as IDomainIndex]).get(0);
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
index = DomainIndex.fromJS([{ columnNames: ['a'], unique: false } as IDomainIndex]).get(0);
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a'], unique: true } as IDomainIndex]).get(0);
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a'], unique: false } as IDomainIndex]).get(0);
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(true);
index = DomainIndex.fromJS([{ columnNames: ['_hashed_a', 'b'], unique: false } as IDomainIndex]).get(0);
expect(index.isMSSQLHashedSingleFieldUniqueConstraint()).toBe(false);
test('isSingleFieldNonUniqueConstraint', () => {
let index = DomainIndex.fromJS([{ columnNames: ['a'], unique: false } as IDomainIndex]).get(0);
expect(index.isSingleFieldNonUniqueConstraint()).toBe(true);
index = DomainIndex.fromJS([{ columnNames: ['a'], unique: true } as IDomainIndex]).get(0);
expect(index.isSingleFieldNonUniqueConstraint()).toBe(false);
index = DomainIndex.fromJS([{ columnNames: ['a', 'b'], unique: false } as IDomainIndex]).get(0);
expect(index.isSingleFieldNonUniqueConstraint()).toBe(false);
});
});

Expand Down
Loading