Skip to content

Commit 7a7bd61

Browse files
authored
Merge pull request #1671 from jetstreamapp/feat/show-name-as-record-popover
Add NameLinkRenderer for clickable popover and update column types
2 parents 89cffdc + 43c8c83 commit 7a7bd61

4 files changed

Lines changed: 78 additions & 16 deletions

File tree

libs/ui/src/lib/data-table/DataTableRenderers.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import { css } from '@emotion/react';
33
import { IconName } from '@jetstream/icon-factory';
44
import { isValidSalesforceRecordId, useDebounce } from '@jetstream/shared/ui-utils';
5-
import { multiWordStringFilter } from '@jetstream/shared/utils';
5+
import { getIdFromRecordUrl, multiWordStringFilter } from '@jetstream/shared/utils';
66
import { CloneEditView, ListItem, SalesforceOrgUi } from '@jetstream/types';
77
import { useVirtualizer } from '@tanstack/react-virtual';
88
import classNames from 'classnames';
99
import { formatISO } from 'date-fns/formatISO';
1010
import { parseISO } from 'date-fns/parseISO';
11+
import lodashGet from 'lodash/get';
1112
import isBoolean from 'lodash/isBoolean';
1213
import isFunction from 'lodash/isFunction';
1314
import isString from 'lodash/isString';
@@ -599,6 +600,43 @@ export const IdLinkRenderer = ({ column, row }: RenderCellProps<RowWithKey, unkn
599600
};
600601
dataTableRenderFnMap.set(IdLinkRenderer, 'IdLinkRenderer');
601602

603+
/**
604+
* Render a Name field (primary or via relationship like Account.Name) as a clickable
605+
* record-lookup popover, showing the Name text as the cell content instead of the id.
606+
* Falls back to plain text when no related record id can be resolved.
607+
*/
608+
export const NameLinkRenderer = ({ column, row }: RenderCellProps<RowWithKey, unknown>): ReactNode => {
609+
const { onRecordAction } = useContext(DataTableGenericContext) as {
610+
onRecordAction?: (action: CloneEditView, recordId: string, sobjectName: string) => void;
611+
};
612+
const nameValue = row[column.key];
613+
// For "Account.Owner.Name" the parent path is "Account.Owner"; for bare "Name" it is the row's record itself.
614+
const parentPath = column.key.includes('.') ? column.key.split('.').slice(0, -1).join('.') : '';
615+
const relatedRecord = parentPath ? lodashGet(row._record, parentPath) : row._record;
616+
const relatedRecordUrl = relatedRecord?.attributes?.url;
617+
const recordId: string | undefined = relatedRecord?.Id || (relatedRecordUrl ? getIdFromRecordUrl(relatedRecordUrl) : undefined);
618+
619+
if (nameValue == null || !recordId) {
620+
return <div className="slds-truncate">{nameValue}</div>;
621+
}
622+
623+
const { skipFrontDoorAuth, url } = getSfdcRetUrl(relatedRecord, recordId, _skipFrontdoorLogin);
624+
625+
return (
626+
<RecordLookupPopover
627+
org={_org}
628+
serverUrl={_serverUrl}
629+
recordId={recordId}
630+
skipFrontDoorAuth={skipFrontDoorAuth}
631+
returnUrl={url}
632+
isTooling={false}
633+
onRecordAction={onRecordAction}
634+
displayValue={<span className="slds-truncate">{nameValue}</span>}
635+
/>
636+
);
637+
};
638+
dataTableRenderFnMap.set(NameLinkRenderer, 'NameLinkRenderer');
639+
602640
export function TextOrIdLinkRenderer(RenderCellProps: RenderCellProps<RowWithKey>): ReactNode {
603641
const { column, row } = RenderCellProps;
604642

libs/ui/src/lib/data-table/data-table-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type ColumnType =
2121
| 'boolean'
2222
| 'address'
2323
| 'salesforceId'
24+
| 'salesforceName'
2425
| 'textOrSalesforceId';
2526
export type FilterType = 'TEXT' | 'NUMBER' | 'DATE' | 'TIME' | 'SET' | 'BOOLEAN_SET';
2627
export const FILTER_SET_TYPES = new Set<FilterType>(['SET', 'BOOLEAN_SET']);

libs/ui/src/lib/data-table/data-table-utils.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
GenericRenderer,
3535
HeaderFilter,
3636
IdLinkRenderer,
37+
NameLinkRenderer,
3738
TextOrIdLinkRenderer,
3839
} from './DataTableRenderers';
3940
import { SubqueryRenderer } from './DataTableSubqueryRenderer';
@@ -280,22 +281,38 @@ function getQueryResultColumn({
280281
if (subqueryRelationshipName) {
281282
fieldLowercase = `${subqueryRelationshipName.toLowerCase()}.${fieldLowercase}`;
282283
}
283-
if (queryColumnsByPath[fieldLowercase]) {
284-
const col = queryColumnsByPath[fieldLowercase];
285-
column.name = col.columnFullPath;
286-
column.key = col.columnFullPath;
287-
updateColumnFromType(column, getColumnTypeFromQueryResultsColumn(col));
284+
const queryResultColumn = queryColumnsByPath[fieldLowercase];
285+
let resolvedType: ColumnType = 'text';
286+
if (queryResultColumn) {
287+
column.name = queryResultColumn.columnFullPath;
288+
column.key = queryResultColumn.columnFullPath;
289+
resolvedType = getColumnTypeFromQueryResultsColumn(queryResultColumn);
290+
updateColumnFromType(column, resolvedType);
288291
// exclude related records from edit mode
289-
if (allowEdit && !col.columnFullPath?.includes('.')) {
290-
updateColumnWithEditMode(column, col, fieldMetadata);
291-
}
292-
} else {
293-
if (field.endsWith('Id')) {
294-
updateColumnFromType(column, 'salesforceId');
295-
} else if (isSubquery) {
296-
updateColumnFromType(column, 'subquery');
292+
if (allowEdit && !queryResultColumn.columnFullPath?.includes('.')) {
293+
updateColumnWithEditMode(column, queryResultColumn, fieldMetadata);
297294
}
295+
} else if (field.endsWith('Id')) {
296+
resolvedType = 'salesforceId';
297+
updateColumnFromType(column, 'salesforceId');
298+
} else if (isSubquery) {
299+
resolvedType = 'subquery';
300+
updateColumnFromType(column, 'subquery');
301+
}
302+
303+
// Upgrade plain-text Name / relationship Name columns (e.g. Name, Account.Name, Account.Owner.Name)
304+
// to a clickable record-lookup popover, mirroring the IdLinkRenderer behavior.
305+
// Skipped for subquery child columns, aggregate (GROUP BY) columns, and anything that already
306+
// resolved to a non-text type (salesforceId, boolean, date, etc).
307+
// Use the canonical resolved column path when available so Name-field detection does not depend
308+
// on the original query casing from getFlattenedFields(results.parsedQuery).
309+
const canonicalColumnPath = queryResultColumn?.columnFullPath ?? column.key;
310+
const isNameField =
311+
!!fieldMetadata?.[field.toLowerCase()]?.nameField || canonicalColumnPath === 'Name' || canonicalColumnPath.endsWith('.Name');
312+
if (!subqueryRelationshipName && !queryResultColumn?.aggregate && resolvedType === 'text' && isNameField) {
313+
updateColumnFromType(column, 'salesforceName');
298314
}
315+
299316
return column;
300317
}
301318

@@ -416,6 +433,9 @@ export function updateColumnFromType(column: Mutable<ColumnWithFilter<any>>, fie
416433
column.renderCell = IdLinkRenderer;
417434
column.width = 175;
418435
break;
436+
case 'salesforceName':
437+
column.renderCell = NameLinkRenderer;
438+
break;
419439
case 'textOrSalesforceId':
420440
column.renderCell = TextOrIdLinkRenderer;
421441
column.width = 175;

libs/ui/src/lib/widgets/RecordLookupPopover.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { css } from '@emotion/react';
22
import { describeGlobal, describeSObject, queryWithCache } from '@jetstream/shared/data';
33
import { appActionObservable, useNonInitialEffect } from '@jetstream/shared/ui-utils';
44
import { CloneEditView, RecordWithAuditFields, SalesforceOrgUi } from '@jetstream/types';
5-
import { FunctionComponent, useEffect, useRef, useState } from 'react';
5+
import { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react';
66
import { dataTableDateFormatter } from '../data-table/data-table-formatters';
77
import ReadOnlyFormElement from '../form/readonly-form-element/ReadOnlyFormElement';
88
import Grid from '../grid/Grid';
@@ -21,6 +21,8 @@ export interface RecordLookupPopoverProps {
2121
skipFrontDoorAuth?: boolean;
2222
returnUrl?: string;
2323
isTooling?: boolean;
24+
/** Custom content to render as the popover trigger. Defaults to the recordId. */
25+
displayValue?: ReactNode;
2426
onRecordAction?: (action: CloneEditView, recordId: string, sobjectName: string) => void;
2527
}
2628

@@ -31,6 +33,7 @@ export const RecordLookupPopover: FunctionComponent<RecordLookupPopoverProps> =
3133
skipFrontDoorAuth,
3234
returnUrl,
3335
isTooling,
36+
displayValue,
3437
onRecordAction,
3538
}) => {
3639
const isMounted = useRef(true);
@@ -250,7 +253,7 @@ export const RecordLookupPopover: FunctionComponent<RecordLookupPopoverProps> =
250253
}
251254
}}
252255
>
253-
{recordId}
256+
{displayValue ?? recordId}
254257
</span>
255258
</Popover>
256259
);

0 commit comments

Comments
 (0)