diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/drive/ContextFileExtractionService.java b/openmetadata-service/src/main/java/org/openmetadata/service/drive/ContextFileExtractionService.java index c94612eab049..cc7db9c4fe82 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/drive/ContextFileExtractionService.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/drive/ContextFileExtractionService.java @@ -1,6 +1,5 @@ package org.openmetadata.service.drive; -import static org.openmetadata.service.Entity.ADMIN_USER_NAME; import static org.openmetadata.service.jdbi3.ContextFileContentRepository.CONTEXT_FILE_CONTENT_ENTITY; import static org.openmetadata.service.jdbi3.ContextFileRepository.CONTEXT_FILE_ENTITY; @@ -299,7 +298,7 @@ private boolean updateFile(UUID fileId, Function updat return false; } try { - repository.updateIfCurrent(null, current, updated, ADMIN_USER_NAME); + repository.updateIfCurrent(null, current, updated, current.getUpdatedBy()); return true; } catch (PreconditionFailedException e) { LOG.debug("Context file {} changed during extraction update", fileId); @@ -325,7 +324,9 @@ private boolean updateContent( return false; } try { - repository.getContentRepository().updateIfCurrent(null, current, updated, ADMIN_USER_NAME); + repository + .getContentRepository() + .updateIfCurrent(null, current, updated, current.getUpdatedBy()); return true; } catch (PreconditionFailedException e) { LOG.debug("Context file content {} changed during extraction update", contentId); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/drive/ContextFileExtractionServiceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/drive/ContextFileExtractionServiceTest.java index 6f4ba1d3cb3b..3201aecdf97a 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/drive/ContextFileExtractionServiceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/drive/ContextFileExtractionServiceTest.java @@ -51,6 +51,8 @@ @ExtendWith(MockitoExtension.class) class ContextFileExtractionServiceTest { + private static final String UPLOADER = "test.user"; + @Mock private ContextFileRepository repository; @Mock private ContextFileContentRepository contentRepository; @Mock private AssetRepository assetRepository; @@ -78,7 +80,8 @@ void setUp() { .withFileType(ContextFileType.PDF) .withFileExtension("pdf") .withHeadContentId(contentId.toString()) - .withProcessingStatus(ProcessingStatus.Uploaded); + .withProcessingStatus(ProcessingStatus.Uploaded) + .withUpdatedBy(UPLOADER); content = new ContextFileContent() @@ -86,7 +89,8 @@ void setUp() { .withName("v1") .withAssetId("asset-1") .withContextFile(file.getEntityReference()) - .withProcessingStatus(ProcessingStatus.Uploaded); + .withProcessingStatus(ProcessingStatus.Uploaded) + .withUpdatedBy(UPLOADER); asset = new Asset(); asset.setId("asset-1"); @@ -112,9 +116,9 @@ void processSuccessMarksAnalyzingThenProcessed() throws Exception { service(Runnable::run, () -> assetService).process(fileId, contentId); verify(repository, times(2)) - .updateIfCurrent(isNull(), same(file), updatedFileCaptor.capture(), anyString()); + .updateIfCurrent(isNull(), same(file), updatedFileCaptor.capture(), eq(UPLOADER)); verify(contentRepository, times(2)) - .updateIfCurrent(isNull(), same(content), updatedContentCaptor.capture(), anyString()); + .updateIfCurrent(isNull(), same(content), updatedContentCaptor.capture(), eq(UPLOADER)); List fileUpdates = updatedFileCaptor.getAllValues(); assertEquals(ProcessingStatus.Analyzing, fileUpdates.get(0).getProcessingStatus()); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDashboard.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDashboard.spec.ts index 3043e877a03f..08c0f42b92b2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDashboard.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterDashboard.spec.ts @@ -124,6 +124,8 @@ test.describe('Context Center - Dashboard', () => { .catch(() => false); if (isStillInTopThree) { await expect(seededDocument).toBeVisible(); + await seededDocument.click(); + await expect(page).toHaveURL(/\/context-center\/documents\?document=/); } }); @@ -156,6 +158,8 @@ test.describe('Context Center - Dashboard', () => { .catch(() => false); if (isStillInTopThree) { await expect(seededMemory).toBeVisible(); + await seededMemory.click(); + await expect(page).toHaveURL(/\/context-center\/memories\?memory=/); } }); }); @@ -184,7 +188,13 @@ test.describe('Context Center - Dashboard', () => { const recentlyViewedCard = page.getByTestId('recently-viewed-card'); await expect(recentlyViewedCard).toBeVisible(); - await expect(recentlyViewedCard.getByText(displayName)).toBeVisible(); + + const recentlyViewedItem = recentlyViewedCard.getByText(displayName); + await expect(recentlyViewedItem).toBeVisible(); + await recentlyViewedItem.click(); + await expect(page).toHaveURL( + new RegExp(`/context-center/articles/${article.fullyQualifiedName}`) + ); }); }); @@ -227,6 +237,10 @@ test.describe('Context Center - Dashboard', () => { const firstItem = mostCitedCard.getByTestId('most-cited-count').first(); await expect(firstItem).toContainText('Cited 999999 times'); + + const firstItemRow = mostCitedCard.getByRole('button').first(); + await firstItemRow.click(); + await expect(page).toHaveURL(/\/context-center\/memories\?memory=/); }); }); @@ -286,6 +300,15 @@ test.describe('Context Center - Dashboard', () => { const childRow = tree.getByRole('row', { name: fileName }); await expect(childRow).toBeVisible(); + + await childRow.getByRole('button', { name: fileName }).click(); + await expect(page).toHaveURL( + new RegExp(`/context-center/documents\\?document=${file.id}`) + ); + + const panel = page.getByTestId('document-preview-panel'); + await expect(panel).toBeVisible(); + await expect(panel.getByTestId('preview-file-name')).toHaveText(fileName); }); }); @@ -374,19 +397,29 @@ test.describe('Context Center - Dashboard', () => { await test.step('Articles card redirects to /context-center/articles', async () => { await navigateToDashboard(page); - await page.getByTestId('article-detail-card').click(); + await page + .getByTestId('article-detail-card') + .getByRole('button', { name: 'View All Articles' }) + .click(); await expect(page).toHaveURL(/\/context-center\/articles/); }); await test.step('Documents card redirects to /context-center/documents', async () => { await navigateToDashboard(page); - await page.getByTestId('document-detail-card').click(); + await page + .getByTestId('document-detail-card') + .getByRole('button', { name: 'View All Documents' }) + .first() + .click(); await expect(page).toHaveURL(/\/context-center\/documents/); }); await test.step('Memories card redirects to /context-center/memories', async () => { await navigateToDashboard(page); - await page.getByTestId('memory-detail-card').click(); + await page + .getByTestId('memory-detail-card') + .getByRole('button', { name: 'View All Memories' }) + .click(); await expect(page).toHaveURL(/\/context-center\/memories/); await waitForAllLoadersToDisappear(page); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterPermission.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterPermission.spec.ts index 282fcf081e90..8088e31c10d6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterPermission.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenterPermission.spec.ts @@ -38,6 +38,7 @@ import { navigateToMemories, scrollHierarchyToNode, scrollListingToCard, + searchAndGetDocumentRow, searchAndGetMemoryRow, uploadDisposableDocument, waitForDocumentInArchive, @@ -1240,6 +1241,19 @@ test.describe('Context Center Permissions', () => { const { apiContext, afterAction } = await getDefaultAdminAPIContext( browser ); + + await waitForDocumentProcessingComplete(apiContext, uploadedData.id); + + await createAllPage.reload(); + await waitForAllLoadersToDisappear(createAllPage); + await navigateToDocuments(createAllPage); + + const row = await searchAndGetDocumentRow(createAllPage, fileName); + await expect(row).toBeVisible(); + await expect(row.getByTestId('document-updated-by')).toHaveText( + createAllUser.responseData.name + ); + await apiContext .delete( `/api/v1/contextCenter/drive/files/${uploadedData.id}?hardDelete=true` diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx index bfa254fe02d9..8df0177d8bfc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx @@ -68,7 +68,7 @@ const ArchiveRow: FC = ({ return ( ; readonly item: PillarRecentItem; }) { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + item.onClick(); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + item.onClick(); + } + }; + return ( - + {item.icon ? ( item.icon ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.interface.ts index 1f3ab6273644..d44161ab1806 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.interface.ts @@ -19,6 +19,7 @@ export interface PillarRecentItem { title: string; meta: string[]; icon?: ReactElement; + onClick: () => void; } export interface ContextKnowledgePillarCardProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.test.tsx new file mode 100644 index 000000000000..77120fd30358 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextKnowledgePillarCard/ContextKnowledgePillarCard.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fireEvent, render, screen } from '@testing-library/react'; +import { ReactComponent as FileIcon } from '../../../assets/svg/common/file.svg'; +import ContextKnowledgePillarCard from './ContextKnowledgePillarCard.component'; +import { PillarRecentItem } from './ContextKnowledgePillarCard.interface'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +const baseProps = { + cta: 'View all', + icon: FileIcon, + stat: '5', + statSub: 'items', + subtitle: 'subtitle', + title: 'Articles', +}; + +describe('ContextKnowledgePillarCard', () => { + it('calls onClick when the card body is clicked', () => { + const onClick = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByTestId('article-detail-card')); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('calls the item onClick and not the card onClick when a recent item with onClick is clicked', () => { + const onCardClick = jest.fn(); + const onItemClick = jest.fn(); + const recent: PillarRecentItem[] = [ + { meta: [], onClick: onItemClick, title: 'Item 1' }, + ]; + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Item 1' })); + + expect(onItemClick).toHaveBeenCalledTimes(1); + expect(onCardClick).not.toHaveBeenCalled(); + }); + + it('calls onClick when the CTA button is clicked, without double-firing from the card', () => { + const onClick = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'View all' })); + + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextSimplePillarCard/ContextSimplePillarCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextSimplePillarCard/ContextSimplePillarCard.component.tsx index 612c66bffd62..cb54c2b3ee05 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextSimplePillarCard/ContextSimplePillarCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextSimplePillarCard/ContextSimplePillarCard.component.tsx @@ -57,7 +57,7 @@ const ContextSimplePillarCard: FC = ({
{isLoading ? ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx index 6677f3dea48a..f59936470801 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateMemoryModal/CreateMemoryModal.component.tsx @@ -198,6 +198,16 @@ const EmptyLinkedAssets: FC = () => { ); }; +const EmptyTags: FC = () => { + const { t } = useTranslation(); + + return ( + + {t('label.no-tags-added')} + + ); +}; + const LinkedAssetsReadOnly: FC<{ assets: DataAssetOption[] }> = ({ assets, }) => { @@ -966,6 +976,9 @@ const CreateMemoryModal: FC = ({
+ {isViewOnly && selectedTags.length === 0 && ( + + )} {selectedTags.map((tag) => isViewOnly ? ( = ({ folders, isLoading = false, onCreateFolder, + onOpenFile, }) => { const { t } = useTranslation(); const [expandedKeys, setExpandedKeys] = useState>(new Set()); @@ -74,6 +82,42 @@ const DashboardFoldersCard: FC = ({ } }; + const toggleExpanded = (folderId: string) => { + const next = new Set(expandedKeys); + if (next.has(folderId)) { + next.delete(folderId); + } else { + next.add(folderId); + } + handleExpandedChange(next); + }; + + const handleToggleExpand = (e: MouseEvent, folderId: string) => { + e.stopPropagation(); + toggleExpanded(folderId); + }; + + const handleToggleExpandKeyDown = (e: KeyboardEvent, folderId: string) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleExpanded(folderId); + } + }; + + const handleOpenFile = (e: MouseEvent, fileId: string) => { + e.stopPropagation(); + onOpenFile(fileId); + }; + + const handleOpenFileKeyDown = (e: KeyboardEvent, fileId: string) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + onOpenFile(fileId); + } + }; + return ( = ({ key={folder.id} textValue={getEntityName(folder)}> = ({ className="tw:flex-1 tw:min-w-0" gap={3} justify="between"> - + + handleToggleExpand(e, folder.id) + } + onKeyDown={(e: KeyboardEvent) => + handleToggleExpandKeyDown(e, folder.id) + }>
= ({
- - - {folder.childrenCount ?? 0} - - - + + {folder.childrenCount ?? 0} + +
@@ -144,18 +198,29 @@ const DashboardFoldersCard: FC = ({ key={file.id} textValue={getEntityName(file)}> - - - {getEntityName(file)} - + handleOpenFile(e, file.id)} + onKeyDown={(e: KeyboardEvent) => + handleOpenFileKeyDown(e, file.id) + }> + + + {getEntityName(file)} + + ))} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.interface.ts index 696c17c9043d..a4f28b31393d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.interface.ts @@ -16,4 +16,5 @@ export interface DashboardFoldersCardProps { folders: Folder[]; isLoading?: boolean; onCreateFolder?: () => void; + onOpenFile: (fileId: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.test.tsx index a611b25929a7..339c9a9e80f5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DashboardFoldersCard/DashboardFoldersCard.test.tsx @@ -17,7 +17,16 @@ import { listContextFiles } from '../../../rest/assetAPI'; import DashboardFoldersCard from './DashboardFoldersCard.component'; jest.mock('../../../rest/assetAPI', () => ({ - listContextFiles: jest.fn().mockResolvedValue({ data: [], paging: {} }), + listContextFiles: jest.fn().mockResolvedValue({ + data: [ + { + id: 'file-1', + name: 'report.pdf', + displayName: 'report.pdf', + }, + ], + paging: {}, + }), })); jest.mock('react-i18next', () => ({ @@ -41,7 +50,9 @@ const MOCK_FOLDERS: Folder[] = [ describe('DashboardFoldersCard', () => { it('renders the folder list with children count badges', () => { - render(); + render( + + ); expect(screen.getByText('Reports')).toBeInTheDocument(); expect(screen.getByText('Archive')).toBeInTheDocument(); @@ -49,7 +60,7 @@ describe('DashboardFoldersCard', () => { }); it('renders the empty state when there are no folders', () => { - render(); + render(); expect( screen.getByText('message.no-folders-yet-create-one') @@ -59,7 +70,11 @@ describe('DashboardFoldersCard', () => { it('renders the New Folder action with a leading icon and triggers onCreateFolder on click', () => { const onCreateFolder = jest.fn(); render( - + ); const newFolderButton = screen.getByRole('button', { @@ -74,8 +89,59 @@ describe('DashboardFoldersCard', () => { }); it('does not fetch children until a folder is expanded', () => { - render(); + render( + + ); expect(listContextFiles).not.toHaveBeenCalled(); }); + + it('fetches children when the folder name/row is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Reports' })); + + expect(listContextFiles).toHaveBeenCalledWith({ + folderId: 'folder-1', + limit: expect.any(Number), + }); + }); + + it('fetches children when the expand chevron is clicked', () => { + render( + + ); + + const folderNameButton = screen.getByRole('button', { name: 'Reports' }); + const folderRow = folderNameButton.closest('[role="row"]') as HTMLElement; + const rowButtons = Array.from(folderRow.querySelectorAll('button')); + const expandButton = rowButtons.find((btn) => btn !== folderNameButton); + + expect(expandButton).toBeDefined(); + + fireEvent.click(expandButton as HTMLButtonElement); + + expect(listContextFiles).toHaveBeenCalledWith({ + folderId: 'folder-1', + limit: expect.any(Number), + }); + }); + + it('calls onOpenFile with the file id when a child file row is clicked', async () => { + const onOpenFile = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Reports' })); + + const fileButton = await screen.findByRole('button', { + name: 'report.pdf', + }); + fireEvent.click(fileButton); + + expect(onOpenFile).toHaveBeenCalledWith('file-1'); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx index 5642945d141e..114b16efab97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx @@ -19,6 +19,7 @@ import { Modal, ModalOverlay, } from '@openmetadata/ui-core-components'; +import { AxiosError } from 'axios'; import { FC, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { v4 as uuidv4 } from 'uuid'; @@ -26,7 +27,7 @@ import { DOCUMENT_MAX_FILE_SIZE } from '../../../constants/ContextCenter.constan import { ContextFile } from '../../../generated/entity/data/contextFile'; import { uploadDriveFile } from '../../../rest/assetAPI'; import { runWithConcurrencyLimit } from '../../../utils/AsyncUtils'; -import { showSuccessToast } from '../../../utils/ToastUtils'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { QueuedFile, UploadDocumentModalProps, @@ -92,12 +93,13 @@ const UploadDocumentModal: FC = ({ ): Promise => { try { return await uploadDriveFile(entry.file, folderFqn); - } catch { + } catch (err) { setFiles((prev) => prev.map((f) => f.id === entry.id ? { ...f, progress: 0, status: 'error' } : f ) ); + showErrorToast(err as AxiosError, t('message.upload-failed')); return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx index 91538c3c0507..d6eb35a60c89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx @@ -19,6 +19,7 @@ import { waitFor, } from '@testing-library/react'; import { uploadDriveFile } from '../../../rest/assetAPI'; +import { showErrorToast } from '../../../utils/ToastUtils'; import UploadDocumentModal from './UploadDocumentModal.component'; jest.mock('rest/assetAPI', () => ({ @@ -26,6 +27,7 @@ jest.mock('rest/assetAPI', () => ({ })); jest.mock('utils/ToastUtils', () => ({ + showErrorToast: jest.fn(), showSuccessToast: jest.fn(), })); @@ -318,6 +320,10 @@ describe('UploadDocumentModal', () => { const bar = await screen.findByTestId('progress-bar-fail.pdf'); expect(bar).toHaveAttribute('data-failed', 'true'); + expect(showErrorToast).toHaveBeenCalledWith( + new Error('upload failed'), + 'message.upload-failed' + ); }); it('shows retry button for failed uploads', async () => { @@ -334,6 +340,10 @@ describe('UploadDocumentModal', () => { fireEvent.click(screen.getByText(/attach-file-plural/i)); expect(await screen.findByTestId('retry-fail.pdf')).toBeInTheDocument(); + expect(showErrorToast).toHaveBeenCalledWith( + new Error('upload failed'), + 'message.upload-failed' + ); }); it('retries a failed upload when the retry button is clicked', async () => { @@ -358,4 +368,29 @@ describe('UploadDocumentModal', () => { expect(defaultProps.onUploaded).toHaveBeenCalledWith([mockAsset]) ); }); + + it('shows an error toast again when a retried upload also fails', async () => { + (uploadDriveFile as jest.Mock) + .mockRejectedValueOnce(new Error('first attempt failed')) + .mockRejectedValueOnce(new Error('retry failed')); + + render(); + + act(() => { + mockOnDropFiles!(makeFileList(new File(['content'], 'fail.pdf'))); + }); + + fireEvent.click(screen.getByText(/attach-file-plural/i)); + + const retryBtn = await screen.findByTestId('retry-fail.pdf'); + fireEvent.click(retryBtn); + + await waitFor(() => expect(uploadDriveFile).toHaveBeenCalledTimes(2)); + await waitFor(() => + expect(showErrorToast).toHaveBeenCalledWith( + new Error('retry failed'), + 'message.upload-failed' + ) + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetSelectList/DataAssetPickerShell.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetSelectList/DataAssetPickerShell.tsx index 01efbe6f1538..2fcd21e32c3a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetSelectList/DataAssetPickerShell.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetSelectList/DataAssetPickerShell.tsx @@ -232,7 +232,7 @@ const DataAssetPickerShell: FC = ({ t('label.search-entity', { entity: t('label.asset-plural') }) } value={searchText} - wrapperClassName="tw:rounded-none tw:bg-transparent tw:shadow-none" + wrapperClassName="tw:rounded-none tw:bg-transparent! tw:shadow-none! tw:outline-0!" onChange={(value) => onSearchChange?.(value)} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx index bc3c35532617..f5583619abb5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx @@ -39,7 +39,6 @@ import { useNavigate } from 'react-router-dom'; import { ReactComponent as AddPlaceHolderIcon } from '../../../assets/svg/add-placeholder.svg'; import { ReactComponent as NoSearchResultIcon } from '../../../assets/svg/common/no-search-result.svg'; import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import Loader from '../../../components/common/Loader/Loader'; import { VotingDataProps } from '../../../components/Entity/Voting/voting.interface'; import { CREATE_PAGE_HASH, @@ -73,6 +72,7 @@ import { searchQuery as fetchSearchResults } from '../../../rest/searchAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../../utils/ToastUtils'; +import Loader from '../../common/Loader/Loader'; import KnowledgeCard from '../KnowledgeCard/KnowledgeCard'; import KnowledgePageListRightPanel from '../KnowledgePageListRightPanel/KnowledgePageListRightPanel'; import { @@ -603,7 +603,7 @@ const KnowledgePageListComponent = forwardRef< ))} - {isLoadingMore ? : null} + {isLoadingMore ? : null}
{ />
- {!isLoading && (hasEverHadItems || items.length > 0) && ( + {(hasEverHadItems || items.length > 0) && (
{ const [documentsCount, setDocumentsCount] = useState(0); const [folders, setFolders] = useState([]); const [folderCount, setFolderCount] = useState(0); - const [memories, setMemories] = useState< - Array<{ title: string; meta: string[] }> - >([]); + const [memories, setMemories] = useState([]); const [memoriesCount, setMemoriesCount] = useState(0); const [mostCitedMemories, setMostCitedMemories] = useState( [] @@ -221,12 +220,7 @@ const ContextCenterDashboardPage: FC = () => { sortOrder: 'desc', }); setMemoriesCount(response.paging.total ?? response.data.length); - setMemories( - response.data.map((m) => ({ - title: m.title ?? m.name, - meta: [`cited ${m.usageCount}×`], - })) - ); + setMemories(response.data); } catch (err) { showErrorToast(err as AxiosError); } finally { @@ -286,6 +280,44 @@ const ContextCenterDashboardPage: FC = () => { fetchFolders(); }, [fetchFolders]); + const handleOpenKnowledgePage = useCallback( + (page: Pick) => { + if (page.pageType === PageType.QUICK_LINK) { + window.open( + (page.page as QuickLink).url, + '_blank', + 'noopener,noreferrer' + ); + + return; + } + navigate(contextCenterClassBase.getArticlePath(page.fullyQualifiedName)); + }, + [navigate] + ); + + const handleOpenDocument = useCallback( + (documentId: string) => { + navigate( + `${contextCenterClassBase.getDocumentsListPath()}?document=${encodeURIComponent( + documentId + )}` + ); + }, + [navigate] + ); + + const handleOpenMemory = useCallback( + (memoryName: string) => { + navigate( + `${contextCenterClassBase.getMemoriesListPath()}?memory=${encodeURIComponent( + memoryName + )}` + ); + }, + [navigate] + ); + const articlesRecentItems = useMemo( () => articles.map((article) => { @@ -303,9 +335,10 @@ const ContextCenterDashboardPage: FC = () => { icon, meta: metaParts, title: getEntityName(article), + onClick: () => handleOpenKnowledgePage(article), }; }), - [articles] + [articles, handleOpenKnowledgePage] ); const documentsRecentItems = useMemo( @@ -314,11 +347,15 @@ const ContextCenterDashboardPage: FC = () => { const metaParts = [ doc.updatedBy, getShortRelativeTime(doc.updatedAt), - ].filter(Boolean); + ].filter((part): part is string => Boolean(part)); - return { title: getEntityName(doc), meta: metaParts }; + return { + title: getEntityName(doc), + meta: metaParts, + onClick: () => handleOpenDocument(doc.id), + }; }), - [documents] + [documents, handleOpenDocument] ); const recentlyViewedItems = useMemo(() => { @@ -330,8 +367,19 @@ const ContextCenterDashboardPage: FC = () => { title: getEntityName(page), pageType: page.pageType, time: page.timestamp ? getShortRelativeTime(page.timestamp) : '', + onClick: () => handleOpenKnowledgePage(page), })); - }, [recentlyViewedQuickLinks]); + }, [recentlyViewedQuickLinks, handleOpenKnowledgePage]); + + const memoriesRecentItems = useMemo( + () => + memories.map((memory) => ({ + title: memory.title ?? getEntityName(memory), + meta: [`cited ${memory.usageCount}×`], + onClick: () => handleOpenMemory(memory.name), + })), + [memories, handleOpenMemory] + ); const mostCitedItems = useMemo( () => @@ -339,8 +387,9 @@ const ContextCenterDashboardPage: FC = () => { id: memory.id, title: memory.title ?? getEntityName(memory), citedCount: memory.usageCount ?? 0, + onClick: () => handleOpenMemory(memory.name), })), - [mostCitedMemories] + [mostCitedMemories, handleOpenMemory] ); return ( @@ -498,7 +547,7 @@ const ContextCenterDashboardPage: FC = () => { dataTestId="memory-detail-card" icon={MemoryIcon} isLoading={isMemoriesLoading} - recent={memories} + recent={memoriesRecentItems} stat={String(memoriesCount)} statSub={t('label.memory-plural')} subtitle={t('message.atomic-facts-ai-should-remember')} @@ -520,9 +569,18 @@ const ContextCenterDashboardPage: FC = () => { {recentlyViewedItems.map((item) => ( + key={item.id} + role="button" + tabIndex={0} + onClick={item.onClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + item.onClick(); + } + }}> {item.pageType === PageType.QUICK_LINK ? ( ) : ( @@ -566,6 +624,7 @@ const ContextCenterDashboardPage: FC = () => { ? () => setIsCreateFolderModalOpen(true) : undefined } + onOpenFile={handleOpenDocument} /> { {mostCitedItems.map((item) => ( + key={item.id} + role="button" + tabIndex={0} + onClick={item.onClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + item.onClick(); + } + }}> { setSearchParams, ]); + useEffect(() => { + const folderId = searchParams.get('folder'); + if (!folderId || isFoldersLoading || selectedFolderId) { + return; + } + setSelectedFolderId(folderId); + setSearchParams((prev) => { + prev.delete('folder'); + + return prev; + }); + }, [isFoldersLoading, selectedFolderId, searchParams, setSearchParams]); + const handleDeleteFile = useCallback((file: ContextFile) => { setFileToDelete(file); }, []);