diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 08e20f7140..d4b8f1e16d 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.7.3", + "version": "7.7.3-fb-urlTargetWindow503.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.7.3", + "version": "7.7.3-fb-urlTargetWindow503.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index ae0ca2eba2..69cb3c3a95 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index f50cafc6d6..3c0c62525b 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -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) diff --git a/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.test.tsx b/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.test.tsx index beaafddf61..4511cec5cd 100644 --- a/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.test.tsx +++ b/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.test.tsx @@ -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'; @@ -29,6 +31,7 @@ const field = DomainField.create({ label: _label, importAliases: _importAliases, URL: _URL, + URLTarget: '_blank', propertyURI: 'test', }); @@ -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, @@ -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(); }); @@ -119,6 +141,9 @@ describe('NameAndLinkingOptions', () => { test('calculated field', () => { render(); 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', () => { @@ -132,4 +157,13 @@ describe('NameAndLinkingOptions', () => { ); expect(document.querySelectorAll('#' + createFormInputId(DOMAIN_FIELD_IMPORTALIASES, 1, 1))).toHaveLength(0); }); + + test('locked field', () => { + render(); + 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); + }); }); diff --git a/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.tsx b/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.tsx index 292762ecfb..5d5c64eb16 100644 --- a/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.tsx +++ b/packages/components/src/internal/components/domainproperties/NameAndLinkingOptions.tsx @@ -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'; @@ -39,6 +40,12 @@ export class NameAndLinkingOptions extends PureComponent { 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 ( <> @@ -120,6 +127,16 @@ export class NameAndLinkingOptions extends PureComponent { )}
+ {!appPropertiesOnly && + hasModule(ONTOLOGY_MODULE_NAME) && + !field.isUniqueIdField() && + !field.isCalculatedField() && ( + + )}
@@ -132,16 +149,19 @@ export class NameAndLinkingOptions extends PureComponent { onChange={this.handleChange} disabled={isFieldFullyLocked(field.lockType)} /> - {!appPropertiesOnly && - hasModule(ONTOLOGY_MODULE_NAME) && - !field.isUniqueIdField() && - !field.isCalculatedField() && ( - - )} + {/*GitHub Issue 503: Field editor URL option to set target window (i.e. _blank)*/} +
+ + Open links in a new tab +
diff --git a/packages/components/src/internal/components/domainproperties/__snapshots__/NameAndLinkingOptions.test.tsx.snap b/packages/components/src/internal/components/domainproperties/__snapshots__/NameAndLinkingOptions.test.tsx.snap index 4ac4b5d759..6d6ea29fa3 100644 --- a/packages/components/src/internal/components/domainproperties/__snapshots__/NameAndLinkingOptions.test.tsx.snap +++ b/packages/components/src/internal/components/domainproperties/__snapshots__/NameAndLinkingOptions.test.tsx.snap @@ -110,6 +110,20 @@ exports[`NameAndLinkingOptions Name and Linking options 1`] = ` type="text" value="This is a URL" /> +
+ + + Open links in a new tab + +
diff --git a/packages/components/src/internal/components/domainproperties/constants.ts b/packages/components/src/internal/components/domainproperties/constants.ts index 7525f0dce1..80aba4c72c 100644 --- a/packages/components/src/internal/components/domainproperties/constants.ts +++ b/packages/components/src/internal/components/domainproperties/constants.ts @@ -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'; diff --git a/packages/components/src/internal/components/domainproperties/models.test.ts b/packages/components/src/internal/components/domainproperties/models.test.ts index 1e4adbe6e1..1b5e02acdc 100644 --- a/packages/components/src/internal/components/domainproperties/models.test.ts +++ b/packages/components/src/internal/components/domainproperties/models.test.ts @@ -122,6 +122,7 @@ const gridDataAppPropsOnlyConst = [ scale: 4000, name: 'a', URL: '', + URLTarget: '', conceptURI: '', rangeURI: 'http://www.w3.org/2001/XMLSchema#int', PHI: '', @@ -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 }, @@ -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', () => { diff --git a/packages/components/src/internal/components/domainproperties/models.tsx b/packages/components/src/internal/components/domainproperties/models.tsx index 50326689f3..c199713296 100644 --- a/packages/components/src/internal/components/domainproperties/models.tsx +++ b/packages/components/src/internal/components/domainproperties/models.tsx @@ -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(); } @@ -914,6 +921,7 @@ export interface IDomainField { uniqueConstraint?: boolean; updatedField: boolean; URL?: string; + URLTarget?: string; visible: boolean; } @@ -985,6 +993,7 @@ export class DomainField required: false, scale: MAX_TEXT_LENGTH, URL: undefined, + URLTarget: undefined, shownInDetailsView: true, shownInInsertView: true, shownInUpdateView: true, @@ -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; @@ -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; } @@ -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; diff --git a/packages/components/src/internal/components/domainproperties/propertiesUtil.ts b/packages/components/src/internal/components/domainproperties/propertiesUtil.ts index 1858fb1dda..9efbce3b2a 100644 --- a/packages/components/src/internal/components/domainproperties/propertiesUtil.ts +++ b/packages/components/src/internal/components/domainproperties/propertiesUtil.ts @@ -147,6 +147,7 @@ export function reorderSummaryColumns(a: DomainPropertiesGridColumn, b: DomainPr 'label', 'importAliases', 'url', + 'urltarget', 'conditionalFormats', 'propertyValidators', 'valueExpression', diff --git a/packages/components/src/theme/domainproperties.scss b/packages/components/src/theme/domainproperties.scss index 17bf43d2c0..d3633a010e 100644 --- a/packages/components/src/theme/domainproperties.scss +++ b/packages/components/src/theme/domainproperties.scss @@ -359,7 +359,7 @@ margin: 6px 20px 0 0 !important; } -.domain-text-option-scannable { +.domain-text-option-scannable, .domain-text-option-urltarget { display: inline; }