Skip to content

Commit 1237588

Browse files
authored
GitHub #503: Add option to open URL links in a new tab in the domain designer UI (LabKey Issue: 54178) (#1919)
### version 7.9.0 *Released*: 6 January 2026 - GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)
1 parent 558a31d commit 1237588

14 files changed

Lines changed: 188 additions & 35 deletions

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

4-
### version 7.8.1
4+
### version 7.9.0
55
*Released*: 6 January 2026
6+
- GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)
7+
8+
### version 7.8.1
9+
*Released*: 5 January 2026
610
- GitHub 562: Combine warning messages for file preview unknown/system fields with the warning for duplicate columns
711
- FilePreviewGrid to combine warningMsg with previewData.warningMsg in a single Alert
812

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export class DomainRowExpandedOptions extends React.Component<Props> {
265265
field,
266266
index,
267267
onChange,
268+
onMultiChange,
268269
showingModal,
269270
appPropertiesOnly,
270271
domainIndex,
@@ -311,6 +312,7 @@ export class DomainRowExpandedOptions extends React.Component<Props> {
311312
domainIndex={domainIndex}
312313
field={field}
313314
onChange={onChange}
315+
onMultiChange={onMultiChange}
314316
appPropertiesOnly={appPropertiesOnly}
315317
domainFormDisplayOptions={domainFormDisplayOptions}
316318
/>

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { createFormInputId } from './utils';
55
import {
66
CALCULATED_CONCEPT_URI,
77
DOMAIN_FIELD_DESCRIPTION,
8+
DOMAIN_FIELD_FULLY_LOCKED,
89
DOMAIN_FIELD_IMPORTALIASES,
910
DOMAIN_FIELD_LABEL,
1011
DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT,
1112
DOMAIN_FIELD_URL,
13+
DOMAIN_FIELD_URL_TARGET,
1214
STORAGE_UNIQUE_ID_CONCEPT_URI,
1315
STRING_RANGE_URI,
1416
} from './constants';
@@ -29,6 +31,7 @@ const field = DomainField.create({
2931
label: _label,
3032
importAliases: _importAliases,
3133
URL: _URL,
34+
isTargetBlank: true,
3235
propertyURI: 'test',
3336
});
3437

@@ -50,6 +53,15 @@ const calculatedField = DomainField.create({
5053
conceptURI: CALCULATED_CONCEPT_URI,
5154
});
5255

56+
const lockedField = DomainField.create({
57+
name: 'lockedField',
58+
rangeURI: STRING_RANGE_URI,
59+
propertyId: 3,
60+
description: 'locked field desc',
61+
label: 'Locked Field',
62+
lockType: DOMAIN_FIELD_FULLY_LOCKED,
63+
});
64+
5365
const DEFAULT_PROPS = {
5466
index: 1,
5567
domainIndex: 1,
@@ -73,21 +85,31 @@ describe('NameAndLinkingOptions', () => {
7385
let formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_DESCRIPTION, 1, 1));
7486
expect(formField.length).toEqual(1);
7587
expect(formField[0].textContent).toEqual(_description);
88+
expect(formField[0].hasAttribute('disabled')).toEqual(false);
7689

7790
// Label
7891
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_LABEL, 1, 1));
7992
expect(formField.length).toEqual(1);
8093
expect(formField[0].getAttribute('value')).toEqual(_label);
94+
expect(formField[0].hasAttribute('disabled')).toEqual(false);
8195

8296
// Aliases
8397
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1));
8498
expect(formField.length).toEqual(1);
8599
expect(formField[0].getAttribute('value')).toEqual(_importAliases);
100+
expect(formField[0].hasAttribute('disabled')).toEqual(false);
86101

87102
// URL
88103
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1));
89104
expect(formField.length).toEqual(1);
90105
expect(formField[0].getAttribute('value')).toEqual(_URL);
106+
expect(formField[0].hasAttribute('disabled')).toEqual(false);
107+
108+
// URL Target
109+
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1));
110+
expect(formField.length).toEqual(1);
111+
expect(formField[0].hasAttribute('checked')).toEqual(true);
112+
expect(formField[0].hasAttribute('disabled')).toEqual(false);
91113

92114
expect(container).toMatchSnapshot();
93115
});
@@ -119,6 +141,13 @@ describe('NameAndLinkingOptions', () => {
119141
test('calculated field', () => {
120142
render(<NameAndLinkingOptions {...DEFAULT_PROPS} field={calculatedField} />);
121143
expect(document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1))).toHaveLength(0);
144+
145+
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1)).getAttribute('value')).toEqual(
146+
''
147+
);
148+
expect(
149+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1)).hasAttribute('checked')
150+
).toEqual(false);
122151
});
123152

124153
test('hideImportAliases', () => {
@@ -132,4 +161,23 @@ describe('NameAndLinkingOptions', () => {
132161
);
133162
expect(document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1))).toHaveLength(0);
134163
});
164+
165+
test('locked field', () => {
166+
render(<NameAndLinkingOptions {...DEFAULT_PROPS} field={lockedField} />);
167+
expect(
168+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_LABEL, 1, 1)).hasAttribute('disabled')
169+
).toEqual(true);
170+
expect(
171+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_DESCRIPTION, 1, 1)).hasAttribute('disabled')
172+
).toEqual(true);
173+
expect(
174+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1)).hasAttribute('disabled')
175+
).toEqual(true);
176+
expect(
177+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1)).hasAttribute('disabled')
178+
).toEqual(true);
179+
expect(
180+
document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1)).hasAttribute('disabled')
181+
).toEqual(true);
182+
});
135183
});

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

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { PureComponent, ReactNode } from 'react';
2+
import { List } from 'immutable';
23

34
import { HelpLink, URL_ENCODING_TOPIC } from '../../util/helpLinks';
45

@@ -9,15 +10,16 @@ import { ONTOLOGY_MODULE_NAME } from '../ontology/actions';
910
import { hasModule } from '../../app/utils';
1011

1112
import { isFieldFullyLocked } from './propertiesUtil';
12-
import { createFormInputId, createFormInputName } from './utils';
13+
import { createFormInputId, createFormInputName, isEmptyString } from './utils';
1314
import {
1415
DOMAIN_FIELD_DESCRIPTION,
1516
DOMAIN_FIELD_IMPORTALIASES,
1617
DOMAIN_FIELD_LABEL,
1718
DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT,
1819
DOMAIN_FIELD_URL,
20+
DOMAIN_FIELD_URL_TARGET,
1921
} from './constants';
20-
import { DomainField, IDomainFormDisplayOptions } from './models';
22+
import { DomainField, IDomainFormDisplayOptions, IFieldChange } from './models';
2123
import { SectionHeading } from './SectionHeading';
2224
import { DomainFieldLabel } from './DomainFieldLabel';
2325

@@ -28,6 +30,7 @@ interface NameAndLinkingProps {
2830
field: DomainField;
2931
index: number;
3032
onChange: (string, any) => void;
33+
onMultiChange: (changes: List<IFieldChange>) => void;
3134
}
3235

3336
export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
@@ -36,7 +39,30 @@ export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
3639
};
3740

3841
onChange = (id: string, value: any): void => {
39-
this.props?.onChange(id, value);
42+
this.props.onChange(id, value);
43+
};
44+
45+
handleURLChange = (evt: any): void => {
46+
const { index, domainIndex } = this.props;
47+
const val = evt.target.value;
48+
const isEmpty = isEmptyString(val);
49+
50+
// make sure to uncheck the "open in new tab" option if URL is cleared out
51+
if (isEmpty) {
52+
let changes = List<IFieldChange>();
53+
changes = changes.push({ id: evt.target.id, value: null });
54+
changes = changes.push({
55+
id: createFormInputId(DOMAIN_FIELD_URL_TARGET, domainIndex, index),
56+
value: false,
57+
});
58+
this.props.onMultiChange(changes);
59+
} else {
60+
this.onChange(evt.target.id, isEmpty ? null : val);
61+
}
62+
};
63+
64+
handleURLTargetChange = (evt: any): void => {
65+
this.onChange(evt.target.id, evt.target.checked);
4066
};
4167

4268
getImportAliasHelpText = (): ReactNode => {
@@ -70,78 +96,91 @@ export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
7096
<div>
7197
<div className="row">
7298
<div className="col-xs-12">
73-
<SectionHeading title="Name and Linking Options" cls="domain-field-section-hdr" />
99+
<SectionHeading cls="domain-field-section-hdr" title="Name and Linking Options" />
74100
</div>
75101
</div>
76102
<div className="row">
77103
<div className="col-xs-5">
78104
<div className="domain-field-label">Description</div>
79105
<textarea
80106
className="form-control"
81-
rows={4}
82-
value={field.description || ''}
107+
disabled={isFieldFullyLocked(field.lockType)}
83108
id={createFormInputId(DOMAIN_FIELD_DESCRIPTION, domainIndex, index)}
84109
name={createFormInputName(DOMAIN_FIELD_DESCRIPTION)}
85110
onChange={this.handleChange}
86-
disabled={isFieldFullyLocked(field.lockType)}
111+
rows={4}
112+
value={field.description || ''}
87113
/>
88114
</div>
89115
<div className="col-xs-3">
90116
<div className="domain-field-label">Label</div>
91117
<input
92118
className="form-control"
93-
type="text"
94-
value={field.label || ''}
119+
disabled={isFieldFullyLocked(field.lockType)}
95120
id={createFormInputId(DOMAIN_FIELD_LABEL, domainIndex, index)}
96121
name={createFormInputName(DOMAIN_FIELD_LABEL)}
97122
onChange={this.handleChange}
98-
disabled={isFieldFullyLocked(field.lockType)}
123+
type="text"
124+
value={field.label || ''}
99125
/>
100126
{!field.isUniqueIdField() &&
101127
!field.isCalculatedField() &&
102128
!domainFormDisplayOptions?.hideImportAliases && (
103129
<>
104130
<div className="domain-field-label">
105131
<DomainFieldLabel
106-
label="Import Aliases"
107132
helpTipBody={this.getImportAliasHelpText()}
133+
label="Import Aliases"
108134
/>
109135
</div>
110136
<input
111137
className="form-control"
112-
type="text"
113-
value={field.importAliases || ''}
138+
disabled={isFieldFullyLocked(field.lockType)}
114139
id={createFormInputId(DOMAIN_FIELD_IMPORTALIASES, domainIndex, index)}
115140
name={createFormInputName(DOMAIN_FIELD_IMPORTALIASES)}
116141
onChange={this.handleChange}
117-
disabled={isFieldFullyLocked(field.lockType)}
142+
type="text"
143+
value={field.importAliases || ''}
118144
/>
119145
</>
120146
)}
121147
</div>
122148
<div className="col-xs-4">
123-
<div className="domain-field-label">
124-
<DomainFieldLabel label="URL" helpTipBody={this.getURLHelpText()} />
125-
</div>
126-
<input
127-
className="form-control"
128-
type="text"
129-
value={field.URL || ''}
130-
id={createFormInputId(DOMAIN_FIELD_URL, domainIndex, index)}
131-
name={createFormInputName(DOMAIN_FIELD_URL)}
132-
onChange={this.handleChange}
133-
disabled={isFieldFullyLocked(field.lockType)}
134-
/>
135149
{!appPropertiesOnly &&
136150
hasModule(ONTOLOGY_MODULE_NAME) &&
137151
!field.isUniqueIdField() &&
138152
!field.isCalculatedField() && (
139153
<OntologyConceptAnnotation
140-
id={createFormInputId(DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT, domainIndex, index)}
141154
field={field}
155+
id={createFormInputId(DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT, domainIndex, index)}
142156
onChange={this.onChange}
143157
/>
144158
)}
159+
<div className="domain-field-label">
160+
<DomainFieldLabel helpTipBody={this.getURLHelpText()} label="URL" />
161+
</div>
162+
<input
163+
className="form-control"
164+
disabled={isFieldFullyLocked(field.lockType)}
165+
id={createFormInputId(DOMAIN_FIELD_URL, domainIndex, index)}
166+
name={createFormInputName(DOMAIN_FIELD_URL)}
167+
onChange={this.handleURLChange}
168+
type="text"
169+
value={field.URL || ''}
170+
/>
171+
{/*GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)*/}
172+
<div className="domain-text-options-col">
173+
<input
174+
checked={field.isTargetBlank}
175+
className="form-control domain-text-option-istargetblank"
176+
disabled={isFieldFullyLocked(field.lockType) || isEmptyString(field.URL)}
177+
id={createFormInputId(DOMAIN_FIELD_URL_TARGET, domainIndex, index)}
178+
name={createFormInputName(DOMAIN_FIELD_URL_TARGET)}
179+
onChange={this.handleURLTargetChange}
180+
type="checkbox"
181+
/>
182+
<span>Open links in a new tab</span>
183+
</div>
145184
</div>
146185
</div>
147186
</div>

packages/components/src/internal/components/domainproperties/__snapshots__/NameAndLinkingOptions.test.tsx.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ exports[`NameAndLinkingOptions Name and Linking options 1`] = `
110110
type="text"
111111
value="This is a URL"
112112
/>
113+
<div
114+
class="domain-text-options-col"
115+
>
116+
<input
117+
checked=""
118+
class="form-control domain-text-option-istargetblank"
119+
id="domainpropertiesrow-isTargetBlank-1-1"
120+
name="domainpropertiesrow-isTargetBlank"
121+
type="checkbox"
122+
/>
123+
<span>
124+
Open links in a new tab
125+
</span>
126+
</div>
113127
</div>
114128
</div>
115129
</div>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const DOMAIN_FIELD_DESCRIPTION = 'description';
2727
export const DOMAIN_FIELD_LABEL = 'label';
2828
export const DOMAIN_FIELD_IMPORTALIASES = 'importAliases';
2929
export const DOMAIN_FIELD_URL = 'URL';
30+
export const DOMAIN_FIELD_URL_TARGET = 'isTargetBlank';
3031
export const DOMAIN_FIELD_LOOKUP_CONTAINER = 'lookupContainer';
3132
export const DOMAIN_FIELD_LOOKUP_QUERY = 'lookupQueryValue';
3233
export const DOMAIN_FIELD_LOOKUP_SCHEMA = 'lookupSchema';

0 commit comments

Comments
 (0)