Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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.7.3",
"version": "7.7.3-fb-urlTargetWindow503.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
4 changes: 4 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version TBD
*Released*: TBD
- GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)

### version 7.7.3
*Released*: 31 December 2025
- [GitHub Issue #495](https://github.com/LabKey/internal-issues/issues/495)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { createFormInputId } from './utils';
import {
CALCULATED_CONCEPT_URI,
DOMAIN_FIELD_DESCRIPTION,
DOMAIN_FIELD_FULLY_LOCKED,
DOMAIN_FIELD_IMPORTALIASES,
DOMAIN_FIELD_LABEL,
DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT,
DOMAIN_FIELD_URL,
DOMAIN_FIELD_URL_TARGET,
STORAGE_UNIQUE_ID_CONCEPT_URI,
STRING_RANGE_URI,
} from './constants';
Expand All @@ -29,6 +31,7 @@ const field = DomainField.create({
label: _label,
importAliases: _importAliases,
URL: _URL,
URLTarget: '_blank',
propertyURI: 'test',
});

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

const lockedField = DomainField.create({
name: 'lockedField',
rangeURI: STRING_RANGE_URI,
propertyId: 3,
description: 'locked field desc',
label: 'Locked Field',
lockType: DOMAIN_FIELD_FULLY_LOCKED,
});

const DEFAULT_PROPS = {
index: 1,
domainIndex: 1,
Expand All @@ -73,21 +85,31 @@ describe('NameAndLinkingOptions', () => {
let formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_DESCRIPTION, 1, 1));
expect(formField.length).toEqual(1);
expect(formField[0].textContent).toEqual(_description);
expect(formField[0].hasAttribute('disabled')).toEqual(false);

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

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

// URL
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1));
expect(formField.length).toEqual(1);
expect(formField[0].getAttribute('value')).toEqual(_URL);
expect(formField[0].hasAttribute('disabled')).toEqual(false);

// URL Target
formField = document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1));
expect(formField.length).toEqual(1);
expect(formField[0].hasAttribute('checked')).toEqual(true);
expect(formField[0].hasAttribute('disabled')).toEqual(false);

expect(container).toMatchSnapshot();
});
Expand Down Expand Up @@ -119,6 +141,9 @@ describe('NameAndLinkingOptions', () => {
test('calculated field', () => {
render(<NameAndLinkingOptions {...DEFAULT_PROPS} field={calculatedField} />);
expect(document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1))).toHaveLength(0);

expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1)).getAttribute('value')).toEqual('');
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1)).hasAttribute('checked')).toEqual(false);
});

test('hideImportAliases', () => {
Expand All @@ -132,4 +157,13 @@ describe('NameAndLinkingOptions', () => {
);
expect(document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1))).toHaveLength(0);
});

test('locked field', () => {
render(<NameAndLinkingOptions {...DEFAULT_PROPS} field={lockedField} />);
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_LABEL, 1, 1)).hasAttribute('disabled')).toEqual(true);
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_DESCRIPTION, 1, 1)).hasAttribute('disabled')).toEqual(true);
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1)).hasAttribute('disabled')).toEqual(true);
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL, 1, 1)).hasAttribute('disabled')).toEqual(true);
expect(document.querySelector('#' + createFormInputId(DOMAIN_FIELD_URL_TARGET, 1, 1)).hasAttribute('disabled')).toEqual(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DOMAIN_FIELD_LABEL,
DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT,
DOMAIN_FIELD_URL,
DOMAIN_FIELD_URL_TARGET,
} from './constants';
import { DomainField, IDomainFormDisplayOptions } from './models';
import { SectionHeading } from './SectionHeading';
Expand All @@ -39,6 +40,12 @@ export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
this.props?.onChange(id, value);
};

handleURLTargetChange = (evt: any): void => {
const id = evt.target.id;
const isChecked = evt.target.checked;
this.onChange(id, isChecked ? '_blank' : null);
};

getImportAliasHelpText = (): ReactNode => {
return (
<>
Expand Down Expand Up @@ -120,6 +127,16 @@ export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
)}
</div>
<div className="col-xs-4">
{!appPropertiesOnly &&
hasModule(ONTOLOGY_MODULE_NAME) &&
!field.isUniqueIdField() &&
!field.isCalculatedField() && (
<OntologyConceptAnnotation
field={field}
id={createFormInputId(DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT, domainIndex, index)}
onChange={this.onChange}
/>
)}
<div className="domain-field-label">
<DomainFieldLabel label="URL" helpTipBody={this.getURLHelpText()} />
</div>
Expand All @@ -132,16 +149,19 @@ export class NameAndLinkingOptions extends PureComponent<NameAndLinkingProps> {
onChange={this.handleChange}
disabled={isFieldFullyLocked(field.lockType)}
/>
{!appPropertiesOnly &&
hasModule(ONTOLOGY_MODULE_NAME) &&
!field.isUniqueIdField() &&
!field.isCalculatedField() && (
<OntologyConceptAnnotation
id={createFormInputId(DOMAIN_FIELD_ONTOLOGY_PRINCIPAL_CONCEPT, domainIndex, index)}
field={field}
onChange={this.onChange}
/>
)}
{/*GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)*/}
<div className="domain-text-options-col">
<input
type="checkbox"
id={createFormInputId(DOMAIN_FIELD_URL_TARGET, domainIndex, index)}
name={createFormInputName(DOMAIN_FIELD_URL_TARGET)}
className="form-control domain-text-option-urltarget"
checked={field.URLTarget === '_blank'}
onChange={this.handleURLTargetChange}
disabled={isFieldFullyLocked(field.lockType)}
/>
<span>Open links in a new tab</span>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ exports[`NameAndLinkingOptions Name and Linking options 1`] = `
type="text"
value="This is a URL"
/>
<div
class="domain-text-options-col"
>
<input
checked=""
class="form-control domain-text-option-urltarget"
id="domainpropertiesrow-URLTarget-1-1"
name="domainpropertiesrow-URLTarget"
type="checkbox"
/>
<span>
Open links in a new tab
</span>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const DOMAIN_FIELD_DESCRIPTION = 'description';
export const DOMAIN_FIELD_LABEL = 'label';
export const DOMAIN_FIELD_IMPORTALIASES = 'importAliases';
export const DOMAIN_FIELD_URL = 'URL';
export const DOMAIN_FIELD_URL_TARGET = 'URLTarget';
export const DOMAIN_FIELD_LOOKUP_CONTAINER = 'lookupContainer';
export const DOMAIN_FIELD_LOOKUP_QUERY = 'lookupQueryValue';
export const DOMAIN_FIELD_LOOKUP_SCHEMA = 'lookupSchema';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const gridDataAppPropsOnlyConst = [
scale: 4000,
name: 'a',
URL: '',
URLTarget: '',
conceptURI: '',
rangeURI: 'http://www.w3.org/2001/XMLSchema#int',
PHI: '',
Expand Down Expand Up @@ -188,6 +189,7 @@ const nameCol = new GridColumn({
const gridColumnsConst = [
selectionCol,
nameCol,
{ index: 'URLTarget', caption: 'URL Target', sortable: true },
{ index: 'URL', caption: 'URL', sortable: true },
{ index: 'PHI', caption: 'PHI', sortable: true },
{ index: 'rangeURI', caption: 'Range URI', sortable: true },
Expand Down Expand Up @@ -1490,6 +1492,13 @@ describe('resolveBaseProperties', () => {
DOMAIN_FIELD_PARTIALLY_LOCKED
);
});

test('URLTarget', () => {
expect(DomainField.resolveBaseProperties({}).URLTarget).toBeUndefined();
expect(DomainField.resolveBaseProperties({ URLTarget: '_self' }).URLTarget).toBeUndefined();
expect(DomainField.resolveBaseProperties({ urltarget: '_self' }).URLTarget).toBe('_self');
expect(DomainField.resolveBaseProperties({ urltarget: '_blank' }).URLTarget).toBe('_blank');
});
});

describe('resolveLookupQueryValue', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,14 @@ export class DomainDesign
if (!showFilterCriteria) delete columns.filterCriteria;

const unsortedColumns = List(
Object.keys(columns).map(key => ({ index: key, caption: camelCaseToTitleCase(key), sortable: true }))
Object.keys(columns).map(key => {
let caption = camelCaseToTitleCase(key);
// special case for url and urltarget
if (key.toLowerCase() === 'url') caption = 'URL';
if (key.toLowerCase() === 'urltarget') caption = 'URL Target';

return { index: key, caption, sortable: true };
})
);
return specialCols.concat(unsortedColumns.sort(reorderSummaryColumns)).toList();
}
Expand Down Expand Up @@ -914,6 +921,7 @@ export interface IDomainField {
uniqueConstraint?: boolean;
updatedField: boolean;
URL?: string;
URLTarget?: string;
visible: boolean;
}

Expand Down Expand Up @@ -985,6 +993,7 @@ export class DomainField
required: false,
scale: MAX_TEXT_LENGTH,
URL: undefined,
URLTarget: undefined,
shownInDetailsView: true,
shownInInsertView: true,
shownInUpdateView: true,
Expand Down Expand Up @@ -1047,6 +1056,7 @@ export class DomainField
declare scale?: number;
declare scannable?: boolean;
declare URL?: string;
declare URLTarget?: string;
declare shownInDetailsView?: boolean;
declare shownInInsertView?: boolean;
declare shownInUpdateView?: boolean;
Expand Down Expand Up @@ -1190,6 +1200,11 @@ export class DomainField
field.rangeURI = raw.rangeURI;
}

// handle URLTarget prop casing mismatch
if (raw['urltarget']) {
field.URLTarget = raw['urltarget'];
}

return field;
}

Expand All @@ -1215,6 +1230,10 @@ export class DomainField
json.url = json.URL;
delete json.URL;
}
if (json.URLTarget !== undefined) {
json.urltarget = json.URLTarget; // note casing because of GWTPropertyDescriptor behavior on server side
delete json.URLTarget;
}
if (json.PHI !== undefined) {
json.phi = json.PHI;
delete json.PHI;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export function reorderSummaryColumns(a: DomainPropertiesGridColumn, b: DomainPr
'label',
'importAliases',
'url',
'urltarget',
'conditionalFormats',
'propertyValidators',
'valueExpression',
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/theme/domainproperties.scss
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@
margin: 6px 20px 0 0 !important;
}

.domain-text-option-scannable {
.domain-text-option-scannable, .domain-text-option-urltarget {
display: inline;
}

Expand Down