From 80ce0459caaeb4871d36b592ff66d3f4bd37c748 Mon Sep 17 00:00:00 2001
From: Shailesh Parmar
Date: Thu, 8 Oct 2026 16:46:36 +0530
Subject: [PATCH 01/12] refactor(ui): remove antd from AI-mode Observability
Migrate every component reachable from AI-mode Observability to
@openmetadata/ui-core-components in place, with no behaviour change:
alerts, incidents, test cases and suites, pipelines, learning drawer,
entity headers, block editor, tasks and activity feed.
- Table.interface.ts now owns its table types instead of re-exporting antd's
- SlideoutMenu overlay stacks at z-50 like the core modal overlay
- EditorSlots skips the editing menus for read-only previews
- Playwright helpers target the core DOM instead of .ant-* selectors
Part of open-metadata/openmetadata-collate#6826
---
.../slideout-menu.theme.test.tsx | 14 +-
.../slideout-menus/slideout-menu.tsx | 2 +-
.../e2e/Features/ActivityAPI.spec.ts | 5 +-
.../ColumnBulkOperationsTagsGlossary.spec.ts | 10 +-
.../DataQuality/DataQualityDashboard.spec.ts | 7 +-
.../e2e/Features/IncidentManager.spec.ts | 19 +-
.../ui/playwright/e2e/Features/Tasks.spec.ts | 4 +-
.../Tasks/TaskAssigneeManagement.spec.ts | 6 +-
.../e2e/Features/Tasks/TaskCreation.spec.ts | 16 +-
.../e2e/Features/Tasks/TaskNavigation.spec.ts | 2 +-
.../ui/playwright/utils/activityFeed.ts | 11 +-
.../ui/playwright/utils/addTestCaseList.ts | 22 +-
.../ui/playwright/utils/dataQuality.ts | 15 +-
.../resources/ui/playwright/utils/dragDrop.ts | 2 +-
.../resources/ui/playwright/utils/glossary.ts | 28 +-
.../ui/playwright/utils/incidentManager.ts | 13 +-
.../resources/ui/playwright/utils/task.ts | 28 +-
.../ui/playwright/utils/taskWorkflow.ts | 16 +-
.../ActivityFeedcardNew.component.test.tsx | 10 +-
.../ActivityFeedcardNew.component.tsx | 241 ++++----
.../ActivityDescriptionFeed.tsx | 12 +-
.../OwnerFeed/ActivityOwnersFeed.tsx | 60 +-
.../TagsFeed/ActivityTagsFeed.tsx | 42 +-
.../ActivityEventFooter.test.tsx | 21 +
.../FeedCardFooter/ActivityEventFooter.tsx | 40 +-
.../FeedCardFooter/FeedCardFooterNew.tsx | 56 +-
.../ActivityFeedDrawer.test.tsx | 121 ++++
.../ActivityFeedDrawer/ActivityFeedDrawer.tsx | 137 ++---
.../activity-feed-drawer.less | 17 +-
.../ActivityFeedEditor/SendButton.tsx | 30 +-
.../ActivityFeedEditor/send-button.less | 2 +-
.../ActivityFeedPanel/FeedPanelHeader.tsx | 32 +-
.../ActivityFeedPanel/TaskPanelHeader.tsx | 29 +-
.../ActivityFeedPanel/feed-panel-body-v1.less | 12 +-
.../ActivityFeedPanel/feed-panel-header.less | 8 +-
.../ActivityFeedTab/activity-feed-tab.less | 43 +-
.../Shared/ActivityFeedActions.test.tsx | 16 +
.../Shared/ActivityFeedActions.tsx | 141 +++--
.../Shared/activity-feed-actions.less | 20 +-
.../BlockEditor/BlockEditor.interface.ts | 1 +
.../components/BlockEditor/BlockEditor.tsx | 7 +-
.../BubbleMenu/BubbleMenu.test.tsx | 4 +-
.../BlockEditor/BubbleMenu/BubbleMenu.tsx | 43 +-
.../BlockEditor/EditorSlots.test.tsx | 40 +-
.../components/BlockEditor/EditorSlots.tsx | 8 +-
.../Extensions/Callout/CalloutComponent.tsx | 8 +-
.../CodeBlock/CodeBlockComponent.tsx | 16 +-
.../AttachmentPlaceholder.tsx | 9 +-
.../Extensions/File/file-node.less | 2 +-
.../MathEquationComponent.test.tsx | 71 +++
.../MathEquation/MathEquationComponent.tsx | 69 ++-
.../Extensions/mention/MentionList.tsx | 18 +-
.../slash-command/SlashCommandList.tsx | 26 +-
.../BlockEditor/LinkModal/LinkModal.test.tsx | 4 +-
.../BlockEditor/LinkModal/LinkModal.tsx | 86 ++-
.../BlockEditor/LinkPopup/LinkPopup.tsx | 34 +-
.../BlockEditor/TableMenu/TableMenu.tsx | 100 ++--
.../components/BlockEditor/block-editor.less | 12 +-
.../AddTestCaseList.component.test.tsx | 105 +++-
.../AddTestCaseList.component.tsx | 254 ++++----
.../AddTestCaseListFilters.component.tsx | 7 +-
.../AddToBundleSuiteModal.component.tsx | 61 +-
.../AddToBundleSuiteModal.test.tsx | 64 +-
.../BundleSuiteForm.interface.ts | 2 -
.../Severity/Severity.component.tsx | 2 +-
.../Severity/Severity.interface.ts | 2 +-
.../Severity/SeverityModal.component.tsx | 85 +--
.../Severity/SeverityModal.test.tsx | 65 +-
...estCaseIncidentManagerStatus.component.tsx | 43 +-
.../TestCaseStatusModal.component.tsx | 320 +++++-----
.../TestCaseStatusModal.interface.ts | 16 +-
.../TestCaseStatusModal.test.tsx | 229 ++++++-
.../TestCases/TestCases.component.tsx | 4 +-
.../TestCases/useTestCaseFilterOptions.tsx | 18 +-
.../TestCases/useTestCaseFilters.test.tsx | 37 +-
.../TestCases/useTestCaseFilters.ts | 45 +-
.../TestCases/useTestCaseList.test.ts | 7 +-
.../DataQuality/TestCases/useTestCaseList.ts | 10 +-
.../TestCases/useTestCaseListPage.test.tsx | 29 +-
.../TestCases/useTestCaseListPage.tsx | 10 +-
.../TestSuitePipelineTab.component.tsx | 318 +++++-----
.../TestSuitePipelineTab.test.tsx | 17 +-
.../ColumnDetailPanel.test.tsx | 1 +
.../ProfilerProgressWidget.test.tsx | 39 +-
.../ProfilerProgressWidget.tsx | 62 +-
.../Database/SchemaEditor/CodeEditor.tsx | 24 +-
.../Database/SchemaEditor/SchemaEditor.tsx | 21 +-
.../SchemaTable/SchemaTable.component.tsx | 3 +-
.../EntityStatusBadge.component.tsx | 7 +-
.../EntityVersionTimeLine.tsx | 60 +-
.../Task/TaskTab/TaskTabNew.component.tsx | 563 +++++++++---------
.../Entity/Task/TaskTab/task-tab-new.less | 2 +-
.../Entity/Task/TaskTab/task-tab.less | 2 +-
...asktabIncidentManagerHeaderNewFromTask.tsx | 164 +++--
.../LineageTab/LineageTabContent.test.tsx | 17 -
.../LineageTab/LineageTabContent.tsx | 17 +-
.../ExploreSearchCard.test.tsx | 1 +
.../LearningDrawer.component.tsx | 88 ++-
.../LearningDrawer/learning-drawer.less | 75 ---
.../ArticleViewer.component.tsx | 36 +-
.../ResourcePlayer/ArticleViewer.test.tsx | 1 +
.../StorylaneTour.component.tsx | 4 +-
.../ResourcePlayer/StorylaneTour.test.tsx | 6 +-
.../ResourcePlayer/VideoPlayer.component.tsx | 4 +-
.../ResourcePlayer/VideoPlayer.test.tsx | 6 +-
.../ConfirmationModal.test.tsx | 20 +
.../ConfirmationModal/ConfirmationModal.tsx | 111 ++--
.../IngestionRunDetailsModal.test.tsx | 14 +-
.../IngestionRunDetailsModal.tsx | 116 ++--
.../KillIngestionPipelineModal.tsx | 15 +-
.../ModalWithMarkdownEditor.tsx | 114 ++--
.../modal-with-markdown-editor.less | 20 -
.../IngestionListTable.interface.ts | 6 +-
.../IngestionStatusCount.tsx | 27 +-
.../PipelineActions.interface.ts | 3 +-
.../PipelineActions/PipelineActions.tsx | 141 ++---
.../PipelineActions/pipeline-actions.less | 32 -
.../IngestionRecentRuns.component.tsx | 9 +-
.../SuggestionsAlert/SuggestionsAlert.tsx | 24 +-
.../SuggestionsAlert/suggestions-alert.less | 8 +-
.../SuggestionsSlider/SuggestionsSlider.tsx | 54 +-
.../TestDefinitionList.test.tsx | 8 +-
.../TestDefinitionTable.component.tsx | 42 +-
.../Chart/CustomPieChart.component.tsx | 11 +-
.../AirflowMessageBanner.tsx | 14 +-
.../AsyncSelectList.interface.ts | 15 +-
.../AsyncSelectList/AsyncSelectList.test.tsx | 33 +-
.../AsyncSelectList/AsyncSelectList.tsx | 299 ++++++----
.../AsyncSelectListSubmission.test.tsx | 46 +-
.../AsyncSelectList/async-select-list.less | 39 --
.../common/AvatarCarousel/AvatarCarousel.tsx | 35 +-
.../AvatarCarousel/avatar-carousel.less | 42 +-
.../AvatarCarouselItem/AvatarCarouselItem.tsx | 42 +-
.../src/components/common/Banner/Banner.tsx | 12 +-
.../CertificationTag/CertificationTag.tsx | 12 +-
.../DescriptionSourceBadge.test.tsx | 4 -
.../DescriptionSourceBadge.tsx | 9 +-
.../ManageButton/ManageButton.interface.ts | 5 +-
.../ManageButton/ManageButton.tsx | 37 +-
.../AssignErrorPlaceHolder.tsx | 7 +-
.../CreateErrorPlaceHolder.tsx | 32 +-
.../ErrorPlaceHolderIngestion.tsx | 20 +-
.../FilterErrorPlaceHolder.tsx | 7 +-
.../PermissionErrorPlaceholder.tsx | 13 +-
.../common/IconButtons/EditIconButton.tsx | 244 +++-----
.../common/InlineAlert/InlineAlert.test.tsx | 39 ++
.../common/InlineAlert/InlineAlert.tsx | 144 ++---
.../common/InlineAlert/inline-alert.less | 80 ---
.../InlineEdit/InlineEdit.component.tsx | 42 +-
.../common/InlineEdit/InlineEdit.interface.ts | 4 +-
.../common/InlineEdit/InlineEdit.test.tsx | 2 +-
.../common/InlineEdit/inline-edit.less | 6 -
.../common/ListView/ListView.interface.ts | 2 +-
.../CommonEntitySummaryInfoV1.test.tsx | 9 -
.../CommonEntitySummaryInfoV1.tsx | 8 +-
.../RichTextEditorPreviewNew.tsx | 7 +-
.../RichTextEditorPreviewerV1.tsx | 7 +-
.../rich-text-editor-previewerV1.less | 8 +-
.../SelectionCardGroup.test.tsx | 89 +++
.../SelectionCardGroup/SelectionCardGroup.tsx | 21 +-
.../selection-card-group.less | 145 +++--
.../ServiceDocPanel/ServiceDocPanel.tsx | 7 +-
.../LabelCountSkeleton.component.tsx | 15 +-
.../SummaryPanelSkeleton.component.tsx | 38 +-
.../common/Table/Table.interface.ts | 340 ++++++++++-
.../ConnectionStepCard.test.tsx | 74 +++
.../ConnectionStepCard/ConnectionStepCard.tsx | 107 ++--
.../connection-step-card.less | 13 -
.../components/common/TierCard/TierCard.tsx | 11 +-
.../CustomPropertiesDetailPage.test.tsx | 12 +-
.../Alerts/AlertAiFormFields.interface.ts | 8 +-
.../Alerts/AlertEditModal.component.test.tsx | 75 ++-
.../Alerts/AlertEditModal.component.tsx | 27 +-
.../AddNotificationPage.tsx | 2 +
.../AddObservabilityPage.interface.ts | 31 +-
.../AddObservabilityPage.test.tsx | 27 +
.../components/ObservabilityAlertForm.tsx | 8 +-
.../ObservabilityAlertFormFields.tsx | 6 +-
.../hooks/useObservabilityAlertForm.ts | 35 +-
.../ExplorePage/ExplorePage.interface.ts | 10 +-
.../IncidentManagerPage.test.tsx | 1 +
.../ObservabilityAlertsPage.test.tsx | 1 +
.../pages/TasksPage/shared/Assignees.test.tsx | 138 ++++-
.../src/pages/TasksPage/shared/Assignees.tsx | 289 ++++++---
.../pages/TasksPage/shared/TagSuggestion.tsx | 68 +--
.../shared/TaskPayloadSchemaFields.test.tsx | 29 +
.../shared/TaskPayloadSchemaFields.tsx | 203 ++++---
.../components/entity-version-time-line.less | 28 +-
.../resources/ui/src/utils/AlertsClassBase.ts | 8 +-
.../ui/src/utils/IngestionListTableUtils.tsx | 52 +-
190 files changed, 5105 insertions(+), 3734 deletions(-)
create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.test.tsx
create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.test.tsx
delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Learning/LearningDrawer/learning-drawer.less
delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithMarkdownEditor/modal-with-markdown-editor.less
delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/pipeline-actions.less
delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/async-select-list.less
delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/InlineAlert/inline-alert.less
create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/SelectionCardGroup/SelectionCardGroup.test.tsx
create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/ConnectionStepCard/ConnectionStepCard.test.tsx
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.theme.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.theme.test.tsx
index e1eca987805a..df82760f6eb0 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.theme.test.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.theme.test.tsx
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
-import { Dialog, Modal } from './slideout-menu';
+import { Dialog, Modal, SlideoutMenu } from './slideout-menu';
describe('SlideoutMenu theme roles', () => {
it('renders the drawer panel on the overlay surface with overlay elevation', () => {
@@ -17,4 +17,16 @@ describe('SlideoutMenu theme roles', () => {
// Modal wrapper owns the elevation shadow.
expect(panel.closest('.tw\\:shadow-overlay')).not.toBeNull();
});
+
+ it('stacks the overlay above page content that sets its own z-index', () => {
+ render(
+
+ Body
+
+ );
+
+ const overlay = screen.getByRole('dialog').closest('.tw\\:fixed');
+
+ expect(overlay).toHaveClass('tw:z-50');
+ });
});
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.tsx
index 241fa16043cd..ab4bffc92a38 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.tsx
@@ -27,7 +27,7 @@ export const ModalOverlay = (props: ModalOverlayProps) => {
{...props}
className={(state) =>
cx(
- 'tw:fixed tw:inset-0 tw:flex tw:min-h-dvh tw:w-full tw:items-center tw:justify-end tw:bg-overlay/70 tw:pl-6 tw:outline-hidden tw:ease-linear tw:md:pl-10',
+ 'tw:fixed tw:inset-0 tw:z-50 tw:flex tw:min-h-dvh tw:w-full tw:items-center tw:justify-end tw:bg-overlay/70 tw:pl-6 tw:outline-hidden tw:ease-linear tw:md:pl-10',
state.isEntering && 'tw:duration-300 tw:animate-in tw:fade-in',
state.isExiting && 'tw:duration-500 tw:animate-out tw:fade-out',
typeof props.className === 'function'
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts
index 1e11678060f0..ce90bb1ccfe8 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityAPI.spec.ts
@@ -368,7 +368,10 @@ test.describe(
.includes(`/api/v1/conversations/${firstActivityId}/replies/`) &&
response.request().method() === 'DELETE'
);
- await page.locator('.ant-modal').getByTestId('save-button').click();
+ await page
+ .getByTestId('confirmation-modal')
+ .getByTestId('save-button')
+ .click();
await deleteResponse;
await expect(editedReplyCard).toHaveCount(0);
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperationsTagsGlossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperationsTagsGlossary.spec.ts
index eaca35b48e1b..32e64e3eb90b 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperationsTagsGlossary.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperationsTagsGlossary.spec.ts
@@ -221,16 +221,16 @@ test.describe('Column Bulk Operations - Tags & Glossary Select in Drawer', () =>
await searchRes;
});
- await test.step('Option renders inside the drawer top layer', async () => {
- // Before the fix this option portaled to document.body, outside the
- // drawer — so scoping the locator to the drawer would not find it.
+ await test.step('Option renders above the drawer', async () => {
+ // The core listbox is a react-aria popover nested in the drawer's modal,
+ // so it portals to the body yet stays interactive above the drawer.
await expect(
- drawer.getByTestId(`tag-${CLASSIFICATION_TAG_FQN}`)
+ page.getByTestId(`tag-${CLASSIFICATION_TAG_FQN}`)
).toBeVisible();
});
await test.step('Clicking the option selects the tag', async () => {
- await drawer.getByTestId(`tag-${CLASSIFICATION_TAG_FQN}`).click();
+ await page.getByTestId(`tag-${CLASSIFICATION_TAG_FQN}`).click();
await expect(
tagsField.locator('[data-testid^="selected-tag-"]')
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts
index 29d5c186612d..45db3d001187 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts
@@ -750,8 +750,11 @@ test.describe(
await test.step('Reopen the incident as Acknowledged from the header', async () => {
await page.click('[data-testid="edit-resolution-icon"]');
- await page.click('[data-testid="test-case-resolution-status-type"]');
- await page.click('[title="Ack"]');
+ await page
+ .getByTestId('test-case-resolution-status-type')
+ .getByRole('button')
+ .click();
+ await page.getByRole('option', { name: 'Ack', exact: true }).click();
const reopenResponse = page.waitForResponse(
(response) =>
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts
index 68abdb66ab83..2581d2d35317 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts
@@ -107,6 +107,7 @@ const waitForIncidentTask = async (page: Page, testCaseFqn?: string) => {
category: 'Incident',
limit: 100,
fields: 'about,payload,assignees',
+ ...(testCaseFqn ? { aboutEntity: testCaseFqn } : {}),
},
});
@@ -175,7 +176,7 @@ const openIncidentReassignModal = async (page: Page, testCaseName?: string) => {
'[data-testid="edit-assignees"]:visible'
);
const reassignModal = page
- .locator('.ant-modal-wrap:visible')
+ .locator('[role="dialog"]:visible')
.filter({ hasText: /Re-?assign Task/i });
const reassignMenuItem = page
.locator('.task-action-dropdown:visible')
@@ -259,16 +260,18 @@ const reassignIncidentTask = async (
) => {
const reassignModal = await openIncidentReassignModal(page, testCaseName);
const assigneeSelect = reassignModal.getByTestId('select-assignee');
- const assigneeSelector = assigneeSelect.locator('.ant-select-selector');
const assigneeInput = assigneeSelect.getByRole('combobox');
const assigneeOption = page.getByTestId(assignee.name.toLowerCase());
- await expect(assigneeSelector).toBeVisible();
- await assigneeSelector.click();
+ await expect(assigneeInput).toBeVisible();
+ await assigneeInput.click();
await assigneeInput.fill(assignee.displayName);
await expect(assigneeOption).toBeVisible({ timeout: ACTION_TIMEOUT });
await assigneeOption.click();
+ // The open combobox popover aria-hides the rest of the dialog, Save included.
+ await reassignModal.locator('h2').click();
+ await expect(assigneeOption).toBeHidden();
const updateAssignee = waitForTaskResolveResponse(page);
@@ -303,8 +306,9 @@ const openIncidentResolveDialog = async (
const actionTrigger = page.locator(
'[data-testid="incident-task-action-trigger"]:visible, [data-testid="workflow-task-action-trigger"]:visible'
);
+ // Menu popovers are role=dialog too; data-trigger tells them from modals.
const resolveModal = page.locator(
- '.ant-modal-wrap:visible .ant-modal-content'
+ '[role="dialog"]:not([data-trigger]):visible'
);
const modalTextareas = resolveModal.locator('textarea');
@@ -743,7 +747,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
const resolveModal = await openIncidentResolveDialog(actorPage);
const resolveTextareas = resolveModal.locator('textarea');
const resolveReasonSelect = resolveModal
- .locator('.ant-select-selector')
+ .locator('button[aria-haspopup="listbox"]')
.first();
const textareaCount = await resolveTextareas.count();
@@ -1149,7 +1153,8 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
);
await page
.getByTestId('select-assignee')
- .getByLabel('close-circle')
+ .getByTestId('autocomplete-selected-item')
+ .getByRole('button')
.click();
await nonAssigneeFilterRes;
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks.spec.ts
index 5817987ba99b..365a6168a281 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks.spec.ts
@@ -156,9 +156,7 @@ test.describe('Task Workflow Tests', () => {
});
// Add suggested tags
- const tagsInput = page.locator(
- '[data-testid="tag-selector"] .ant-select-selection-search input'
- );
+ const tagsInput = page.getByTestId('tag-selector').getByRole('combobox');
if (await tagsInput.isVisible().catch(() => false)) {
await addTagSuggestion({
page,
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskAssigneeManagement.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskAssigneeManagement.spec.ts
index 7faea5f3ea15..c651ab0dca8d 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskAssigneeManagement.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskAssigneeManagement.spec.ts
@@ -99,9 +99,9 @@ test.describe('Task Assignee Management', () => {
await expect(page.getByTestId('select-assignee')).toBeVisible();
const existingAssigneeRemoveButton = page
- .locator(
- '[data-testid="select-assignee"] .ant-select-selection-item-remove'
- )
+ .getByTestId('select-assignee')
+ .getByTestId('autocomplete-selected-item')
+ .getByRole('button')
.first();
await existingAssigneeRemoveButton.click();
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskCreation.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskCreation.spec.ts
index b375f0f89b6e..252590993411 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskCreation.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskCreation.spec.ts
@@ -175,9 +175,9 @@ test.describe('Task Creation - Request Description', () => {
await expect(page.getByTestId('form-container')).toBeVisible();
// Assignee field - search and select user
- const assigneeInput = page.locator(
- '[data-testid="select-assignee"] .ant-select-selector input'
- );
+ const assigneeInput = page
+ .getByTestId('select-assignee')
+ .getByRole('combobox');
await assigneeInput.click();
// Search for user
@@ -422,9 +422,7 @@ test.describe('Task Creation - Suggest Tags', () => {
await expect(page.getByTestId('form-container')).toBeVisible();
// Add suggested tags using the tag selector
- const tagsInput = page.locator(
- '[data-testid="tag-selector"] .ant-select-selector input'
- );
+ const tagsInput = page.getByTestId('tag-selector').getByRole('combobox');
if (await tagsInput.isVisible()) {
await tagsInput.click();
@@ -436,8 +434,10 @@ test.describe('Task Creation - Suggest Tags', () => {
await expect(tagOption).toBeVisible();
await tagOption.click();
- // Close the dropdown by pressing Escape
- await page.keyboard.press('Escape');
+ // The multi-select schedules a re-open (~150ms) while its input still
+ // has focus, which would undo an Escape; blurring closes it for good.
+ await tagsInput.blur();
+ await expect(tagsInput).toHaveAttribute('aria-expanded', 'false');
}
// Submit - tag request pages use submit-tag-request
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskNavigation.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskNavigation.spec.ts
index bfd95bb79448..d4e1838a1814 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskNavigation.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskNavigation.spec.ts
@@ -276,7 +276,7 @@ test.describe('Task Navigation - Entity Page', () => {
await taskCard.click();
// Should open drawer with task details
- const drawer = page.locator('.ant-drawer-content');
+ const drawer = page.locator('.activity-feed-drawer');
if (await drawer.isVisible({ timeout: 5000 })) {
// Drawer should show task details
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts
index 409b7d2283ee..35fc9995de34 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts
@@ -11,7 +11,7 @@
* limitations under the License.
*/
import { expect, Locator, Page } from '@playwright/test';
-import { getDescriptionBox, waitForAntdModalToSettle } from './common';
+import { getDescriptionBox } from './common';
import { waitForAllLoadersToDisappear } from './entity';
import { waitForPageLoaded } from './polling';
import { TaskDetails } from './task';
@@ -47,9 +47,11 @@ export const checkDescriptionInEditModal = async (
await taskActionMenu.getByRole('menuitem', { name: 'edit' }).click();
- await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
+ const editTaskModal = page.getByTestId('suggestion-edit-task-modal');
- await expect(page.locator('.ant-modal-title')).toContainText(
+ await expect(editTaskModal).toBeVisible();
+
+ await expect(editTaskModal.getByRole('heading')).toContainText(
`Update description for table ${taskValue.term} columns/${taskValue.columnName}`
);
@@ -79,8 +81,7 @@ export const deleteFeedComments = async (page: Page, feed: Locator) => {
await page.locator('[data-testid="delete-message"]').click();
- await page.locator('[role="dialog"].ant-modal').waitFor();
- await waitForAntdModalToSettle(page);
+ await page.getByTestId('confirmation-modal').waitFor();
const deleteResponse = page.waitForResponse(
'/api/v1/conversations/*/replies/*'
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/addTestCaseList.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/addTestCaseList.ts
index 2b2ce260dedf..e2eb58917d1c 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/addTestCaseList.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/addTestCaseList.ts
@@ -22,6 +22,16 @@ export interface AddTestCaseListFilterContext {
waitAfterFilterUpdate: () => Promise;
}
+// The filters sit below the fold of the modal's scroll area. Playwright's
+// own scroll-before-click dispatches its scroll event after the popover has
+// opened, and react-aria closes a popover on any ancestor scroll. Scrolling
+// first lets that event flush before the click.
+async function openAddTestCaseListFilter(page: Page, searchKey: string) {
+ const trigger = page.getByTestId(`search-dropdown-${searchKey}`);
+ await trigger.scrollIntoViewIfNeeded();
+ await trigger.click();
+}
+
// The filter dropdown is a popover rendered outside the Add Test Cases modal
// and can close again right after it opens (timing-dependent). Reopen and
// retype until the searched option is shown instead of racing that close.
@@ -36,7 +46,7 @@ async function searchAddTestCaseListFilterOption(
await expect(async () => {
if (!(await menu.isVisible())) {
- await page.getByTestId(`search-dropdown-${searchKey}`).click();
+ await openAddTestCaseListFilter(page, searchKey);
}
await menu.getByTestId('search-input').fill(searchText, { timeout: 5_000 });
await expect(option).toBeVisible({ timeout: 10_000 });
@@ -52,7 +62,7 @@ export async function addTestCaseListFilterByTestType(
const listResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list*'
);
- await page.getByTestId('search-dropdown-Test Type').click();
+ await openAddTestCaseListFilter(page, 'Test Type');
await page
.getByTestId('drop-down-menu')
.getByRole('menuitemradio', { name: label })
@@ -68,7 +78,7 @@ export async function addTestCaseListFilterByStatus(
const listResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list*'
);
- await page.getByTestId('search-dropdown-Status').click();
+ await openAddTestCaseListFilter(page, 'Status');
await page
.getByTestId('drop-down-menu')
.getByRole('menuitemradio', { name: label })
@@ -138,7 +148,7 @@ export async function addTestCaseListResetFilters(
const clearTableResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list*'
);
- await page.getByTestId('search-dropdown-Table').click();
+ await openAddTestCaseListFilter(page, 'Table');
await page.getByTestId('drop-down-menu').getByTestId(tableFqn).click();
await page.getByTestId('drop-down-menu').getByTestId('update-btn').click();
await clearTableResponse;
@@ -146,7 +156,7 @@ export async function addTestCaseListResetFilters(
const clearColumnResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list*'
);
- await page.getByTestId('search-dropdown-Column').click();
+ await openAddTestCaseListFilter(page, 'Column');
await page.getByTestId('drop-down-menu').getByTestId(columnName).click();
await page.getByTestId('drop-down-menu').getByTestId('update-btn').click();
await clearColumnResponse;
@@ -154,7 +164,7 @@ export async function addTestCaseListResetFilters(
const clearStatusResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list*'
);
- await page.getByTestId('search-dropdown-Status').click();
+ await openAddTestCaseListFilter(page, 'Status');
await page
.getByTestId('drop-down-menu')
.getByRole('menuitemradio', { name: 'Success' })
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts
index 2a25de9343f8..d6bb099400a8 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts
@@ -588,14 +588,13 @@ export const selectExistingBundleSuite = async (
await dropdownInput.click();
await dropdownInput.fill(suiteName);
- // AddToBundleSuiteModal still renders an antd Select (not migrated to the
- // react-aria stack), so scope to the visible antd dropdown and its option
- // rows. A generic `[role="listbox"]` matches multiple listboxes on the page
- // (e.g. the header asset search) and resolves ambiguously.
- const dropdown = page.locator('.ant-select-dropdown:visible');
- const option = dropdown.locator('.ant-select-item-option', {
- hasText: suiteName,
- });
+ // Scope to the popover owned by this combobox: a generic listbox locator
+ // also matches other listboxes on the page (e.g. the header asset search).
+ await expect(dropdownInput).toHaveAttribute('aria-controls', /.+/);
+ const listboxId = (await dropdownInput.getAttribute('aria-controls')) ?? '';
+ const option = page
+ .locator(`[role="listbox"][id="${listboxId}"]`)
+ .getByRole('option', { name: suiteName });
await expect(option).toBeVisible();
await option.click();
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts
index 0118d3a0d6e8..b758fb19d03f 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts
@@ -103,7 +103,7 @@ export const confirmationDragAndDropTeam = async (
) => {
// Confirmation message before the transfer
await expect(
- page.locator('[data-testid="confirmation-modal"] .ant-modal-body')
+ page.getByTestId('confirmation-modal').getByTestId('body-text')
).toContainText(
`Click on Confirm if you’d like to move ${dragTeam} team under ${dropTeam} team.`
);
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts
index 346ad592747f..5ec5debab257 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts
@@ -1287,18 +1287,16 @@ export const createDescriptionTaskForGlossary = async (
);
if (isUndefined(value.assignee)) {
- await expect(
- page.locator('[data-testid="select-assignee"] > .ant-select-selector')
- ).toHaveText(value.assignee ?? '');
+ await expect(page.getByTestId('select-assignee')).toHaveText(
+ value.assignee ?? ''
+ );
await expect(
- page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector input'
- )
+ page.locator('[data-testid="select-assignee"] input')
).toBeDisabled();
} else {
const assigneeField = page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector #assignees'
+ '[data-testid="select-assignee"] #assignees'
);
await assigneeField.click();
@@ -1341,19 +1339,17 @@ export const createTagTaskForGlossary = async (
);
if (isUndefined(value.assignee)) {
- await expect(
- page.locator('[data-testid="select-assignee"] > .ant-select-selector')
- ).toHaveText(value.assignee ?? '');
+ await expect(page.getByTestId('select-assignee')).toHaveText(
+ value.assignee ?? ''
+ );
await expect(
- page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector input'
- )
+ page.locator('[data-testid="select-assignee"] input')
).toBeDisabled();
} else {
// select assignee
const assigneeField = page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector #assignees'
+ '[data-testid="select-assignee"] #assignees'
);
await assigneeField.click();
const userSearchResponse = page.waitForResponse(
@@ -1371,9 +1367,7 @@ export const createTagTaskForGlossary = async (
if (addTag) {
// select tags
- const suggestTags = page.locator(
- '[data-testid="tag-selector"] > .ant-select-selector .ant-select-selection-search-input'
- );
+ const suggestTags = page.locator('[data-testid="tag-selector"] input');
await suggestTags.click();
const querySearchResponse = page.waitForResponse(
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts
index be15c477bfd3..233fd9ab6c2d 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts
@@ -151,8 +151,11 @@ export const acknowledgeTask = async (data: {
await page.getByTestId(testCase).getByText(testCase).click();
await waitForAllLoadersToDisappear(page);
await page.click('[data-testid="edit-resolution-icon"]');
- await page.click('[data-testid="test-case-resolution-status-type"]');
- await page.click('[title="Ack"]');
+ await page
+ .getByTestId('test-case-resolution-status-type')
+ .getByRole('button')
+ .click();
+ await page.getByRole('option', { name: 'Ack', exact: true }).click();
const statusChangeResponse = waitForTaskResolveResponse(page);
await page.click('#update-status-button');
await statusChangeResponse;
@@ -187,10 +190,10 @@ export const addAssigneeFromPopoverWidget = async (data: {
await taskTabEditAssigneesButton.click();
await waitForAllLoadersToDisappear(page);
- const assigneeModal = page.locator('.ant-modal-content').last();
+ const assigneeModal = page.getByRole('dialog').last();
const assigneeSelect = assigneeModal.getByTestId('select-assignee');
- const assigneeSelector = assigneeSelect.locator('.ant-select-selector');
- const assigneeInput = assigneeSelect.locator('input').last();
+ const assigneeSelector = assigneeSelect.getByRole('combobox');
+ const assigneeInput = assigneeSelector;
const assigneeOption = page.getByTestId(user.name).first();
const normalizedAssigneeOption = page
.getByTestId(user.name.toLowerCase())
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts
index 2a7e1b68e83b..a9965bf56eb6 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts
@@ -117,18 +117,16 @@ export const createDescriptionTask = async (
);
if (isUndefined(value.assignee) || assigneeDisabled) {
- await expect(
- page.locator('[data-testid="select-assignee"] > .ant-select-selector')
- ).toHaveText(value.assignee);
+ await expect(page.getByTestId('select-assignee')).toHaveText(
+ value.assignee
+ );
await expect(
- page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector input'
- )
+ page.locator('[data-testid="select-assignee"] input')
).toBeDisabled();
} else {
const assigneeField = page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector #assignees'
+ '[data-testid="select-assignee"] #assignees'
);
await assigneeField.click();
await assigneeField.fill(value.assignee);
@@ -162,19 +160,17 @@ export const createTagTask = async (
);
if (isUndefined(value.assignee) || assigneeDisabled) {
- await expect(
- page.locator('[data-testid="select-assignee"] > .ant-select-selector')
- ).toHaveText(value.assignee);
+ await expect(page.getByTestId('select-assignee')).toHaveText(
+ value.assignee
+ );
await expect(
- page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector input'
- )
+ page.locator('[data-testid="select-assignee"] input')
).toBeDisabled();
} else {
// select assignee
const assigneeField = page.locator(
- '[data-testid="select-assignee"] > .ant-select-selector #assignees'
+ '[data-testid="select-assignee"] #assignees'
);
await assigneeField.click();
await assigneeField.fill(value.assignee);
@@ -189,9 +185,7 @@ export const createTagTask = async (
if (addTag) {
// select tags
- const suggestTags = page.locator(
- '[data-testid="tag-selector"] > .ant-select-selector .ant-select-selection-search-input'
- );
+ const suggestTags = page.locator('[data-testid="tag-selector"] input');
await suggestTags.click();
const querySearchResponse = page.waitForResponse(
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts
index 1c75883a602b..96f91712a58f 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/taskWorkflow.ts
@@ -41,7 +41,7 @@ export interface CreatedTask {
const TASK_TAB_SELECTOR = '[data-testid="task-tab"]';
const TASK_PANEL_SELECTOR = '#task-panel';
-const VISIBLE_TASK_MODAL_SELECTOR = '.ant-modal-wrap:visible';
+const VISIBLE_TASK_MODAL_SELECTOR = '[role="dialog"]:visible';
const logTaskDebug = (...messages: Array) => {
if (process.env.PW_TASK_DEBUG) {
@@ -64,7 +64,7 @@ const selectTagSuggestion = async ({
tagTestId: string;
}) => {
const tagSelector = root.locator('[data-testid="tag-selector"]');
- const tagsInput = tagSelector.locator('.ant-select-selection-search-input');
+ const tagsInput = tagSelector.getByRole('combobox');
const tagOption = page.getByTestId(tagTestId);
const tagSearchResponse = page
.waitForResponse(
@@ -93,7 +93,11 @@ const selectTagSuggestion = async ({
await expect(tagOption).toBeVisible({ timeout: 5000 });
logTaskDebug('selectTagSuggestion:optionVisible', tagTestId);
await tagOption.click();
- await page.keyboard.press('Escape');
+ // The multi-select schedules a re-open (~150ms) while its input still has
+ // focus, which would undo an Escape sent right after the pick. Blurring the
+ // input closes the menu and cancels that re-open for good.
+ await tagsInput.blur();
+ await expect(tagsInput).toHaveAttribute('aria-expanded', 'false');
logTaskDebug('selectTagSuggestion:done', tagTestId);
};
@@ -214,9 +218,9 @@ export const openTaskForm = async (page: Page, route: string) => {
};
export const selectAssignee = async (page: Page, assigneeName: string) => {
- const assigneeInput = page.locator(
- '[data-testid="select-assignee"] .ant-select-selection-search input'
- );
+ const assigneeInput = page
+ .getByTestId('select-assignee')
+ .getByRole('combobox');
const assigneeOption = page.getByTestId(assigneeName);
const assigneeSearchResponse = page
.waitForResponse(
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.test.tsx
index c4f2927e44d3..68798cd3f392 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.test.tsx
@@ -234,14 +234,14 @@ describe('ActivityFeedCardNew', () => {
);
// The hover reveal in activity-feed-actions.less is written as
- // `.activity-feed-card-new:hover > .ant-card-body > .feed-actions`. antd's
- // Card puts its children inside .ant-card-body, so that middle step is
- // load-bearing: drop it and the selector stops matching, leaving the
- // reply/resolve/edit/delete bar hidden from mouse users.
+ // `.activity-feed-card-new:hover > .activity-feed-card-new-body >
+ // .feed-actions`, so that middle step is load-bearing: drop it and the
+ // selector stops matching, leaving the reply/resolve/edit/delete bar
+ // hidden from mouse users.
const actions = screen.getByTestId('feed-actions');
const body = actions.parentElement;
- expect(body).toHaveClass('ant-card-body');
+ expect(body).toHaveClass('activity-feed-card-new-body');
expect(body?.parentElement).toHaveClass('activity-feed-card-new');
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx
index 700bb000f367..b71b52c245b9 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx
@@ -17,7 +17,6 @@ import {
Tooltip,
Typography,
} from '@openmetadata/ui-core-components';
-import { Card, Space } from 'antd';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined, orderBy } from 'lodash';
@@ -64,6 +63,11 @@ const ActivityFeedEditorNew = withSuspenseFallback(
const POST_SKELETON_KEYS = ['first', 'second', 'third'];
+// Each child of the antd Space this markup replaced sat in its own block
+// wrapper, which sets the line box the header text and avatars align in;
+// keep that wrapper, and hide it when empty as antd's did.
+const SPACE_ITEM_CLASS_NAME = 'tw:empty:hidden';
+
interface ActivityFeedCardNewProps {
feed?: Conversation;
activity?: ActivityEvent;
@@ -100,11 +104,11 @@ const getHeaderTagsClassName = (
showThread: boolean | undefined,
entityRefType?: string
): string =>
- classNames('d-flex gap-1', {
+ classNames('d-flex', {
'header-container-card': !showThread,
'flex-wrap': showThread && entityRefType !== EntityType.CONTAINER,
'items-start': showThread && entityRefType === EntityType.CONTAINER,
- ' items-center': showThread && entityRefType !== EntityType.CONTAINER,
+ 'items-center': !showThread || entityRefType !== EntityType.CONTAINER,
});
const computeFeedId = (feed?: Conversation, activity?: ActivityEvent): string =>
@@ -398,10 +402,26 @@ const ActivityFeedCardNew = ({
return isPost ? post?.message ?? '' : feed?.message ?? '';
}, [isActivityEvent, activity, isPost, feed, post]);
+ const renderHeaderTags = () => (
+
+
+
+ {feedHeaderText}
+
+
+ {renderEntityLink}
+
+ );
+
const renderWidgetCard = () => (
-
-
+
{timestamp}
- {!isPost && (
-
-
- {feedHeaderText}
-
-
- {renderEntityLink}
-
- )}
+ {!isPost && renderHeaderTags()}
-
- {feedActions}
-
+ {feedActions}
+
+
);
if (isFeedWidget) {
@@ -569,10 +574,13 @@ const ActivityFeedCardNew = ({
);
+ const isContainerThread =
+ showThread && entityRef?.type === EntityType.CONTAINER;
+
const renderFullCard = () => (
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
- {getEntityName(user)}
-
-
-
- {timestamp}
-
- {!isPost && (
-
-
- {feedHeaderText}
-
-
- {renderEntityLink}
-
- )}
-
-
-
- setIsEditPost(false)}
- onUpdate={onUpdate}
- />
+
+
+
+
+
+
+
+
+ {getEntityName(user)}
+
+
+
+
+
{timestamp}
+
+
+ {!isPost && (
+
+ {renderHeaderTags()}
+
+ )}
+
+
+
+
- {!isActivityEvent && feed && (
-
+ setIsEditPost(false)}
+ onUpdate={onUpdate}
/>
+
+
+ {!isActivityEvent && feed && (
+
+
+
)}
{isActivityEvent && activity && (
-
+
)}
-
-
- {(showThread || isOpenInDrawer) && renderCommentsSection()}
- {feedActions}
-
+
+ {(showThread || isOpenInDrawer) && renderCommentsSection()}
+ {feedActions}
+
+
);
return renderFullCard();
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/DescriptionFeed/ActivityDescriptionFeed.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/DescriptionFeed/ActivityDescriptionFeed.tsx
index f7996ce9a32c..7070043e19f9 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/DescriptionFeed/ActivityDescriptionFeed.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/DescriptionFeed/ActivityDescriptionFeed.tsx
@@ -11,7 +11,7 @@
* limitations under the License.
*/
-import { Col, Row } from 'antd';
+import { Box } from '@openmetadata/ui-core-components';
import { useMemo } from 'react';
import { ReactComponent as AddIcon } from '../../../../../assets/svg/added-icon.svg';
import { ReactComponent as UpdatedIcon } from '../../../../../assets/svg/updated-icon.svg';
@@ -44,15 +44,15 @@ function ActivityDescriptionFeed({
}, [isAdded]);
return (
-
- {operationIcon}
-
+
+ {operationIcon}
+
-
-
+
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/OwnerFeed/ActivityOwnersFeed.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/OwnerFeed/ActivityOwnersFeed.tsx
index 419bb071b8ee..4426aa20c534 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/OwnerFeed/ActivityOwnersFeed.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/OwnerFeed/ActivityOwnersFeed.tsx
@@ -11,8 +11,12 @@
* limitations under the License.
*/
-import { Owner, OwnerChip, Typography } from '@openmetadata/ui-core-components';
-import { Col, Row } from 'antd';
+import {
+ Box,
+ Owner,
+ OwnerChip,
+ Typography,
+} from '@openmetadata/ui-core-components';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { useCallback, useMemo } from 'react';
@@ -96,7 +100,7 @@ function ActivityOwnersFeed({
const getOwnerItems = useCallback(
(ownerList: EntityReference[]) => {
return ownerList.length <= maxVisibleOwners ? (
-
+
{ownerList.map((owner: EntityReference) =>
owner.type === EntityType.USER ? (
@@ -129,7 +133,7 @@ function ActivityOwnersFeed({
)
)}
-
+
) : (
+
{!isEmpty(updatedOwner) && (
-
-
-
-
+
+
+
+
+ {t('label.owner-plural-with-colon')}
+
+
+
+ {renderUpdatedOwner}
+
+ )}
+ {!isEmpty(previousOwner) && (
+
+
+
+
{t('label.owner-plural-with-colon')}
-
-
- {renderUpdatedOwner}
-
-
- )}
- {!isEmpty(previousOwner) && (
-
-
-
-
-
-
- {t('label.owner-plural-with-colon')}
-
-
-
- {renderPreviousOwner}
-
-
+
+
+ {renderPreviousOwner}
+
)}
-
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/TagsFeed/ActivityTagsFeed.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/TagsFeed/ActivityTagsFeed.tsx
index 5a4aebdbcc8f..ab202ab8e6b5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/TagsFeed/ActivityTagsFeed.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardBody/TagsFeed/ActivityTagsFeed.tsx
@@ -11,7 +11,7 @@
* limitations under the License.
*/
-import { Col, Row } from 'antd';
+import { Box } from '@openmetadata/ui-core-components';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { ReactComponent as AddIcon } from '../../../../../assets/svg/added-icon.svg';
@@ -60,32 +60,28 @@ function ActivityTagsFeed({ activity }: Readonly) {
}, [activity.oldValue, activity.newValue]);
return (
-
+
{!isEmpty(updatedTags) && (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
)}
{!isEmpty(previousTags) && (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)}
-
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.test.tsx
index 229d4a7ee582..e38230ba8842 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.test.tsx
@@ -159,6 +159,27 @@ describe('ActivityEventFooter', () => {
expect(mockOnActivityClick).not.toHaveBeenCalled();
});
+ it('lets the click reach the card container behind the footer', () => {
+ const activity = createMockActivity();
+ const mockOnActivityClick = jest.fn();
+ const mockContainerClick = jest.fn();
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByTestId('comment-button'));
+
+ expect(mockOnActivityClick).toHaveBeenCalledWith(activity);
+ expect(mockContainerClick).toHaveBeenCalled();
+ });
+
it('should not throw when onActivityClick is not provided', () => {
const activity = createMockActivity();
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.tsx
index 222424df6754..10fb3f742ebe 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/ActivityEventFooter.tsx
@@ -11,10 +11,12 @@
* limitations under the License.
*/
-import { Button, Col, Row } from 'antd';
+import { Box, Button } from '@openmetadata/ui-core-components';
import classNames from 'classnames';
import { noop } from 'lodash';
import { useCallback } from 'react';
+import { PressEvent } from 'react-aria-components';
+import { useTranslation } from 'react-i18next';
import { ReactComponent as ThreadIcon } from '../../../../assets/svg/ic-reply-2.svg';
import { ReactionOperation } from '../../../../enums/reactions.enum';
import { ActivityEvent } from '../../../../generated/entity/activity/activityEvent';
@@ -33,6 +35,7 @@ function ActivityEventFooter({
isForFeedTab = false,
onActivityClick,
}: Readonly) {
+ const { t } = useTranslation();
const { updateActivityReaction } = useActivityFeedProvider();
const onReactionUpdate = useCallback(
@@ -45,30 +48,41 @@ function ActivityEventFooter({
[updateActivityReaction, activity.id]
);
- const handleCommentClick = useCallback(() => {
- onActivityClick?.(activity);
- }, [onActivityClick, activity]);
+ // The press must keep bubbling: the card container behind this footer
+ // selects the activity on click, as it did when this was a native button.
+ const handleCommentPress = useCallback(
+ (event: PressEvent) => {
+ event.continuePropagation();
+ if (isForFeedTab) {
+ onActivityClick?.(activity);
+ }
+ },
+ [isForFeedTab, onActivityClick, activity]
+ );
return (
-
-
+
+
+ iconLeading={
+
+ }
+ onPress={handleCommentPress}
+ />
-
-
+
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/FeedCardFooterNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/FeedCardFooterNew.tsx
index d607f2b0a73c..863553445351 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/FeedCardFooterNew.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardV2/FeedCardFooter/FeedCardFooterNew.tsx
@@ -11,11 +11,12 @@
* limitations under the License.
*/
-import { AvatarStack } from '@openmetadata/ui-core-components';
-import { Button, Col, Row } from 'antd';
+import { AvatarStack, Box, Button } from '@openmetadata/ui-core-components';
import classNames from 'classnames';
import { noop } from 'lodash';
import { useCallback, useMemo } from 'react';
+import { PressEvent } from 'react-aria-components';
+import { useTranslation } from 'react-i18next';
import { ReactComponent as ThreadIcon } from '../../../../assets/svg/ic-reply-2.svg';
import { ReactionOperation } from '../../../../enums/reactions.enum';
import { ReactionType } from '../../../../generated/type/reaction';
@@ -35,6 +36,7 @@ function FeedCardFooterNew({
isReply = false,
isForFeedTab = false,
}: Readonly) {
+ const { t } = useTranslation();
const { showDrawer, updateReactions } = useActivityFeedProvider();
const postLength = useMemo(
@@ -76,12 +78,24 @@ function FeedCardFooterNew({
}
}, [showDrawer, conversation]);
+ // The press must keep bubbling: the card container behind this footer
+ // selects the conversation on click, as it did when these were native
+ // buttons.
+ const handleRepliesPress = useCallback(
+ (event: PressEvent) => {
+ event.continuePropagation();
+ if (isForFeedTab) {
+ showReplies();
+ }
+ },
+ [isForFeedTab, showReplies]
+ );
+
return (
-
-
+
+
{postLength > 0 && !isReply && (
@@ -89,10 +103,11 @@ function FeedCardFooterNew({
avatarSize={AVATAR_SIZE}
items={repliedUsers.map((user) => (
-
-
+
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.test.tsx
new file mode 100644
index 000000000000..8c9c7afa4981
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.test.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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 ActivityFeedDrawer from './ActivityFeedDrawer';
+
+const mockHideDrawer = jest.fn();
+const mockProvider = jest.fn();
+
+jest.mock('../ActivityFeedProvider/ActivityFeedProvider', () => ({
+ useActivityFeedProvider: () => mockProvider(),
+}));
+
+jest.mock('../ActivityFeedPanel/FeedPanelHeader', () =>
+ jest.fn(({ onCancel }) => (
+
+ header
+
+ ))
+);
+
+jest.mock('../ActivityFeedPanel/FeedPanelBodyV1', () =>
+ jest.fn(() => )
+);
+
+jest.mock('../ActivityFeedPanel/ActivityPanelHeader', () =>
+ jest.fn(() => )
+);
+
+jest.mock('../ActivityFeedPanel/ActivityPanelBody', () =>
+ jest.fn(() => )
+);
+
+jest.mock('../ActivityFeedPanel/TaskPanelHeader', () =>
+ jest.fn(() => )
+);
+
+jest.mock('../../Entity/Task/TaskTab/TaskTabNew.component', () => ({
+ TaskTabNew: jest.fn(() => ),
+}));
+
+const thread = { id: 'thread-1', about: '<#E::table::a.b>' };
+
+describe('ActivityFeedDrawer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockProvider.mockReturnValue({
+ hideDrawer: mockHideDrawer,
+ selectedThread: thread,
+ });
+ });
+
+ it('renders nothing without a selected thread, task or activity', () => {
+ mockProvider.mockReturnValue({ hideDrawer: mockHideDrawer });
+
+ render();
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+
+ it('renders the thread header and body in the drawer panel', () => {
+ render();
+
+ const dialog = screen.getByRole('dialog');
+
+ expect(dialog).toHaveClass('activity-feed-drawer');
+ expect(screen.getByTestId('closeDrawer')).toBeInTheDocument();
+ expect(dialog.querySelector('#feed-panel')).toContainElement(
+ screen.getByTestId('feed-panel-body')
+ );
+ });
+
+ it('renders the activity panel when an activity is selected', () => {
+ mockProvider.mockReturnValue({
+ hideDrawer: mockHideDrawer,
+ selectedActivity: { id: 'activity-1' },
+ });
+
+ render();
+
+ expect(screen.getByTestId('activity-panel-header')).toBeInTheDocument();
+ expect(screen.getByTestId('activity-panel-body')).toBeInTheDocument();
+ });
+
+ it('renders the task panel when a task is selected', async () => {
+ mockProvider.mockReturnValue({
+ hideDrawer: mockHideDrawer,
+ selectedTask: { id: 'task-1', about: { type: 'table' } },
+ });
+
+ render();
+
+ expect(screen.getByTestId('task-panel-header')).toBeInTheDocument();
+ expect(await screen.findByTestId('task-tab')).toBeInTheDocument();
+ });
+
+ it('hides the drawer on Escape', () => {
+ render();
+
+ fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
+
+ expect(mockHideDrawer).toHaveBeenCalled();
+ });
+
+ it('hides the drawer from the header close action', () => {
+ render();
+
+ fireEvent.click(screen.getByTestId('closeDrawer'));
+
+ expect(mockHideDrawer).toHaveBeenCalled();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx
index 8d847c79c2e0..4ba3559ba595 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx
@@ -11,9 +11,10 @@
* limitations under the License.
*/
-import { Col, Drawer, Row } from 'antd';
+import { SlideoutMenu } from '@openmetadata/ui-core-components';
import classNames from 'classnames';
-import { FC, lazy, useMemo } from 'react';
+import { FC, lazy, ReactNode, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
import { EntityType } from '../../../enums/entity.enum';
import withSuspenseFallback from '../../AppRouter/withSuspenseFallback';
import ActivityPanelBody from '../ActivityFeedPanel/ActivityPanelBody';
@@ -25,6 +26,7 @@ import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedPro
import './activity-feed-drawer.less';
const ACTIVITY_FEED_DRAWER_CLASS = 'activity-feed-drawer';
+const DRAWER_WIDTH = 576;
const TaskTabNew = withSuspenseFallback(
lazy(() =>
@@ -43,6 +45,7 @@ const ActivityFeedDrawer: FC = ({
open,
className,
}) => {
+ const { t } = useTranslation();
const { hideDrawer, selectedThread, selectedTask, selectedActivity } =
useActivityFeedProvider();
@@ -54,93 +57,85 @@ const ActivityFeedDrawer: FC = ({
return EntityType.TABLE;
}, [selectedTask]);
- if (!selectedThread && !selectedTask && !selectedActivity) {
- return null;
- }
-
- if (selectedTask) {
- return (
- {
+ if (selectedTask) {
+ return {
+ header: (
- }
- width={576}
- onClose={hideDrawer}>
-
-
-
-
-
-
- );
- }
+ ),
+ body: (
+
+ ),
+ };
+ }
- if (selectedActivity) {
- return (
-
- }
- width={576}
- onClose={hideDrawer}>
-
-
-
-
-
-
- );
- }
-
- if (!selectedThread) {
- return null;
- }
+ ),
+ body: ,
+ };
+ }
- return (
-
- }
- width={576}
- onClose={hideDrawer}>
-
-
+ if (selectedThread) {
+ return {
+ header: (
+
+ ),
+ body: (
-
-
-
+ ),
+ };
+ }
+
+ return null;
+ }, [selectedTask, selectedActivity, selectedThread, entityType, hideDrawer]);
+
+ if (!panel) {
+ return null;
+ }
+
+ return (
+ !isOpen && hideDrawer()}>
+ {panel.header}
+
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/activity-feed-drawer.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/activity-feed-drawer.less
index 65a4fcb4ef3c..4c547cdbacb3 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/activity-feed-drawer.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/activity-feed-drawer.less
@@ -20,11 +20,20 @@
color: @primary-heading-color;
}
}
+// The header and body chrome antd's Drawer used to supply (its title
+// typography, a scrolling body), less the padding this drawer always removed.
.activity-feed-drawer {
- .ant-drawer-body {
- padding: 0;
+ .activity-feed-drawer-header {
+ flex-shrink: 0;
+ font-size: var(--om-font-size-md);
+ font-weight: var(--om-font-weight-medium);
+ line-height: 22px;
+ color: @heading-color;
}
- .ant-drawer-header {
- padding: 0;
+
+ .activity-feed-drawer-body {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
}
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/SendButton.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/SendButton.tsx
index 714347625fa9..756384d321d4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/SendButton.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/SendButton.tsx
@@ -11,10 +11,10 @@
* limitations under the License.
*/
-import Icon from '@ant-design/icons/lib/components/Icon';
-import { Button } from 'antd';
+import { ButtonUtility } from '@openmetadata/ui-core-components';
import classNames from 'classnames';
import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
import { ReactComponent as IconPaperPlanePrimary } from '../../../assets/svg/paper-plane-fill.svg';
import './send-button.less';
@@ -28,13 +28,19 @@ export const SendButton: FC = ({
editorValue,
className,
onSaveHandler,
-}) => (
- }
- type="text"
- onClick={onSaveHandler}
- />
-);
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/send-button.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/send-button.less
index 89470e04f9e5..a419887ebfaa 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/send-button.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/send-button.less
@@ -18,7 +18,7 @@
color: var(--tw-color-text-white);
}
-.ant-btn.send-button {
+button.send-button {
position: absolute;
bottom: 8px;
right: 8px;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelHeader.tsx
index 1a20cdace941..f58a5d236b12 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelHeader.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelHeader.tsx
@@ -11,8 +11,8 @@
* limitations under the License.
*/
-import { PlusOutlined } from '@ant-design/icons';
-import { Button, Tooltip } from 'antd';
+import { Button } from '@openmetadata/ui-core-components';
+import { Plus } from '@openmetadata/ui-core-components/icons';
import classNames from 'classnames';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
@@ -71,22 +71,22 @@ const FeedPanelHeader: FC = ({
{onShowNewConversation ? (
-
- }
- size="small"
- type="primary"
- onClick={() => {
- onShowNewConversation(true);
- }}
- />
-
+ color="primary"
+ data-testid="add-new-conversation"
+ iconLeading={Plus}
+ size="sm"
+ tooltip={t('label.start-entity', {
+ entity: t('label.conversation-lowercase'),
+ })}
+ tooltipPlacement="bottom"
+ onPress={() => {
+ onShowNewConversation(true);
+ }}
+ />
) : null}
{hideCloseIcon ? null : (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/TaskPanelHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/TaskPanelHeader.tsx
index e85654548f37..c54755bbe831 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/TaskPanelHeader.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/TaskPanelHeader.tsx
@@ -11,9 +11,12 @@
* limitations under the License.
*/
-import { CloseOutlined } from '@ant-design/icons';
-import { Typography } from '@openmetadata/ui-core-components';
-import { Button, Space } from 'antd';
+import {
+ Box,
+ ButtonUtility,
+ Typography,
+} from '@openmetadata/ui-core-components';
+import { XClose } from '@openmetadata/ui-core-components/icons';
import classNames from 'classnames';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
@@ -42,8 +45,8 @@ const TaskPanelHeader: FC
= ({
return (
-
-
+
+
{`#${getTaskDisplayId(task.taskId)} `}
{t(taskTypeLabel)}
@@ -53,8 +56,8 @@ const TaskPanelHeader: FC = ({
{getEntityName(task.about)}
)}
-
-
+
+
= ({
{task.status}
{onCancel && (
- }
- type="text"
+ icon={XClose}
+ size="xs"
onClick={onCancel}
/>
)}
-
-
+
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-body-v1.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-body-v1.less
index 197438a46e6b..2d0c1e164246 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-body-v1.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-body-v1.less
@@ -21,20 +21,16 @@
flex-wrap: wrap;
width: inherit;
- &.ant-card {
+ &.activity-feed-card-new-surface {
background: @grey-9;
border-radius: var(--om-radius-xl);
margin-bottom: var(--om-space-20);
border: 0.5px solid @grey-15;
}
- .ant-card-body {
+ .activity-feed-card-new-body {
width: inherit;
padding: var(--om-space-20);
-
- .ant-space-item {
- width: inherit;
- }
}
.header-link {
word-wrap: break-word;
@@ -55,9 +51,9 @@
.activity-feed-card-new-right-panel {
// Surface (== bg-primary in light) so the thread blends into the
// bg-surface right panel / drawer instead of a black slab in dark.
- &.ant-card {
+ &.activity-feed-card-new-surface {
border: 1px solid var(--om-color-bg-surface);
- .ant-card-body {
+ .activity-feed-card-new-body {
background-color: var(--om-color-bg-surface);
border: 1px solid var(--om-color-bg-surface);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-header.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-header.less
index b16d75c3a830..1fcd3cac488e 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-header.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/feed-panel-header.less
@@ -14,7 +14,13 @@
.feed-panel-header {
padding: var(--om-space-16);
border-bottom: 1px solid var(--om-legacy-color-e8e8e8);
- background-color: var(--om-color-gray-50);
+ // bg-secondary is the same gray-50 in light, and flips in dark where the
+ // drawer surface does.
+ background-color: var(--om-color-bg-secondary);
+
+ .dark-mode & {
+ border-bottom-color: var(--om-color-border-secondary);
+ }
.task-status-badge {
padding: var(--om-space-2) var(--om-space-8);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less
index 9cb86a68150a..227a653c28e1 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less
@@ -126,13 +126,9 @@
width: inherit;
border: 0.5px solid @grey-15;
- .ant-card-body {
+ .activity-feed-card-new-body {
width: inherit;
padding: var(--om-space-20);
-
- .ant-space-item {
- width: inherit;
- }
}
}
@@ -178,7 +174,7 @@
flex-wrap: wrap;
width: inherit;
- .ant-card-body {
+ .activity-feed-card-new-body {
width: inherit;
padding: 0;
// margin: 10px 0px 20px 0px;
@@ -187,10 +183,6 @@
background: var(--om-legacy-color-239-244-250-0-25);
padding-bottom: var(--om-space-16);
-
- .ant-space-item {
- width: inherit;
- }
}
.activity-feed-reply-card-message {
@@ -235,6 +227,33 @@
}
}
+// The box antd's Card used to give the feed card. :where() keeps it at zero
+// specificity so every card rule in these files overrides it, as they did
+// antd's own .ant-card.
+:where(.activity-feed-card-new-surface) {
+ position: relative;
+ color: @text-color;
+ background: var(--om-color-bg-surface);
+ border: 1px solid @border-color;
+}
+
+// The landing-page feed widget lays out its own card body through a descendant
+// `.ant-card-body` rule (feed-widget.less, widget-wrapper.less) that also
+// reached this card's body while it was an antd Card. Mirror it so widget
+// cards keep their compact, unpadded layout.
+.feed-widget-container,
+.widget-wrapper-container {
+ .activity-feed-card-new .activity-feed-card-new-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ height: 100%;
+ padding: 0;
+ overflow: hidden;
+ position: relative;
+ }
+}
+
.activity-feed-task {
background-color: @grey-1;
}
@@ -277,7 +296,7 @@
.activity-feed-editor-send-btn {
color: var(--tw-color-text-white);
- .ant-btn.send-button {
+ .send-button {
position: absolute;
bottom: 8px;
right: 8px;
@@ -450,7 +469,7 @@
// normal flow is what stops the bar overlapping the comment text - it can no
// longer be mispositioned relative to a padding or line-height it doesn't know
// about.
-.reply-card .feed-actions.ant-space {
+.reply-card .feed-actions {
position: static;
padding-top: var(--om-space-2);
padding-bottom: var(--om-space-2);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.test.tsx
index 566497154054..f2c0751f111d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.test.tsx
@@ -117,6 +117,22 @@ describe('ActivityFeedActions', () => {
expect(screen.queryByTestId('delete-message')).not.toBeInTheDocument();
});
+ it('renders no action bar when the user has no action on the post', () => {
+ mockUseApplicationStore.mockReturnValue({
+ currentUser: { id: 'other-id', name: 'bob', isAdmin: false },
+ });
+
+ render(
+
+ );
+
+ expect(screen.queryByTestId('feed-actions')).not.toBeInTheDocument();
+ });
+
it('allows an administrator to edit and delete another users reply', () => {
mockUseApplicationStore.mockReturnValue({
currentUser: { id: 'admin-id', name: 'admin', isAdmin: true },
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.tsx
index d7bebb7dd89b..757e69010e7b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/ActivityFeedActions.tsx
@@ -10,8 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ButtonUtility } from '@openmetadata/ui-core-components';
-import { Space } from 'antd';
+import { Box, ButtonUtility } from '@openmetadata/ui-core-components';
import classNames from 'classnames';
import { lazy, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -83,15 +82,22 @@ interface ActivityFeedActionsProps {
const resolveActionVisibility = (
isAuthor: boolean,
isAdmin: boolean,
+ canReply: boolean,
canEdit?: boolean,
canDelete?: boolean
) => {
const canManage = isAuthor || isAdmin;
+ const showDelete = canDelete ?? canManage;
+ const showEdit = canEdit ?? canManage;
return {
- canManage,
- showDelete: canDelete ?? canManage,
- showEdit: canEdit ?? canManage,
+ showReply: canReply,
+ showResolve: canReply && canManage,
+ showDelete,
+ showEdit,
+ // antd's Space rendered nothing without children; keep an empty, bordered
+ // bar from appearing on hover when the user may not act on this post.
+ hasActions: canReply || showEdit || showDelete,
};
};
@@ -176,12 +182,14 @@ const ActivityFeedActions = ({
}
};
- const { canManage, showEdit, showDelete } = resolveActionVisibility(
- isAuthor,
- Boolean(currentUser?.isAdmin),
- canEdit,
- canDelete
- );
+ const { showReply, showResolve, showEdit, showDelete, hasActions } =
+ resolveActionVisibility(
+ isAuthor,
+ Boolean(currentUser?.isAdmin),
+ !isReply && Boolean(conversation),
+ canEdit,
+ canDelete
+ );
const handleResolvedChange = () => {
if (!conversation || isReply || !conversationId) {
@@ -198,63 +206,66 @@ const ActivityFeedActions = ({
return (
<>
-
- {!isReply && conversation && (
-
- )}
+ {hasActions && (
+
+ {showReply && (
+
+ )}
- {!isReply && conversation && canManage && (
-
- )}
+ {showResolve && (
+
+ )}
- {showEdit && (
-
- )}
+ {showEdit && (
+
+ )}
- {showDelete && (
- setShowDeleteDialog(true)}
- />
- )}
-
+ {showDelete && (
+ setShowDeleteDialog(true)}
+ />
+ )}
+
+ )}
.ant-card-body > .feed-actions.ant-space,
-.reply-card:hover .feed-actions.ant-space {
+// The conversation's bar sits inside the card's body wrapper, so it is a
+// grandchild of the card, not a child. A test in
+// ActivityFeedcardNew.component.test.tsx pins that shape so a markup change
+// fails loudly rather than silently leaving these hidden.
+.activity-feed-card-new:hover > .activity-feed-card-new-body > .feed-actions,
+.reply-card:hover .feed-actions {
opacity: 1;
pointer-events: auto;
}
-.feed-actions.ant-space {
+.feed-actions {
position: absolute;
right: 10px;
top: 24px;
@@ -55,7 +55,7 @@
color: @text-color;
}
-.feed-actions.ant-space[dir='rtl'] {
+.feed-actions[dir='rtl'] {
right: auto;
left: 10px;
}
@@ -72,7 +72,7 @@
// buttons. Written here rather than passed as a class because a Tailwind
// utility would tie on specificity with p-1.5 and win or lose on stylesheet
// order.
-.feed-actions.ant-space .toolbar-button {
+.feed-actions .toolbar-button {
padding: var(--om-space-2);
// ButtonUtility's `tertiary` colour paints the glyph tw:text-quaternary, a
// mid grey. Fall back to the bar's own text colour, which is what these
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts
index 2c9fd644b531..bd6f1ed85850 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts
@@ -94,6 +94,7 @@ export interface BlockEditorAttachmentProps {
}
export interface EditorSlotsProps {
+ editable?: boolean;
editor: Editor | null;
menuType: MenuType;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
index 983e17605c6d..ec480baa198d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.tsx
@@ -307,7 +307,12 @@ const BlockEditor = forwardRef(
placeholder={placeholder}
onMouseDown={editorSlots.current?.onMouseDown}
/>
-
+
);
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.test.tsx
index b73b7a52b8bd..21e1715de8f8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.test.tsx
@@ -139,9 +139,11 @@ describe('BubbleMenu', () => {
render();
const bold = screen.getByLabelText('Bold');
- fireEvent.mouseDown(bold);
+ const isNotPrevented = fireEvent.mouseDown(bold);
expect(mockToggleBold).toHaveBeenCalled();
+ // default prevented so the editor keeps focus and its selection
+ expect(isNotPrevented).toBe(false);
});
it('should call toggleItalic when clicking on Italic', async () => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx
index 0aa1e0380b5c..546a35a68bf0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx
@@ -10,16 +10,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Typography } from '@openmetadata/ui-core-components';
+import { ButtonUtility, Typography } from '@openmetadata/ui-core-components';
import { isNodeSelection, type Editor } from '@tiptap/core';
import {
BubbleMenu as CoreBubbleMenu,
BubbleMenuProps as CoreBubbleMenuProps,
} from '@tiptap/react';
-import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import { isString } from 'lodash';
-import { FC, useMemo } from 'react';
+import { FC, MouseEvent, useMemo } from 'react';
import { ReactComponent as FormatBoldIcon } from '../../../assets/svg/ic-format-bold.svg';
import { ReactComponent as FormatInlineCodeIcon } from '../../../assets/svg/ic-format-inline-code.svg';
import { ReactComponent as FormatItalicIcon } from '../../../assets/svg/ic-format-italic.svg';
@@ -133,29 +132,31 @@ const BubbleMenu: FC = ({ editor, toggleLink }) => {
shouldShow={handleShouldShow}>
{menuList.map(
({ icon: Icon, ariaLabel, className, command, isActive }) => (
-
- {
- // To prevent losing focus from editor
- // The mouseDown event fires before the click event and before focus changes,
- // so we can intercept it and prevent the default focus behavior.
- e.preventDefault();
- command();
- }}>
- {isString(Icon) ? (
+
{Icon}
) : (
- )}
-
-
+ )
+ }
+ key={ariaLabel}
+ tooltip={ariaLabel}
+ onMouseDown={(e: MouseEvent) => {
+ // To prevent losing focus from editor
+ // The mouseDown event fires before the click event and before focus changes,
+ // so we can intercept it and prevent the default focus behavior.
+ e.preventDefault();
+ command();
+ }}
+ />
)
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.test.tsx
index c2e16dd218f5..3adb3d23aa91 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.test.tsx
@@ -25,9 +25,15 @@ import { EditorSlotsRef } from './BlockEditor.interface';
import EditorSlots from './EditorSlots';
import { LinkExtension } from './Extensions/link';
-jest.mock('./BlockMenu/BlockMenu', () => () => null);
-jest.mock('./BubbleMenu/BubbleMenu', () => () => null);
-jest.mock('./TableMenu/TableMenu', () => () => null);
+jest.mock('./BlockMenu/BlockMenu', () => () => (
+
+));
+jest.mock('./BubbleMenu/BubbleMenu', () => () => (
+
+));
+jest.mock('./TableMenu/TableMenu', () => () => (
+
+));
jest.mock('./LinkPopup/LinkPopup', () => () => null);
jest.mock('tippy.js', () => ({
__esModule: true,
@@ -43,7 +49,7 @@ const createTestEditor = (content: string) =>
const fillLinkAndSave = async (href: string) => {
await waitFor(() => expect(screen.getByRole('textbox')).toBeInTheDocument());
fireEvent.change(screen.getByRole('textbox'), { target: { value: href } });
- fireEvent.click(screen.getByRole('button', { name: 'Save' }));
+ fireEvent.click(screen.getByRole('button', { name: 'label.save' }));
};
describe('EditorSlots link handling', () => {
@@ -164,3 +170,29 @@ describe('EditorSlots link handling', () => {
document.body.removeChild(dialog);
});
});
+
+describe('EditorSlots menus', () => {
+ it('renders the editing menus for an editable editor', () => {
+ const editor = createTestEditor('Hello
');
+
+ render();
+
+ expect(screen.getByTestId('bubble-menu')).toBeInTheDocument();
+ expect(screen.getByTestId('block-menu')).toBeInTheDocument();
+ expect(screen.getByTestId('table-menu')).toBeInTheDocument();
+
+ editor.destroy();
+ });
+
+ it('skips the editing menus for a read-only editor', () => {
+ const editor = createTestEditor('Hello
');
+
+ render();
+
+ expect(screen.queryByTestId('bubble-menu')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('block-menu')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('table-menu')).not.toBeInTheDocument();
+
+ editor.destroy();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
index 1054069f0d10..deea0382a359 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx
@@ -23,7 +23,7 @@ import LinkPopup from './LinkPopup/LinkPopup';
import TableMenu from './TableMenu/TableMenu';
const EditorSlots = forwardRef(
- ({ editor, menuType }, ref) => {
+ ({ editable = true, editor, menuType }, ref) => {
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
const handleLinkToggle = () => {
@@ -163,7 +163,7 @@ const EditorSlots = forwardRef(
* render the bubble menu only if the editor is available
* and the menu type is bubble
*/
- const menus = !isNil(editor) && menuType === 'bubble' && (
+ const menus = !isNil(editor) && editable && menuType === 'bubble' && (
);
@@ -193,7 +193,9 @@ const EditorSlots = forwardRef(
/>
)}
{menus}
- {!isNil(editor) && (
+ {/* Read-only previews never use these menus. Their react-aria buttons
+ would also capture the trigger of any tooltip they render inside. */}
+ {!isNil(editor) && editable && (
<>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/Callout/CalloutComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/Callout/CalloutComponent.tsx
index fe98dec0c99a..95087a3923e0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/Callout/CalloutComponent.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/Callout/CalloutComponent.tsx
@@ -11,12 +11,12 @@
* limitations under the License.
*/
import {
+ Button,
ButtonUtility,
Popover,
PopoverTrigger,
} from '@openmetadata/ui-core-components';
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
-import { Button } from 'antd';
import { startCase } from 'lodash';
import { FC, useState } from 'react';
import { CALLOUT_CONTENT } from '../../../../constants/BlockEditor.constants';
@@ -31,13 +31,13 @@ const PopoverContent = ({
{Object.entries(CALLOUT_CONTENT).map(([key, CalloutIcon]) => {
return (
}
key={key}
- type="text"
- onClick={() => onSelect(key)}>
+ onPress={() => onSelect(key)}>
{startCase(key)}
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx
index ff8fc1a383ab..13667d10de36 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx
@@ -10,9 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { ButtonUtility, Tooltip } from '@openmetadata/ui-core-components';
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
-import { Button, Tooltip } from 'antd';
-import { FC, useCallback } from 'react';
+import { FC, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CopyIcon } from '../../../../assets/svg/icon-copy.svg';
import { useClipboard } from '../../../../hooks/useClipBoard';
@@ -20,6 +20,7 @@ import { useClipboard } from '../../../../hooks/useClipBoard';
const CodeBlockComponent: FC = ({ node }) => {
const { t } = useTranslation();
const { onCopyToClipBoard, hasCopied } = useClipboard('', 2000);
+ const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const handleCopy = useCallback(async () => {
await onCopyToClipBoard(node.textContent);
@@ -30,12 +31,15 @@ const CodeBlockComponent: FC = ({ node }) => {
-
+ }
- type="text"
onClick={handleCopy}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.tsx
index 602deba70244..093e62fba302 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/AttachmentComponents/AttachmentPlaceholder.tsx
@@ -10,7 +10,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import Icon from '@ant-design/icons';
import { Typography } from '@openmetadata/ui-core-components';
import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -33,7 +32,13 @@ const AttachmentPlaceholder: FC = ({
className="image-placeholder"
contentEditable={false}
data-testid="image-placeholder">
-
+
{t('label.add-an-file-type', {
fileType: t(`label.${fileType}`),
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/file-node.less b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/file-node.less
index 3bea50eac68e..34562430e425 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/file-node.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/File/file-node.less
@@ -296,7 +296,7 @@
background-color: @grey-2;
}
- .anticon {
+ .attachment-placeholder-icon {
font-size: var(--om-font-size-32);
color: @grey-4;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.test.tsx
new file mode 100644
index 000000000000..9d8822a1499e
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { NodeViewProps } from '@tiptap/react';
+import { MathEquationComponent } from './MathEquationComponent';
+
+jest.mock('react-latex-next', () =>
+ jest.fn().mockImplementation(({ children }) => {children})
+);
+
+const updateAttributes = jest.fn();
+
+const renderComponent = (isEditing: boolean) =>
+ render(
+
+ );
+
+describe('MathEquationComponent', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('saves the edited equation from the textarea', () => {
+ renderComponent(true);
+
+ fireEvent.change(screen.getByRole('textbox'), {
+ target: { value: 'y^2' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'label.save' }));
+
+ expect(updateAttributes).toHaveBeenCalledWith({
+ math_equation: 'y^2',
+ isEditing: false,
+ });
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+ });
+
+ it('discards the edit on cancel', () => {
+ renderComponent(true);
+
+ fireEvent.click(screen.getByRole('button', { name: 'label.cancel' }));
+
+ expect(updateAttributes).not.toHaveBeenCalled();
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+ expect(screen.getByText('x^2')).toBeInTheDocument();
+ });
+
+ it('enters edit mode from the edit button', () => {
+ renderComponent(false);
+
+ fireEvent.click(screen.getByRole('button', { name: 'label.edit-entity' }));
+
+ expect(screen.getByRole('textbox')).toHaveValue('x^2');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.tsx
index 320c7a9e3855..e40d26f60b27 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/MathEquation/MathEquationComponent.tsx
@@ -10,10 +10,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
+import {
+ Box,
+ Button,
+ ButtonUtility,
+ TextArea,
+} from '@openmetadata/ui-core-components';
+import { Check, XClose } from '@openmetadata/ui-core-components/icons';
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
-import { Button, Input, Space, Tooltip } from 'antd';
-import { TextAreaRef } from 'antd/lib/input/TextArea';
import classNames from 'classnames';
import 'katex/dist/katex.min.css';
import { FC, useRef, useState } from 'react';
@@ -28,7 +32,7 @@ export const MathEquationComponent: FC = ({
editor,
}) => {
const { t } = useTranslation();
- const inputRef = useRef(null);
+ const inputRef = useRef(null);
const equation = node.attrs.math_equation;
const [isEditing, setIsEditing] = useState(() =>
@@ -37,8 +41,7 @@ export const MathEquationComponent: FC = ({
const handleSaveEquation = () => {
updateAttributes({
- math_equation:
- inputRef.current?.resizableTextArea?.textArea.value ?? equation,
+ math_equation: inputRef.current?.value ?? equation,
isEditing: false,
});
setIsEditing(false);
@@ -52,46 +55,52 @@ export const MathEquationComponent: FC = ({
})}>
{isEditing ? (
-
+ classNames(
+ 'math-equation-input tw:bg-transparent tw:shadow-none',
+ { 'tw:outline-transparent': !isFocused }
+ )
+ }
+ textAreaRef={inputRef}
/>
-
+
}
- size="small"
- type="default"
- onClick={() => setIsEditing(false)}
+ aria-label={t('label.cancel')}
+ color="secondary"
+ iconLeading={XClose}
+ size="xs"
+ onPress={() => setIsEditing(false)}
/>
}
- size="small"
- type="primary"
- onClick={handleSaveEquation}
+ aria-label={t('label.save')}
+ color="primary"
+ iconLeading={Check}
+ size="xs"
+ onPress={handleSaveEquation}
/>
-
+
) : (
{equation}
)}
{/* Show edit button only when the editor is editable */}
{!isEditing && editor.isEditable && (
-
- }
- size="small"
- type="text"
- onClick={() => setIsEditing(true)}
- />
-
+ }
+ size="xs"
+ tooltip={t('label.edit-entity', { entity: t('label.equation') })}
+ onClick={() => setIsEditing(true)}
+ />
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/mention/MentionList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/mention/MentionList.tsx
index 99f0ffe2ba1a..c8c8e7e0db7b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/mention/MentionList.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/mention/MentionList.tsx
@@ -10,9 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Typography } from '@openmetadata/ui-core-components';
+import { Box, Typography } from '@openmetadata/ui-core-components';
import { SuggestionProps } from '@tiptap/suggestion';
-import { Space } from 'antd';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react';
@@ -105,15 +104,20 @@ export default forwardRef>(
}));
return (
-
{items.map((item, index) => (
- selectItem(index)}>
@@ -121,9 +125,9 @@ export default forwardRef>(
{item.label}
-
+
))}
-
+
);
}
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/SlashCommandList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/SlashCommandList.tsx
index afa7fb82aa7b..34a3519ffd21 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/SlashCommandList.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/SlashCommandList.tsx
@@ -10,9 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Typography } from '@openmetadata/ui-core-components';
+import { Box, Typography } from '@openmetadata/ui-core-components';
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
-import { Image, Space } from 'antd';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react';
@@ -114,34 +113,39 @@ export const SlashCommandList = forwardRef(
}
return (
-
{filteredItems.map((item, index) => (
- selectItem(index)}>
-
-
+
{item.title}
{item.description}
-
-
+
+
))}
-
+
);
}
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.test.tsx
index 390c517930b4..a9ad8c73c8f4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.test.tsx
@@ -47,7 +47,7 @@ describe('LinkModal', () => {
fireEvent.change(screen.getByRole('textbox'), {
target: { value: '{{buildEntityUrl event.entityType entity}}' },
});
- fireEvent.click(screen.getByRole('button', { name: 'Save' }));
+ fireEvent.click(screen.getByRole('button', { name: 'label.save' }));
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith({
@@ -59,7 +59,7 @@ describe('LinkModal', () => {
it('should call onCancel when the cancel button is clicked', () => {
render();
- fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ fireEvent.click(screen.getByRole('button', { name: 'label.cancel' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.tsx
index 31a9786e0130..b83b7e694473 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkModal/LinkModal.tsx
@@ -10,8 +10,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Form, FormProps, Input, Modal } from 'antd';
-import { FC } from 'react';
+import {
+ Button,
+ Dialog,
+ Input,
+ Modal,
+ ModalOverlay,
+} from '@openmetadata/ui-core-components';
+import { FC, FormEvent, useState } from 'react';
+import { useTranslation } from 'react-i18next';
export interface LinkData {
href: string;
@@ -36,38 +43,55 @@ const LinkModal: FC = ({
onCancel,
getContainer,
}) => {
- const handleSubmit: FormProps['onFinish'] = (values) => {
- onSave(values);
+ const { t } = useTranslation();
+ const [href, setHref] = useState(data.href);
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ onSave({ href });
};
return (
-
-
-
-
-
-
+ !open && onCancel()}>
+
+
+
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkPopup/LinkPopup.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkPopup/LinkPopup.tsx
index 12e0dcab44b6..b6ab8e94494d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkPopup/LinkPopup.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/LinkPopup/LinkPopup.tsx
@@ -10,13 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Button, Space } from 'antd';
+import { Box, ButtonUtility } from '@openmetadata/ui-core-components';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg';
import { ReactComponent as UnlinkIcon } from '../../../assets/svg/ic-format-unlink.svg';
-import { FC } from 'react';
-
interface LinkPopupProps {
href: string;
handleLinkToggle: () => void;
@@ -30,32 +30,38 @@ const LinkPopup: FC = ({
handleLinkToggle,
handleUnlink,
}) => {
+ const { t } = useTranslation();
+ const linkLabel = t('label.link');
+
return (
-
-
+ }
- type="text"
onClick={handleLinkToggle}
/>
- }
rel="noopener noreferrer"
target="_blank"
- type="link"
/>
- }
- type="text"
onClick={handleUnlink}
/>
-
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx
index 388155af1dcb..dae7382a3cdc 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx
@@ -10,8 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { Box, ButtonUtility } from '@openmetadata/ui-core-components';
import type { Editor } from '@tiptap/react';
-import { Button, Space, Tooltip } from 'antd';
import { useCallback, useEffect, useRef } from 'react';
import tippy, { Instance } from 'tippy.js';
import { ReactComponent as IconDeleteTable } from '../../../assets/svg/ic-delete.svg';
@@ -158,55 +158,57 @@ const TableMenu = (props: TableMenuProps) => {
return (
-
-
- editor.chain().focus().addRowAfter().run()}>
-
-
-
-
-
- editor.chain().focus().addColumnAfter().run()}>
-
-
-
-
-
- editor.chain().focus().deleteRow().run()}>
-
-
-
-
-
- editor.chain().focus().deleteColumn().run()}>
-
-
-
-
-
- {
- editor.chain().focus().deleteTable().run();
- tableMenuPopup.current?.hide();
- }}>
+
+ }
+ tooltip="Add row after current row"
+ onClick={() => editor.chain().focus().addRowAfter().run()}
+ />
+
+ }
+ tooltip="Add column after current column"
+ onClick={() => editor.chain().focus().addColumnAfter().run()}
+ />
+
+ }
+ tooltip="Delete current row"
+ onClick={() => editor.chain().focus().deleteRow().run()}
+ />
+
+ }
+ tooltip="Delete current column"
+ onClick={() => editor.chain().focus().deleteColumn().run()}
+ />
+
+
-
-
-
+ }
+ tooltip="Delete table"
+ onClick={() => {
+ editor.chain().focus().deleteTable().run();
+ tableMenuPopup.current?.hide();
+ }}
+ />
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
index ec931e5bd570..6d5e118d306f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less
@@ -358,15 +358,6 @@
word-break: break-word;
}
-.block-editor-link-modal,
-.block-editor-image-modal {
- .ant-modal-content {
- .ant-modal-body {
- padding: var(--om-space-16);
- }
- }
-}
-
.link-popup {
align-items: center;
padding: var(--om-space-4);
@@ -517,10 +508,11 @@ body .om-image-node-embed-link-btn-col {
display: flex;
flex-direction: column;
align-items: baseline;
- .ant-btn {
+ button {
width: 100%;
display: inline-flex;
gap: var(--om-space-4);
+ justify-content: flex-start;
text-align: left;
padding: var(--om-space-8) var(--om-space-16);
height: auto;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx
index 807a4e736ca5..86e7bdbcaeda 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseList.component.test.tsx
@@ -12,10 +12,10 @@
*/
import {
fireEvent,
- queryByAttribute,
render,
screen,
waitFor,
+ within,
} from '@testing-library/react';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -103,6 +103,10 @@ jest.mock('../../../constants/constants', () => ({
PAGE_SIZE_MEDIUM: 25,
}));
+// Core Checkbox puts the test id on its