Skip to content

Commit 312af9d

Browse files
jjoyce0510John Joyce
andauthored
feat(context): Various UI improvements for Context Base (Part 1/2) (#15413)
Co-authored-by: John Joyce <[email protected]>
1 parent 9fceb38 commit 312af9d

23 files changed

+1163
-337
lines changed

datahub-web-react/src/app/document/hooks/__tests__/useExtractMentions.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ describe('useExtractMentions', () => {
163163
const { result } = renderHook(() => useExtractMentions(content));
164164

165165
expect(result.current.documentUrns).toEqual([]);
166-
// Note: The regex stops at the first closing ) which is part of the URN structure
167-
// This is a known limitation of the simple regex pattern
168-
expect(result.current.assetUrns).toEqual(['urn:li:dataset:(urn:li:dataPlatform:kafka,topic-123,PROD']);
166+
// The regex now correctly handles nested parentheses in URNs
167+
expect(result.current.assetUrns).toEqual(['urn:li:dataset:(urn:li:dataPlatform:kafka,topic-123,PROD)']);
169168
});
170169
});

datahub-web-react/src/app/document/hooks/__tests__/useUpdateDocument.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,9 @@ describe('useUpdateDocument', () => {
485485
const success = await result.current.updateRelatedEntities(input);
486486

487487
expect(success).toBe(false);
488-
expect(message.error).toHaveBeenCalledWith('Failed to related assets. An unexpected error occurred!');
488+
expect(message.error).toHaveBeenCalledWith(
489+
'Failed to update related assets. An unexpected error occurred!',
490+
);
489491
});
490492
});
491493

datahub-web-react/src/app/document/hooks/useExtractMentions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export const useExtractMentions = (content: string) => {
99
if (!content) return { documentUrns: [], assetUrns: [] };
1010

1111
// Match markdown link syntax: [text](urn:li:entityType:id)
12-
const urnPattern = /\[([^\]]+)\]\((urn:li:[a-zA-Z]+:[^\s)]+)\)/g;
12+
// Handle URNs with nested parentheses by matching everything between the markdown link's parens
13+
// The pattern matches: [text](urn:li:entityType:...) where ... can include nested parens
14+
// We match the URN prefix, then allow nested paren groups or non-paren characters (one or more)
15+
const urnPattern = /\[([^\]]+)\]\((urn:li:[a-zA-Z]+:(?:[^)(]+|\([^)]*\))+)\)/g;
1316
const matches = Array.from(content.matchAll(urnPattern));
1417

1518
const documentUrns: string[] = [];

datahub-web-react/src/app/document/hooks/useUpdateDocument.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export function useUpdateDocument() {
166166
throw new Error('Failed to update related entities');
167167
} catch (error) {
168168
console.error('Failed to update related entities:', error);
169-
message.error('Failed to related assets. An unexpected error occurred!');
169+
message.error('Failed to update related assets. An unexpected error occurred!');
170170
// Silent fail - don't show error message for this operation
171171
return false;
172172
}

datahub-web-react/src/app/entityV2/document/DocumentEntity.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { colors } from '@components';
12
import { FileText } from '@phosphor-icons/react';
23
import * as React from 'react';
34

@@ -48,7 +49,7 @@ export class DocumentEntity implements Entity<Document> {
4849
);
4950
}
5051

51-
return <FileText size={fontSize || 20} color={color} weight="duotone" />;
52+
return <FileText size={fontSize || 20} color={color || colors.gray[1700]} weight="duotone" />;
5253
};
5354

5455
isSearchEnabled = () => true;

datahub-web-react/src/app/entityV2/document/DocumentNativeProfile.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import styled from 'styled-components';
55

66
import EntityContext from '@app/entity/shared/EntityContext';
77
import { DocumentSummaryTab } from '@app/entityV2/document/summary/DocumentSummaryTab';
8-
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
9-
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
108
import EntityProfileSidebar from '@app/entityV2/shared/containers/profile/sidebar/EntityProfileSidebar';
119
import EntitySidebarSectionsTab from '@app/entityV2/shared/containers/profile/sidebar/EntitySidebarSectionsTab';
1210
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
13-
import { SidebarGlossaryTermsSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarGlossaryTermsSection';
14-
import { SidebarTagsSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarTagsSection';
1511
import { PropertiesTab } from '@app/entityV2/shared/tabs/Properties/PropertiesTab';
1612
import { PageTemplateProvider } from '@app/homeV3/context/PageTemplateContext';
1713
import CompactContext from '@app/shared/CompactContext';
@@ -81,22 +77,11 @@ interface Props {
8177
}
8278

8379
// Define sidebar sections - these will be wrapped in a Summary tab
80+
// For context documents, we only show Owners to keep the sidebar simple
8481
const sidebarSections = [
8582
{
8683
component: SidebarOwnerSection,
8784
},
88-
{
89-
component: SidebarTagsSection,
90-
},
91-
{
92-
component: SidebarGlossaryTermsSection,
93-
},
94-
{
95-
component: SidebarDomainSection,
96-
},
97-
{
98-
component: DataProductSection,
99-
},
10085
];
10186

10287
/**
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Button, Tooltip } from '@components';
2+
import React, { useCallback, useState } from 'react';
3+
import styled from 'styled-components';
4+
5+
import { EntitySearchDropdown } from '@app/entityV2/shared/EntitySearchSelect/EntitySearchDropdown';
6+
import colors from '@src/alchemy-components/theme/foundations/colors';
7+
8+
import { AndFilterInput, EntityType } from '@types';
9+
10+
const ActionsContainer = styled.div`
11+
display: flex;
12+
gap: 8px;
13+
padding: 0px;
14+
justify-content: flex-end;
15+
`;
16+
17+
const AddButton = styled(Button)`
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
width: 24px;
22+
height: 24px;
23+
padding: 0;
24+
margin: 0;
25+
border: none;
26+
border-radius: 4px;
27+
background: transparent;
28+
color: ${colors.gray[400]};
29+
cursor: pointer;
30+
transition: all 0.2s ease;
31+
32+
svg {
33+
width: 20px;
34+
height: 20px;
35+
}
36+
`;
37+
38+
export interface AddRelatedEntityDropdownProps {
39+
entityTypes: EntityType[];
40+
existingUrns: Set<string>;
41+
documentUrn: string;
42+
onConfirm: (selectedUrns: string[]) => Promise<void>;
43+
placeholder?: string;
44+
defaultFilters?: AndFilterInput[];
45+
viewUrn?: string;
46+
disabled?: boolean;
47+
// Initial selected URNs (existing related entities) - these will be pre-selected in the dropdown
48+
initialSelectedUrns?: string[];
49+
}
50+
51+
/**
52+
* A dropdown component for adding related entities to a document.
53+
* Encapsulates the search, selection, and confirmation logic.
54+
*/
55+
export const AddRelatedEntityDropdown: React.FC<AddRelatedEntityDropdownProps> = ({
56+
entityTypes,
57+
existingUrns: _existingUrns,
58+
documentUrn,
59+
onConfirm,
60+
placeholder = 'Add related entities...',
61+
defaultFilters,
62+
viewUrn,
63+
disabled = false,
64+
initialSelectedUrns = [],
65+
}) => {
66+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
67+
const [selectedUrns, setSelectedUrns] = useState<string[]>(initialSelectedUrns);
68+
69+
const handleConfirmAdd = useCallback(async () => {
70+
// Filter out the document itself (can't relate a document to itself)
71+
const finalUrns = selectedUrns.filter((urn) => urn !== documentUrn);
72+
73+
try {
74+
// Pass the final list of URNs (includes both additions and removals)
75+
// The parent will handle replacing the entire list
76+
await onConfirm(finalUrns);
77+
setIsDropdownOpen(false);
78+
// Reset to initial state when closing
79+
setSelectedUrns(initialSelectedUrns);
80+
} catch (error) {
81+
console.error('Failed to update related entities:', error);
82+
}
83+
}, [selectedUrns, documentUrn, onConfirm, initialSelectedUrns]);
84+
85+
const handleCancel = useCallback(() => {
86+
setIsDropdownOpen(false);
87+
// Reset to initial state when canceling
88+
setSelectedUrns(initialSelectedUrns);
89+
}, [initialSelectedUrns]);
90+
91+
const actionButtons = (
92+
<ActionsContainer>
93+
<Button onClick={handleCancel} variant="secondary" size="sm">
94+
Cancel
95+
</Button>
96+
<Button onClick={handleConfirmAdd} size="sm" disabled={selectedUrns.length === 0}>
97+
Update
98+
</Button>
99+
</ActionsContainer>
100+
);
101+
102+
const triggerButton = (
103+
<Tooltip title="Link related assets or context docs" placement="bottom">
104+
<AddButton
105+
variant="text"
106+
isCircle
107+
icon={{ icon: 'Plus', source: 'phosphor' }}
108+
aria-label="Add related entity"
109+
disabled={disabled}
110+
/>
111+
</Tooltip>
112+
);
113+
114+
return (
115+
<Tooltip title="Add related entity" placement="bottom">
116+
<EntitySearchDropdown
117+
entityTypes={entityTypes}
118+
selectedUrns={selectedUrns}
119+
onSelectionChange={setSelectedUrns}
120+
placeholder={placeholder}
121+
defaultFilters={defaultFilters}
122+
viewUrn={viewUrn}
123+
trigger={triggerButton}
124+
open={isDropdownOpen}
125+
onOpenChange={(open) => {
126+
setIsDropdownOpen(open);
127+
if (!open) {
128+
// Reset to initial state when closing without confirming
129+
setSelectedUrns(initialSelectedUrns);
130+
} else {
131+
// Initialize with existing URNs when opening
132+
setSelectedUrns(initialSelectedUrns);
133+
}
134+
}}
135+
placement="top"
136+
disabled={disabled}
137+
actionButtons={actionButtons}
138+
dropdownContainerStyle={{
139+
minWidth: '400px',
140+
maxHeight: '500px',
141+
}}
142+
/>
143+
</Tooltip>
144+
);
145+
};

datahub-web-react/src/app/entityV2/document/summary/EditableContent.tsx

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { useDocumentPermissions } from '@app/document/hooks/useDocumentPermissio
66
import { useExtractMentions } from '@app/document/hooks/useExtractMentions';
77
import { useUpdateDocument } from '@app/document/hooks/useUpdateDocument';
88
import { useRefetch } from '@app/entity/shared/EntityContext';
9-
import { RelatedAssetsSection } from '@app/entityV2/document/summary/RelatedAssetsSection';
10-
import { RelatedDocumentsSection } from '@app/entityV2/document/summary/RelatedDocumentsSection';
9+
import { RelatedSection } from '@app/entityV2/document/summary/RelatedSection';
1110
import useFileUpload from '@app/shared/hooks/useFileUpload';
1211
import useFileUploadAnalyticsCallbacks from '@app/shared/hooks/useFileUploadAnalyticsCallbacks';
1312
import colors from '@src/alchemy-components/theme/foundations/colors';
@@ -133,7 +132,10 @@ export const EditableContent: React.FC<EditableContentProps> = ({
133132
try {
134133
// Extract mentions from the content to save
135134
// Pattern matches markdown link syntax: [text](urn:li:entityType:id)
136-
const urnPattern = /\[([^\]]+)\]\((urn:li:[a-zA-Z]+:[^\s)]+)\)/g;
135+
// Handle URNs with nested parentheses by matching everything between the markdown link's parens
136+
// The pattern matches: [text](urn:li:entityType:...) where ... can include nested parens
137+
// We match the URN prefix, then allow nested paren groups or non-paren characters
138+
const urnPattern = /\[([^\]]+)\]\((urn:li:[a-zA-Z]+:(?:[^)(]+|\([^)]*\))+)\)/g;
137139
const matches = Array.from(contentToSave.matchAll(urnPattern));
138140
const documentUrnsToSave: string[] = [];
139141
const assetUrnsToSave: string[] = [];
@@ -149,17 +151,30 @@ export const EditableContent: React.FC<EditableContentProps> = ({
149151
}
150152
});
151153

154+
// Merge new URNs with existing ones (additive, not replacement)
155+
// Get existing URNs
156+
const existingAssetUrns = new Set(relatedAssets?.map((ra) => ra.asset.urn) || []);
157+
const existingDocumentUrns = new Set(relatedDocuments?.map((rd) => rd.document.urn) || []);
158+
159+
// Add new URNs to existing sets (automatically handles duplicates)
160+
assetUrnsToSave.forEach((urn) => existingAssetUrns.add(urn));
161+
documentUrnsToSave.forEach((urn) => existingDocumentUrns.add(urn));
162+
163+
// Convert back to arrays
164+
const finalAssetUrns = Array.from(existingAssetUrns);
165+
const finalDocumentUrns = Array.from(existingDocumentUrns);
166+
152167
// Save content
153168
await updateContents({
154169
urn: documentUrn,
155170
contents: { text: contentToSave },
156171
});
157172

158-
// Update related entities based on @ mentions
173+
// Update related entities - merge new mentions with existing ones
159174
await updateRelatedEntities({
160175
urn: documentUrn,
161-
relatedAssets: assetUrnsToSave,
162-
relatedDocuments: documentUrnsToSave,
176+
relatedAssets: finalAssetUrns,
177+
relatedDocuments: finalDocumentUrns,
163178
});
164179

165180
// Track that we just saved this content to prevent remount on refetch
@@ -173,7 +188,17 @@ export const EditableContent: React.FC<EditableContentProps> = ({
173188
setIsSaving(false);
174189
}
175190
},
176-
[isSaving, initialContent, canEditContents, updateContents, updateRelatedEntities, documentUrn, refetch],
191+
[
192+
isSaving,
193+
initialContent,
194+
canEditContents,
195+
updateContents,
196+
updateRelatedEntities,
197+
documentUrn,
198+
refetch,
199+
relatedAssets,
200+
relatedDocuments,
201+
],
177202
);
178203

179204
// Auto-save after 2 seconds of no typing
@@ -212,6 +237,52 @@ export const EditableContent: React.FC<EditableContentProps> = ({
212237
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
213238
}, [content, initialContent, canEditContents, isSaving, saveDocument]);
214239

240+
// Handle updating related entities (supports both adding and removing)
241+
// The passed URNs represent the final desired list after user selections/deselections
242+
const handleAddEntities = useCallback(
243+
async (assetUrns: string[], documentUrns: string[]) => {
244+
// The URNs passed here are the final list (after user selections/deselections in the dropdown)
245+
// So we replace the entire list, which handles both additions and removals
246+
await updateRelatedEntities({
247+
urn: documentUrn,
248+
relatedAssets: assetUrns,
249+
relatedDocuments: documentUrns,
250+
});
251+
252+
// Refetch to get updated data
253+
await refetch();
254+
},
255+
[documentUrn, updateRelatedEntities, refetch],
256+
);
257+
258+
// Handle removing a single related entity
259+
const handleRemoveEntity = useCallback(
260+
async (urnToRemove: string) => {
261+
// Get existing URNs
262+
const existingAssetUrns = relatedAssets?.map((ra) => ra.asset.urn) || [];
263+
const existingDocumentUrns = relatedDocuments?.map((rd) => rd.document.urn) || [];
264+
265+
// Remove the URN from the appropriate list
266+
const isDocument = urnToRemove.includes(':document:');
267+
const finalAssetUrns = isDocument
268+
? existingAssetUrns
269+
: existingAssetUrns.filter((urn) => urn !== urnToRemove);
270+
const finalDocumentUrns = isDocument
271+
? existingDocumentUrns.filter((urn) => urn !== urnToRemove)
272+
: existingDocumentUrns;
273+
274+
await updateRelatedEntities({
275+
urn: documentUrn,
276+
relatedAssets: finalAssetUrns,
277+
relatedDocuments: finalDocumentUrns,
278+
});
279+
280+
// Refetch to get updated data
281+
await refetch();
282+
},
283+
[documentUrn, updateRelatedEntities, refetch, relatedAssets, relatedDocuments],
284+
);
285+
215286
return (
216287
<ContentWrapper>
217288
<EditorSection
@@ -253,8 +324,16 @@ export const EditableContent: React.FC<EditableContentProps> = ({
253324
)}
254325
</EditorSection>
255326

256-
<RelatedDocumentsSection relatedDocuments={relatedDocuments} />
257-
<RelatedAssetsSection relatedAssets={relatedAssets} />
327+
{!isEditorFocused && (
328+
<RelatedSection
329+
relatedAssets={relatedAssets}
330+
relatedDocuments={relatedDocuments}
331+
documentUrn={documentUrn}
332+
onAddEntities={handleAddEntities}
333+
onRemoveEntity={handleRemoveEntity}
334+
canEdit={canEditContents}
335+
/>
336+
)}
258337
</ContentWrapper>
259338
);
260339
};

0 commit comments

Comments
 (0)