diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json
index 4ab55e2b23e1..09a813824641 100644
--- a/.github/playwright/impact-map.generated.json
+++ b/.github/playwright/impact-map.generated.json
@@ -740,8 +740,10 @@
"playwright/e2e/Features/AppMode/AppModeResolver.spec.ts",
"playwright/e2e/Features/AppMode/AppModeRouteIsolation.spec.ts",
"playwright/e2e/Features/AppMode/AppModeUrlHandling.spec.ts",
+ "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/RunTestCase.spec.ts",
"playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/ProfilePlatformSettings.spec.ts",
"playwright/e2e/Pages/BotsProfile.spec.ts"
]
@@ -5130,6 +5132,7 @@
"playwright/e2e/Features/CustomizeDetailPage.spec.ts",
"playwright/e2e/Features/DataProductPersonaCustomization.spec.ts",
"playwright/e2e/Features/DataQuality/DataQuality.spec.ts",
+ "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts",
"playwright/e2e/Features/FailedTestCaseSampleData.spec.ts",
"playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts",
@@ -5249,6 +5252,15 @@
"playwright/e2e/Features/FeedEditorDuplicateId.spec.ts"
]
},
+ {
+ "sources": [
+ "openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx"
+ ],
+ "specs": [
+ "playwright/e2e/Features/ActivityFeed.spec.ts",
+ "playwright/e2e/Features/Tasks/TaskNavigation.spec.ts"
+ ]
+ },
{
"sources": [
"openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditorNew.tsx"
@@ -5433,6 +5445,7 @@
"specs": [
"playwright/e2e/Features/AIMode/InboxTasks.spec.ts",
"playwright/e2e/Features/DataQuality/DataObservabilityGovernanceTab.spec.ts",
+ "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DataQuality/TestDefinitionFilters.spec.ts",
"playwright/e2e/Features/DomainFilterQueryFilter.spec.ts",
"playwright/e2e/Features/ExploreFilterComposition.spec.ts",
@@ -5441,6 +5454,7 @@
"playwright/e2e/Features/ExploreSortOrderFilter.spec.ts",
"playwright/e2e/Features/ExploreUrlState.spec.ts",
"playwright/e2e/Features/ImpactAnalysis.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/Pagination.spec.ts",
"playwright/e2e/Flow/ExploreDiscovery.spec.ts",
"playwright/e2e/Pages/AuditLogs.spec.ts",
@@ -5490,6 +5504,7 @@
"playwright/e2e/Features/ExploreQuickFilters.spec.ts",
"playwright/e2e/Features/ExploreSortOrderFilter.spec.ts",
"playwright/e2e/Features/ExploreUrlState.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/NestedColumnsExpandCollapse.spec.ts",
"playwright/e2e/Features/SearchExport.spec.ts",
"playwright/e2e/Flow/ExploreDiscovery.spec.ts",
@@ -5696,6 +5711,7 @@
"playwright/e2e/Features/ExploreQuickFilters.spec.ts",
"playwright/e2e/Features/ExploreSortOrderFilter.spec.ts",
"playwright/e2e/Features/ExploreUrlState.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/NestedColumnsExpandCollapse.spec.ts",
"playwright/e2e/Features/SearchExport.spec.ts",
"playwright/e2e/Flow/ExploreDiscovery.spec.ts",
@@ -7343,6 +7359,7 @@
"openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx"
],
"specs": [
+ "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
"playwright/e2e/Features/DomainIsolation/DomainIncidentIsolation.spec.ts",
"playwright/e2e/Features/IncidentManager.spec.ts"
]
@@ -8432,9 +8449,7 @@
],
"specs": [
"playwright/e2e/Features/AgentLogStreamHandover.spec.ts",
- "playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts",
"playwright/e2e/Features/ServiceAgentsStatusDegradation.spec.ts",
- "playwright/e2e/Flow/ServiceCreationPermissions.spec.ts",
"playwright/e2e/Pages/LogsViewer.spec.ts"
]
},
@@ -9062,8 +9077,6 @@
"openmetadata-ui/src/main/resources/ui/src/components/common/DatePickerMenu/DatePickerMenu.component.tsx"
],
"specs": [
- "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts",
- "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Pages/AuditLogs.spec.ts",
"playwright/e2e/Pages/DataInsight.spec.ts"
]
@@ -9738,6 +9751,7 @@
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts",
"playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts",
"playwright/e2e/Features/Glossary/GlossaryRemoveOperations.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/QueryEntity.spec.ts",
"playwright/e2e/Flow/PersonaFlow.spec.ts",
"playwright/e2e/Pages/DataContractInheritance.spec.ts",
@@ -9945,6 +9959,7 @@
"playwright/e2e/Features/AIMode/ObservabilityFilters.spec.ts",
"playwright/e2e/Features/DataAssetRulesDisabled.spec.ts",
"playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/QueryEntity.spec.ts",
"playwright/e2e/Pages/DomainUIInteractions.spec.ts",
"playwright/e2e/Pages/TestSuite.spec.ts"
@@ -12787,6 +12802,7 @@
"playwright/e2e/Features/ExploreQuickFilters.spec.ts",
"playwright/e2e/Features/ExploreSortOrderFilter.spec.ts",
"playwright/e2e/Features/ExploreUrlState.spec.ts",
+ "playwright/e2e/Features/IncidentManager.spec.ts",
"playwright/e2e/Features/NestedColumnsExpandCollapse.spec.ts",
"playwright/e2e/Features/SearchExport.spec.ts",
"playwright/e2e/Flow/ExploreDiscovery.spec.ts",
diff --git a/docs/decisions/2026-10-10-alert-widgets-use-values-and-oss-merges-before-collate.md b/docs/decisions/2026-10-10-alert-widgets-use-values-and-oss-merges-before-collate.md
new file mode 100644
index 000000000000..e520d21e64e1
--- /dev/null
+++ b/docs/decisions/2026-10-10-alert-widgets-use-values-and-oss-merges-before-collate.md
@@ -0,0 +1,41 @@
+# Alert widgets consume form values, and the Observability migration merges OSS before Collate
+
+- **Status:** Accepted
+- **Revisions:** v1 2026-10-10 (initial)
+- **Deciders:** Shailesh Parmar
+- **Guard:** `AlertEditModal.component.test.tsx` and Collate `TestAlertButton.test.tsx`; paired build and merge-order review
+- **Related:** [OpenMetadata #34935](https://github.com/open-metadata/OpenMetadata/issues/34935), [OpenMetadata #34936](https://github.com/open-metadata/OpenMetadata/pull/34936), [Collate #7216](https://github.com/open-metadata/openmetadata-collate/pull/7216)
+
+## Context
+
+The AI Observability alert form now owns its state without Ant Design. Its footer widgets are
+registered through `AlertsClassBase` and implemented downstream in Collate. The previous
+`TestAlertButton` read an Ant Design form instance, which the AI form can no longer provide.
+The same migration removes `pipeline-actions.less`, still imported by Collate main.
+
+## Decision
+
+Alert footer widgets read the current `ModifiedCreateEventSubscription` through the `values`
+prop of `AddAlertFormWidgetProps`. Both Classic and AI callers supply these values. They are
+the current editing state, not a promise of submission validation. Widgets remain responsible
+for deciding whether their operation has the fields it requires.
+
+The AI form does not pass `formRef`. The optional `formRef` remains available to existing
+Classic widgets that write into the legacy form; read-only widgets such as Collate's
+`TestAlertButton` must not require it. Collate uses `values` to derive availability and the
+test-notification request, including edits made after the widget first rendered.
+
+Validate the OSS and Collate migration branches together. Merge OSS #34936 first, then Collate
+#7216 immediately afterward. Keep Collate's OpenMetadata submodule branch on `main` for merge.
+The Collate change consumes the new OSS widget contract and removes the deleted stylesheet
+import. Do not restore the deleted stylesheet or an Ant Design form instance to bridge the two
+merges.
+
+## Consequences
+
+Widgets can serve both form implementations without importing Ant Design or retaining a form
+instance. The companion PR is required for this migration: between the merges, Collate main
+has an obsolete stylesheet import and its test-alert widget still expects `formRef`. Treat
+that interval as a coordinated integration window, and verify Collate against the merged OSS
+main before considering the migration integrated. A future independently deployable transition
+would need an explicit compatibility phase instead of this immediate merge sequence.
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/modals/modal.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/modals/modal.tsx
index fad5f37caa1d..b1a7fbe4fb43 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/modals/modal.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/modals/modal.tsx
@@ -95,6 +95,7 @@ const DialogHeader = ({ title, children, className }: DialogHeaderProps) => (
{title && (
{title}
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/box/box.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/box/box.tsx
index 5a2f8ac3b707..db537f86a5f0 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/box/box.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/box/box.tsx
@@ -105,7 +105,7 @@ const getBoxItems = (
itemClassName: string,
keyPrefix = ''
): ReactNode[] =>
- Children.toArray(children).flatMap((child, index) => {
+ Children.toArray(children).flatMap((child, index) => {
const key = `${keyPrefix}/${isValidElement(child) ? child.key : index}`;
// Fragments group JSX, not layout items; keep their children independently spaced.
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
index 9573c0aefee1..4c7861b05ac4 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts
@@ -165,6 +165,12 @@ export * from './application/popover/popover';
export * from './application/progress-steps/progress-steps';
export { SlideoutMenu } from './application/slideout-menus/slideout-menu';
export * from './application/table/table';
+export {
+ GridList,
+ GridListItem,
+ ListLayout,
+ Virtualizer,
+} from 'react-aria-components';
export * from './application/tabs/tabs';
export * from './application/toast/toast';
export * from './application/toast/toast-provider';
diff --git a/openmetadata-ui/src/main/resources/ui/docs/colors.md b/openmetadata-ui/src/main/resources/ui/docs/colors.md
index 4d29afc5be7f..2462ed576586 100644
--- a/openmetadata-ui/src/main/resources/ui/docs/colors.md
+++ b/openmetadata-ui/src/main/resources/ui/docs/colors.md
@@ -85,7 +85,7 @@ Use background tokens for surfaces, containers, cards, modals, and page-level ba
| Tailwind Class | CSS Variable | Light | Dark | When to Use |
|---|---|---|---|---|
-| `tw:bg-brand-primary` | `--color-bg-brand-primary` | `#eff8ff` | Brand 500 at 16% | Light brand tint surface |
+| `tw:bg-brand-primary` | `--color-bg-brand-primary` | `#eff8ff` | Brand 500 at 14% | Light brand tint surface |
| `tw:bg-brand-primary_alt` | `--color-bg-brand-primary_alt` | `#eff8ff` | `#191919` | Alternate brand tint |
| `tw:bg-brand-secondary` | `--color-bg-brand-secondary` | `#d1e9ff` | `#1570ef` | Stronger brand tint |
| `tw:bg-brand-solid` | `--color-bg-brand-solid` | `#1570ef` | `#1570ef` | Primary CTA button fill |
diff --git a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json
index 18a206c21ef9..aab27a2cf803 100644
--- a/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json
+++ b/openmetadata-ui/src/main/resources/ui/eslint-suppressions.json
@@ -812,7 +812,7 @@
},
"playwright/utils/taskWorkflow.ts": {
"om-playwright/no-positional-locator": {
- "count": 3
+ "count": 2
},
"playwright/no-wait-for-selector": {
"count": 1
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts
index a1a436b6914b..eaf20b6b6e37 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts
@@ -473,7 +473,7 @@ test.describe('FeedWidget on landing page', () => {
await expect(seededCard).toBeVisible();
await seededCard.click();
- const drawer = page.locator('.activity-feed-drawer');
+ const drawer = page.getByTestId('activity-feed-drawer');
await expect(drawer).toBeVisible();
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/DataQuality/IncidentManagerDateFilter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts
index ede0cfe890cc..31a6e7a7b507 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts
@@ -10,15 +10,143 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { expect } from '@playwright/test';
+import { expect, Page } from '@playwright/test';
import { DOMAIN_TAGS } from '../../../constant/config';
import { SidebarItem } from '../../../constant/sidebar';
import { TableClass } from '../../../support/entity/TableClass';
import { performAdminLogin } from '../../../utils/admin';
-import { redirectToHomePage } from '../../../utils/common';
+import {
+ clickIgnoringToasts,
+ redirectToHomePage,
+ scrollIntoViewAndSettle,
+ selectOptionWithRetry,
+} from '../../../utils/common';
import { waitForAllLoadersToDisappear } from '../../../utils/entity';
import { sidebarClick } from '../../../utils/sidebar';
+import {
+ waitForAriaOverlayToSettle,
+ waitForResponseWithStatus,
+} from '../../../utils/waitHelpers';
import { test } from '../../fixtures/pages';
+import {
+ disableAiAppMode,
+ stubUserPreferencesAppMode,
+} from '../../Utils/appMode';
+
+const dateRangeTrigger = (page: Page) =>
+ page
+ .getByRole('group', { name: 'Date Range', exact: true })
+ .getByRole('button');
+
+const waitForIncidentList = (
+ page: Page,
+ matchesParams: (params: URLSearchParams) => boolean = () => true
+) =>
+ waitForResponseWithStatus(
+ page,
+ (response) => {
+ const url = new URL(response.url());
+ return (
+ response.request().method() === 'GET' &&
+ url.pathname ===
+ '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list' &&
+ matchesParams(url.searchParams)
+ );
+ },
+ 200
+ );
+
+const expectEmptyDateRange = async (page: Page) => {
+ await expect(dateRangeTrigger(page)).toHaveText('Select dates');
+ await expect(page).toHaveURL(
+ (url) => !url.searchParams.has('startTs') && !url.searchParams.has('endTs')
+ );
+ await expect(page.getByTestId('incident-clear-filters')).not.toBeVisible();
+};
+
+const applyTodayRange = async (page: Page) => {
+ const trigger = dateRangeTrigger(page);
+ await expect(trigger).toHaveText('Select dates');
+ await scrollIntoViewAndSettle(trigger);
+ await trigger.click();
+ const dialog = page.getByRole('dialog');
+ await expect(dialog).toBeVisible();
+ await waitForAriaOverlayToSettle(page);
+
+ const range = await page.evaluate(() => {
+ const start = new Date();
+ start.setHours(0, 0, 0, 0);
+ const end = new Date(start);
+ end.setDate(end.getDate() + 1);
+ return {
+ startTs: String(start.getTime()),
+ endTs: String(end.getTime() - 1),
+ };
+ });
+ await dialog.getByRole('button', { name: 'Today', exact: true }).click();
+ await expect(dialog).toBeVisible();
+ await expect(trigger).not.toHaveText('Select dates');
+ await expect(page).toHaveURL(
+ (url) => !url.searchParams.has('startTs') && !url.searchParams.has('endTs')
+ );
+
+ const responsePromise = waitForIncidentList(
+ page,
+ (params) =>
+ params.get('startTs') === range.startTs &&
+ params.get('endTs') === range.endTs
+ );
+ await clickIgnoringToasts(
+ dialog.getByRole('button', { name: 'Apply', exact: true })
+ );
+ await responsePromise;
+ await expect(dialog).not.toBeVisible();
+ await expect(trigger).not.toHaveText('Select dates');
+ await expect(page).toHaveURL(
+ (url) =>
+ url.searchParams.get('startTs') === range.startTs &&
+ url.searchParams.get('endTs') === range.endTs
+ );
+ return range;
+};
+
+const clearDateRange = async (page: Page) => {
+ const clearButton = page.getByTestId('incident-clear-filters');
+ await expect(clearButton).toBeVisible();
+ const responsePromise = waitForIncidentList(
+ page,
+ (params) => !params.has('startTs') && !params.has('endTs')
+ );
+ await clearButton.click();
+ await responsePromise;
+ await expectEmptyDateRange(page);
+};
+
+const selectDateField = async (
+ page: Page,
+ field: 'updatedAt' | 'timestamp'
+) => {
+ const responsePromise = waitForIncidentList(
+ page,
+ (params) => params.get('dateField') === field
+ );
+ await selectOptionWithRetry(
+ page.getByTestId('sort-field-dropdown-trigger'),
+ page.getByTestId('drop-down-menu').getByTestId(field)
+ );
+ await responsePromise;
+ await expect(page.getByTestId('sort-field-dropdown-trigger')).toContainText(
+ field === 'updatedAt' ? 'Updated at' : 'Created at'
+ );
+ await expect(page).toHaveURL(
+ (url) => url.searchParams.get('dateField') === field
+ );
+};
+
+test.beforeEach(async ({ page }) => {
+ await disableAiAppMode(page);
+ await stubUserPreferencesAppMode(page, 'classic');
+});
test.describe(
'Incident Manager Date Filter',
@@ -27,13 +155,9 @@ test.describe(
let table: TableClass;
test.beforeAll(async ({ browser }) => {
- test.slow();
const { apiContext, afterAction } = await performAdminLogin(browser);
table = new TableClass();
-
await table.create(apiContext);
-
- // Create table and test cases
await table.createTestCase(apiContext, {
parameterValues: [
{ name: 'minColValue', value: 12 },
@@ -43,24 +167,18 @@ test.describe(
});
const testCase = table.testCasesResponseData[0];
-
- // Create a failed result to generate an incident
await table.addTestCaseResult(apiContext, testCase.fullyQualifiedName, {
testCaseStatus: 'Failed',
result: 'Column count was 10, expected between 12 and 24',
timestamp: Date.now(),
testResultValue: [{ name: 'columnCount', value: '10' }],
});
-
- // Add another failure from 5 days ago for range testing
- const fiveDaysAgo = Date.now() - 5 * 24 * 60 * 60 * 1000;
await table.addTestCaseResult(apiContext, testCase.fullyQualifiedName, {
testCaseStatus: 'Failed',
result: 'Column count was 10, expected between 12 and 24',
- timestamp: fiveDaysAgo,
+ timestamp: Date.now() - 5 * 24 * 60 * 60 * 1000,
testResultValue: [{ name: 'columnCount', value: '10' }],
});
-
await afterAction();
});
@@ -74,367 +192,120 @@ test.describe(
await redirectToHomePage(page);
await table.visitEntityPage(page);
await page.getByTestId('profiler').click();
- const incidentListResponse = page.waitForResponse((response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- )
- );
+ const responsePromise = waitForIncidentList(page);
await page.getByRole('tab', { name: 'Incidents' }).click();
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
+ await responsePromise;
});
test('Date picker shows placeholder when no date is selected', async ({
page,
}) => {
- const datePicker = page.getByTestId('date-picker-menu');
- await expect(datePicker).toBeVisible();
- await expect(datePicker).toContainText('Select Date');
-
- // Verify URL does not have date params
- const url = new URL(page.url());
- expect(url.searchParams.has('startTs')).toBeFalsy();
- expect(url.searchParams.has('endTs')).toBeFalsy();
+ await expectEmptyDateRange(page);
});
test('Select preset date range', async ({ page }) => {
- const datePicker = page.getByTestId('date-picker-menu');
- await datePicker.click();
-
- const last7DaysOption = page.getByRole('menuitem', {
- name: 'Last 7 days',
- });
-
- // Wait for API call that happens on selection change.
- // We strictly wait for a request that has searchParams to avoid catching the initial load or others.
- const incidentListResponse = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('startTs')
- );
-
- await last7DaysOption.click();
-
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
- const url = new URL(response.url());
-
- // Validate API params
- expect(url.searchParams.has('startTs')).toBeTruthy();
- expect(url.searchParams.has('endTs')).toBeTruthy();
-
- const startTs = parseInt(url.searchParams.get('startTs') || '0');
- const endTs = parseInt(url.searchParams.get('endTs') || '0');
-
- expect(startTs).toBeLessThan(endTs);
-
- // Verify button text
- await expect(datePicker).toContainText('Last 7 days');
-
- // Verify URL params
- const pageUrl = new URL(page.url());
- expect(pageUrl.searchParams.get('key')).toBe('last7days');
+ await applyTodayRange(page);
});
test('Clear selected date range', async ({ page }) => {
- // First select a range to ensure we can clear it
- const datePicker = page.getByTestId('date-picker-menu');
- await datePicker.click();
-
- // Wait for request with startTs to ensure selection is applied
- const selectionResponse = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('startTs')
- );
- await page.getByRole('menuitem', { name: 'Last 7 days' }).click();
- const afterSelectResponse = await selectionResponse;
- expect(afterSelectResponse.status()).toBe(200);
-
- await expect(datePicker).toContainText('Last 7 days');
-
- // Now clear it
- const clearButton = page.getByTestId('clear-date-picker');
- await expect(clearButton).toBeVisible();
-
- // Intercept response to verify params are GONE
- const incidentListResponse = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && !response.url().includes('startTs')
- );
-
- await clearButton.click();
-
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
- const url = new URL(response.url());
-
- // Validate API params do NOT exist
- expect(url.searchParams.has('startTs')).toBeFalsy();
- expect(url.searchParams.has('endTs')).toBeFalsy();
-
- // Verify UI reset
- await expect(datePicker).toContainText('Select Date');
- await expect(clearButton).not.toBeVisible();
-
- // Verify Page URL params removed
- const pageUrl = new URL(page.url());
- expect(pageUrl.searchParams.has('startTs')).toBeFalsy();
- expect(pageUrl.searchParams.has('endTs')).toBeFalsy();
- expect(pageUrl.searchParams.has('key')).toBeFalsy();
+ await applyTodayRange(page);
+ await clearDateRange(page);
});
test('Date filter persists on page reload', async ({ page }) => {
- const datePicker = page.getByTestId('date-picker-menu');
- await datePicker.click();
- await page.getByRole('menuitem', { name: 'Last 7 days' }).click();
- await expect(datePicker).toContainText('Last 7 days');
-
- const incidentListResponse = page.waitForResponse((response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- )
+ const range = await applyTodayRange(page);
+ const label = await dateRangeTrigger(page).innerText();
+ const responsePromise = waitForIncidentList(
+ page,
+ (params) =>
+ params.get('startTs') === range.startTs &&
+ params.get('endTs') === range.endTs
);
-
- // Verify URL has the key BEFORE reloading
- await expect(page).toHaveURL(/key=last7days/);
-
await page.reload({ waitUntil: 'domcontentloaded' });
- await page.waitForLoadState('domcontentloaded');
+ await responsePromise;
await waitForAllLoadersToDisappear(page);
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
-
- const datePickerReloaded = page.getByTestId('date-picker-menu');
- await expect(datePickerReloaded).toBeVisible({ timeout: 15000 });
- await expect(datePickerReloaded).toContainText('Last 7 days');
-
- const url = new URL(page.url());
- expect(url.searchParams.get('key')).toBe('last7days');
+ await expect(dateRangeTrigger(page)).toHaveText(label);
+ await expect(page).toHaveURL(
+ (url) =>
+ url.searchParams.get('startTs') === range.startTs &&
+ url.searchParams.get('endTs') === range.endTs
+ );
});
}
);
-/**
- * Incident Manager Date Field Sort Dropdown
- * @description Tests the date field sort dropdown (Created At / Updated At) on the Incidents tab.
- */
test.describe(
'Incident Manager - Date Field Sort Dropdown',
{ tag: `${DOMAIN_TAGS.OBSERVABILITY}:Incident_Manager` },
() => {
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
- const incidentListResponse = page.waitForResponse((response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- )
- );
+ const responsePromise = waitForIncidentList(page);
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
+ await responsePromise;
});
test('should show "Created At" as the default sort field label', async ({
page,
}) => {
- // The trigger button should show the default label
- const sortTrigger = page.locator('.sorting-dropdown');
- await expect(sortTrigger).toBeVisible();
- await expect(sortTrigger).toContainText('Created at');
+ await expect(
+ page.getByTestId('sort-field-dropdown-trigger')
+ ).toContainText('Created at');
});
test('should open sort field dropdown on click', async ({ page }) => {
- const sortTrigger = page.locator('.sorting-dropdown');
- await sortTrigger.click();
-
- // Dropdown menu should appear with both options
- await expect(
- page.getByRole('menuitemradio', { name: 'Created at' })
- ).toBeVisible();
- await expect(
- page.getByRole('menuitemradio', { name: 'Updated at' })
- ).toBeVisible();
+ const trigger = page.getByTestId('sort-field-dropdown-trigger');
+ await scrollIntoViewAndSettle(trigger);
+ await trigger.click();
+ const menu = page.getByTestId('drop-down-menu');
+ await expect(menu.getByTestId('timestamp')).toHaveText('Created at');
+ await expect(menu.getByTestId('updatedAt')).toHaveText('Updated at');
});
test('should switch to "Updated At" and call API with dateField=updatedAt', async ({
page,
}) => {
- const sortTrigger = page.locator('.sorting-dropdown');
- await sortTrigger.click();
-
- // Intercept the API call triggered by the selection change
- // Select Updated at
- const apiResponsePromise = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('dateField=updatedAt')
- );
- await page.getByRole('menuitemradio', { name: 'Updated at' }).click();
-
- const apiResponse = await apiResponsePromise;
- expect(apiResponse.status()).toBe(200);
-
- // Trigger should now show Updated at
- await expect(sortTrigger).toContainText('Updated at');
-
- // URL should contain dateField=updatedAt
- await expect(page).toHaveURL(/dateField=updatedAt/);
+ await selectDateField(page, 'updatedAt');
});
test('should switch back to "Created at" and call API with dateField=timestamp', async ({
page,
}) => {
- // First switch to Updated at
- const sortTrigger = page.locator('.sorting-dropdown');
- await sortTrigger.click();
- const updatedAtRes = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('dateField=updatedAt')
- );
- await page.getByRole('menuitemradio', { name: 'Updated at' }).click();
- await updatedAtRes;
-
- // Now switch back to Created at
- await sortTrigger.click();
- const createdAtRes = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('dateField=timestamp')
- );
- await page.getByRole('menuitemradio', { name: 'Created at' }).click();
- await createdAtRes;
-
- await expect(sortTrigger).toContainText('Created at');
- await expect(page).toHaveURL(/dateField=timestamp/);
+ await selectDateField(page, 'updatedAt');
+ await selectDateField(page, 'timestamp');
});
test('should close sort dropdown after selecting an option', async ({
page,
}) => {
- const sortTrigger = page.locator('.sorting-dropdown');
- await sortTrigger.click();
-
- // Menu should be visible
- await expect(
- page.getByRole('menuitemradio', { name: 'Updated at' })
- ).toBeVisible();
-
- // Select an option
- await page.getByRole('menuitemradio', { name: 'Updated at' }).click();
-
- // Menu should close
- await expect(
- page.getByRole('menuitemradio', { name: 'Updated at' })
- ).not.toBeVisible();
+ await selectDateField(page, 'updatedAt');
+ await expect(page.getByTestId('drop-down-menu')).not.toBeVisible();
});
}
);
-/**
- * Incident Manager Date Filter - Sidebar Navigation
- * @description Tests the date filter functionality when accessing Incident Manager from the sidebar.
- */
test.describe(
'Incident Manager Date Filter - Sidebar',
{ tag: `${DOMAIN_TAGS.OBSERVABILITY}:Incident_Manager` },
() => {
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
- const incidentListResponse = page.waitForResponse((response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- )
- );
+ const responsePromise = waitForIncidentList(page);
await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
- const response = await incidentListResponse;
- expect(response.status()).toBe(200);
+ await responsePromise;
});
test('Date picker shows placeholder by default on Incident Manager page', async ({
page,
}) => {
- const datePicker = page.getByTestId('date-picker-menu');
- await expect(datePicker).toBeVisible();
- await expect(datePicker).toContainText('Select Date');
-
- // Clear button should not be visible when no date is selected
- await expect(page.getByTestId('clear-date-picker')).not.toBeVisible();
+ await expectEmptyDateRange(page);
});
test('Select and clear date range on Incident Manager page', async ({
page,
}) => {
- const datePicker = page.getByTestId('date-picker-menu');
- await datePicker.click();
-
- // Select Last 7 days
- const dateFilterRes = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && response.url().includes('startTs')
- );
- await page.getByRole('menuitem', { name: 'Last 7 days' }).click();
- await dateFilterRes;
-
- await expect(datePicker).toContainText('Last 7 days');
- await expect(page).toHaveURL(/key=last7days/);
-
- // Clear button should be visible
- const clearButton = page.getByTestId('clear-date-picker');
- await expect(clearButton).toBeVisible();
-
- // Clear the selection
- const clearRes = page.waitForResponse(
- (response) =>
- response
- .url()
- .includes(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list'
- ) && !response.url().includes('startTs')
- );
- await clearButton.click();
- await clearRes;
-
- // Verify reset to placeholder
- await expect(datePicker).toContainText('Select Date');
- await expect(clearButton).not.toBeVisible();
-
- // Verify URL params removed
- const pageUrl = new URL(page.url());
- expect(pageUrl.searchParams.has('startTs')).toBeFalsy();
- expect(pageUrl.searchParams.has('key')).toBeFalsy();
+ await applyTodayRange(page);
+ await clearDateRange(page);
});
}
);
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 b224288c11d6..9ae4d6b3bf50 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
@@ -41,6 +41,7 @@ import { waitForTaskResolveResponse } from '../../utils/task';
import { verifyTestCaseLastRunBanner } from '../../utils/testCases';
import { clickAndWaitFor } from '../../utils/waitHelpers';
import { test } from '../fixtures/pages';
+import { disableAiAppMode, stubUserPreferencesAppMode } from '../Utils/appMode';
let user1: UserClass;
let user2: UserClass;
@@ -178,7 +179,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')
@@ -279,6 +280,9 @@ const reassignIncidentTask = async (
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.getByTestId('dialog-title').click();
+ await expect(assigneeOption).toBeHidden();
const updateAssignee = waitForTaskResolveResponse(page);
@@ -313,8 +317,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');
@@ -497,6 +502,8 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
});
test.beforeEach(async ({ page }) => {
+ await disableAiAppMode(page);
+ await stubUserPreferencesAppMode(page, 'classic');
await redirectToHomePage(page);
});
@@ -1099,13 +1106,9 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
testCaseFqn: table1.testCasesResponseData[2]?.['fullyQualifiedName'],
};
const testCase1 = table1.testCasesResponseData[0]?.['name'];
- const incidentDetailsRes = page.waitForResponse(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*'
- );
- await sidebarClick(page, SidebarItem.INCIDENT_MANAGER);
- await incidentDetailsRes;
-
const assignmentStartedAt = Date.now();
+ // assignIncident navigates to Incident Manager itself; a second sidebar
+ // click while already there lands on the half-open Observability submenu.
await assignIncident({
page,
testCaseName: assigneeTestCase.testCaseName,
@@ -1132,14 +1135,18 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
await afterAction();
}
- await page.click('[data-testid="select-assignee"] input');
- const assigneeOption = page.locator(
- `[data-testid="${assigneeTestCase.username}"]`
- );
+ await page.getByTestId('select-assignee').click();
+ await page
+ .getByTestId('select-owner-tabs')
+ .getByRole('tab', { name: /Users/ })
+ .click();
await page
- .getByTestId('select-assignee')
- .locator('input')
+ .getByTestId('owner-select-users-search-bar')
.fill(assigneeTestCase.userDisplayName);
+ const assigneeOption = page
+ .getByTestId('owner-select-users-panel')
+ .getByTestId('owner-option')
+ .filter({ hasText: assigneeTestCase.userDisplayName });
await expect(assigneeOption).toBeVisible();
const assigneeFilterRes = page.waitForResponse(
@@ -1153,13 +1160,20 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
assigneeTestCase.userDisplayName
);
- const nonAssigneeFilterRes = page.waitForResponse(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*'
+ await expect(page.getByTestId('select-assignee')).toContainText(
+ assigneeTestCase.userDisplayName
);
+ await page.getByTestId('select-assignee').click();
+ const nonAssigneeFilterRes = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ url.pathname.endsWith('/testCaseIncidentStatus/search/list') &&
+ !url.searchParams.has('assignee')
+ );
+ });
await page
- .getByTestId('select-assignee')
- .getByTestId('autocomplete-selected-item')
- .getByRole('button')
+ .getByTestId('owner-select-users-panel')
+ .getByTestId('remove-owner')
.click();
await nonAssigneeFilterRes;
@@ -1167,20 +1181,29 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
const statusFilterRes = page.waitForResponse(
'/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*testCaseResolutionStatusType=Assigned*'
);
- await page.click(`[title="Assigned"]`);
+ await page.getByTestId('drop-down-menu').getByTestId('Assigned').click();
await statusFilterRes;
await expectIncidentTableRowsToContain(page, 'Assigned');
- const nonStatusFilterRes = page.waitForResponse(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*'
- );
- await page.getByTestId('status-select').getByLabel('close-circle').click();
+ const nonStatusFilterRes = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ url.pathname.endsWith('/testCaseIncidentStatus/search/list') &&
+ !url.searchParams.has('testCaseResolutionStatusType')
+ );
+ });
+ await page.getByTestId('incident-clear-filters').click();
await nonStatusFilterRes;
await page.click('[data-testid="test-case-select"]');
- const testCaseOption = page.locator(`[title="${testCase1}"]`);
- await page.getByTestId('test-case-select').locator('input').fill(testCase1);
+ const testCaseOption = page
+ .getByTestId('drop-down-menu')
+ .getByTestId(table1.testCasesResponseData[0]?.['fullyQualifiedName']);
+ await page
+ .getByTestId('drop-down-menu')
+ .getByTestId('search-input')
+ .fill(testCase1);
await expect(testCaseOption).toBeVisible();
const testCaseFilterRes = page.waitForResponse(
@@ -1196,20 +1219,43 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
page.locator(`[data-testid="test-case-${testCase1}"]`)
).toBeVisible();
- const nonTestCaseFilterRes = page.waitForResponse(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*'
- );
- await page
- .getByTestId('test-case-select')
- .getByLabel('close-circle')
- .click();
+ const nonTestCaseFilterRes = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ url.pathname.endsWith('/testCaseIncidentStatus/search/list') &&
+ !url.searchParams.has('testCaseFQN')
+ );
+ });
+ await page.getByTestId('incident-clear-filters').click();
await nonTestCaseFilterRes;
- await page.getByTestId('date-picker-menu').click();
- const timeSeriesFilterRes = page.waitForResponse(
- '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*'
+ await page.getByTestId('sort-field-dropdown-trigger').click();
+ const dateFieldResponse = page.waitForResponse(
+ '/api/v1/dataQuality/testCases/testCaseIncidentStatus/search/list?*dateField=updatedAt*'
);
- await page.getByRole('menuitem', { name: 'Yesterday' }).click();
+ await page.getByTestId('drop-down-menu').getByTestId('updatedAt').click();
+ await dateFieldResponse;
+
+ await page
+ .getByRole('group', { name: 'Date Range', exact: true })
+ .getByRole('button')
+ .click();
+ await page
+ .getByRole('dialog')
+ .getByRole('button', { name: 'Today', exact: true })
+ .click();
+ const timeSeriesFilterRes = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ url.pathname.endsWith('/testCaseIncidentStatus/search/list') &&
+ url.searchParams.has('startTs') &&
+ url.searchParams.has('endTs')
+ );
+ });
+ await page
+ .getByRole('dialog')
+ .getByRole('button', { name: 'Apply', exact: true })
+ .click();
await timeSeriesFilterRes;
for (const testCase of table1.testCasesResponseData) {
@@ -1217,5 +1263,22 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
page.locator(`[data-testid="test-case-${testCase?.['name']}"]`)
).toBeVisible();
}
+ const clearFiltersResponse = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ url.pathname.endsWith('/testCaseIncidentStatus/search/list') &&
+ [
+ 'assignee',
+ 'testCaseFQN',
+ 'testCaseResolutionStatusType',
+ 'startTs',
+ 'endTs',
+ 'dateField',
+ ].every((param) => !url.searchParams.has(param))
+ );
+ });
+ await page.getByTestId('incident-clear-filters').click();
+ await clearFiltersResponse;
+ await expect(page.getByTestId('incident-clear-filters')).not.toBeVisible();
});
});
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/TaskCreation.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Tasks/TaskCreation.spec.ts
index de71f67fc954..c19d1985b0c8 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
@@ -428,9 +428,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();
@@ -442,8 +440,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..5c21e17c2a86 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.getByTestId('activity-feed-drawer');
if (await drawer.isVisible({ timeout: 5000 })) {
// Drawer should show task details
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Utils/appMode.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Utils/appMode.ts
index ecb61e82f033..4bf9d8134d9a 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Utils/appMode.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Utils/appMode.ts
@@ -44,7 +44,7 @@ const buildSessionTuple = (mode: string) =>
* cross-test leakage. The interceptor forwards the real request and mutates the
* response, so every other preference the server returns is preserved.
*/
-const stubUserPreferencesAppMode = async (
+export const stubUserPreferencesAppMode = async (
page: Page,
mode: 'ai' | 'classic'
): Promise => {
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs b/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs
index ecfa7313f635..58d106e5e2c9 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs
+++ b/openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs
@@ -50,9 +50,9 @@ test('the suppressions baseline matches its recorded state exactly', () => {
// the profile-menu helper no longer hovers the first avatar, the Glossary
// language switch targets the core language selector by test id, and the
// core assignee/schema-field selectors in the task and incident specs
- // remove three more. Merging main into this branch combines both sets of
- // removals, so the union is two below either side on its own.
- 'om-playwright/no-positional-locator': 592,
+ // remove three more. The merged baseline retains independent locator
+ // fixes from both branches.
+ 'om-playwright/no-positional-locator': 591,
'om-playwright/require-assertion-per-test': 1,
'playwright/no-skipped-test': 1,
'playwright/no-wait-for-selector': 14,
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 857e5a9b6cbf..fdc668957ba1 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/activityFeed.ts
@@ -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}`
);
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 54c6fa068d78..9c93315af25f 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts
@@ -587,14 +587,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..b8ef51498d66 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts
@@ -102,9 +102,7 @@ export const confirmationDragAndDropTeam = async (
dropTeam: string
) => {
// Confirmation message before the transfer
- await expect(
- page.locator('[data-testid="confirmation-modal"] .ant-modal-body')
- ).toContainText(
+ await expect(page.getByTestId('confirmation-modal')).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 437c442060b1..94ba983f974b 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts
@@ -1367,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 b7fbe6c9caca..7d97828d3d35 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,7 +190,7 @@ 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 assigneeInput = assigneeSelect.getByRole('combobox');
const assigneeOption = page.getByTestId(user.name).first();
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 82e65e92871a..f56a53954bd3 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts
@@ -185,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 147b10a56285..25faa92cace0 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,11 @@ 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';
+// Menu and combobox popovers are role="dialog" too; they carry data-trigger,
+// modal dialogs do not. Without the exclusion an open caret menu makes the
+// selector match two elements and every strict locator built on it throws.
+const VISIBLE_TASK_MODAL_SELECTOR =
+ '[role="dialog"]:not([data-trigger]):visible';
const logTaskDebug = (...messages: Array) => {
if (process.env.PW_TASK_DEBUG) {
@@ -64,7 +68,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,10 +97,38 @@ 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. Moving focus
+ // away is what closes it for good, but it has to land inside the modal: a
+ // click on the body (or a bare blur()) makes the dialog's focus containment
+ // hand focus back to the input, and focus re-opens the menu.
+ const containingDialog = page.locator(VISIBLE_TASK_MODAL_SELECTOR).filter({
+ has: page.getByTestId('tag-selector'),
+ });
+ if (await containingDialog.count()) {
+ // The open menu aria-hides the title, so locate its test ID.
+ await containingDialog.getByTestId('dialog-title').click();
+ } else {
+ await clickOutside(page);
+ }
+ await expect(tagsInput).toHaveAttribute('aria-expanded', 'false');
logTaskDebug('selectTagSuggestion:done', tagTestId);
};
+// The dialog is visible before its body has mounted, so a one-shot isVisible()
+// on the comment field races the mount and silently skips the required comment.
+const fillModalComment = async (modal: Locator, comment: string) => {
+ const commentInput = modal.locator('textarea').last();
+ const hasCommentField = await commentInput
+ .waitFor({ state: 'visible', timeout: 3000 })
+ .then(() => true)
+ .catch(() => false);
+
+ if (hasCommentField) {
+ await commentInput.fill(comment);
+ }
+};
+
const clickDropdownMenuItem = async ({
page,
menuPattern,
@@ -664,10 +696,7 @@ export const closeTaskFromDetails = async (page: Page) => {
.catch(() => undefined);
if (await visibleModal.isVisible().catch(() => false)) {
- const commentInput = visibleModal.locator('textarea').last();
- if (await commentInput.isVisible().catch(() => false)) {
- await commentInput.fill('Rejected by Playwright');
- }
+ await fillModalComment(visibleModal, 'Rejected by Playwright');
const rejectButton = visibleModal.getByRole('button', {
name: /reject|decline|close/i,
@@ -713,10 +742,7 @@ export const approveTaskFromDetails = async (page: Page) => {
.catch(() => undefined);
if (await visibleTaskModal.isVisible().catch(() => false)) {
- const commentInput = visibleTaskModal.locator('textarea').last();
- if (await commentInput.isVisible().catch(() => false)) {
- await commentInput.fill('Approved by Playwright');
- }
+ await fillModalComment(visibleTaskModal, 'Approved by Playwright');
const confirmButton = visibleTaskModal
.getByRole('button', { name: /approve|accept|ok|save/i })
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json b/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json
index 17ffe9cd4486..7c4c92bf8340 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json
+++ b/openmetadata-ui/src/main/resources/ui/scripts/bundle-boot-packages.json
@@ -62,7 +62,6 @@
"rc-drawer",
"rc-dropdown",
"rc-field-form",
- "rc-image",
"rc-input",
"rc-input-number",
"rc-menu",
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
index 407fdb7fe211..b0bc3adc10e7 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
@@ -128,6 +128,11 @@ const GROUPS = [
title: 'Legacy colors',
note: 'Exact migrated one-offs (migration debt). Do not use in new code; re-express with a semantic token.',
},
+ {
+ key: /^--om-/,
+ title: 'Legacy LESS aliases',
+ note: 'Existing LESS palette names retain their light fallbacks; app.less supplies dark-mode overrides. Prefer semantic tokens for new work.',
+ },
];
function classify(name) {
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
index 21c58cf6d3b5..a764b167bffe 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
@@ -15,10 +15,9 @@
/**
* Regenerates the generated block of `src/styles/tokens.css`.
*
- * Scans every CSS/LESS file once (without writing) so the token map records
- * which palette / legacy / off-scale / z-index / duration tokens the codebase
- * actually references, then writes the full upstream palette plus exactly those
- * project tokens between the @tokens:generated markers.
+ * Scans CSS/LESS values and token usages in styles and JavaScript/TypeScript,
+ * then writes the full upstream palette plus exactly those project tokens
+ * between the @tokens:generated markers.
*
* Run after `token-migrate.js`, or any time raw values change.
*/
@@ -39,7 +38,8 @@ const SKIP_DIRS = new Set([
]);
const BEGIN = '/* @tokens:generated-begin';
const END = '/* @tokens:generated-end */';
-const OM_USAGE_RE = /var\(\s*(--om-[\w-]+)/g;
+const OM_USAGE_RE =
+ /var\(\s*(--om-[\w-]+)|tw:[\w:!/-]+\((?:[\w-]+:)?(--om-[\w-]+)/g;
function walk(dir, acc) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -48,7 +48,7 @@ function walk(dir, acc) {
if (!SKIP_DIRS.has(entry.name)) {
walk(full, acc);
}
- } else if (/\.(less|css)$/.test(entry.name)) {
+ } else if (/\.(less|css|tsx?|jsx?)$/.test(entry.name)) {
acc.push(full);
}
}
@@ -82,11 +82,13 @@ function populateRegistry() {
const usageNames = new Set();
for (const file of files) {
const text = fs.readFileSync(file, 'utf8');
- processText(text, path.relative(STYLES_ROOT, file));
+ if (/\.(less|css)$/.test(file)) {
+ processText(text, path.relative(STYLES_ROOT, file));
+ }
let m;
OM_USAGE_RE.lastIndex = 0;
while ((m = OM_USAGE_RE.exec(text))) {
- usageNames.add(m[1]);
+ usageNames.add(m[1] || m[2]);
}
}
const legacyValues = loadSidecar();
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.test.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.test.js
new file mode 100644
index 000000000000..d86b29fcd7c3
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.test.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+const assert = require('node:assert/strict');
+const { execFileSync } = require('node:child_process');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { test } = require('node:test');
+
+test('preserves tokens used only by TSX when regenerating styles', (t) => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'om-token-generation-'));
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
+
+ const scripts = path.join(root, 'scripts/design-tokens');
+ const styles = path.join(root, 'src/styles');
+ fs.cpSync(__dirname, scripts, { recursive: true });
+ fs.mkdirSync(styles, { recursive: true });
+ const tokensFile = path.join(styles, 'tokens.css');
+ fs.writeFileSync(
+ tokensFile,
+ '/* @tokens:generated-begin */\n:root {}\n/* @tokens:generated-end */\n'
+ );
+ fs.writeFileSync(
+ path.join(root, 'src/Consumer.tsx'),
+ ``
+ );
+
+ const generate = () =>
+ execFileSync(process.execPath, [path.join(scripts, 'gen-tokens.js')]);
+ generate();
+ const generated = fs.readFileSync(tokensFile, 'utf8');
+
+ assert.match(generated, /--om-legacy-color-f8f8f8:\s*#f8f8f8;/);
+ assert.match(generated, /--om-legacy-color-595959:\s*#595959;/);
+ assert.match(generated, /--om-legacy-color-f7f9fc:\s*#f7f9fc;/);
+ assert.doesNotMatch(generated, /#123456/);
+
+ generate();
+ assert.equal(fs.readFileSync(tokensFile, 'utf8'), generated);
+});
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/legacy-colors.json b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/legacy-colors.json
index db169692d97b..9b4f7399eb21 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/legacy-colors.json
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/legacy-colors.json
@@ -56,6 +56,7 @@
"--om-legacy-color-1677ff": "#1677ff",
"--om-legacy-color-1890ff": "#1890ff",
"--om-legacy-color-1d7c4d": "#1d7c4d",
+ "--om-legacy-color-1f242f": "#1f242f",
"--om-legacy-color-1f67e9": "#1f67e9",
"--om-legacy-color-203-36-49-0-059": "#cb24310f",
"--om-legacy-color-209-236-241-0-867": "#d1ecf1dd",
@@ -99,6 +100,7 @@
"--om-legacy-color-373e44": "#373e44",
"--om-legacy-color-3793ff": "#3793ff",
"--om-legacy-color-3ca2f4": "#3ca2f4",
+ "--om-legacy-color-3e4456": "#3e4456",
"--om-legacy-color-40-167-69-0-059": "#28a7450f",
"--om-legacy-color-40-167-69-0-302": "#28a7454d",
"--om-legacy-color-40a9ff": "#40a9ff",
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
index 457252802d77..6238bd94f5e7 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
@@ -243,7 +243,7 @@ test('uses shared dark interaction and feedback recipes', () => {
'--color-bg-primary_hover': '--alpha(theme(--color-white) / 6%)',
'--color-bg-secondary_hover': '--alpha(theme(--color-white) / 6%)',
'--color-bg-active': '--alpha(theme(--color-white) / 10%)',
- '--color-bg-brand-primary': '--alpha(theme(--color-brand-500) / 16%)',
+ '--color-bg-brand-primary': '--alpha(theme(--color-brand-500) / 14%)',
'--color-bg-error-primary': '--alpha(theme(--color-error-500) / 16%)',
'--color-bg-warning-primary': '--alpha(theme(--color-warning-500) / 16%)',
'--color-bg-success-primary': '--alpha(theme(--color-success-500) / 16%)',
diff --git a/openmetadata-ui/src/main/resources/ui/specs/components/badge.md b/openmetadata-ui/src/main/resources/ui/specs/components/badge.md
index 3a3be6160bcd..3c49d6066f25 100644
--- a/openmetadata-ui/src/main/resources/ui/specs/components/badge.md
+++ b/openmetadata-ui/src/main/resources/ui/specs/components/badge.md
@@ -7,7 +7,7 @@
| **Name** | Badge |
| **Category** | Base / status |
| **Status** | Stable |
-| **Styles** | [`src/styles/components/badge.less`](../../src/styles/components/badge.less) |
+| **Styles** | [`badge.style.less`](../../src/components/common/Badge/badge.style.less) |
| **Component** | `@openmetadata/ui-core-components` → `Badge`, `BadgeGroup` (new work); Ant Design `Badge` count (legacy) |
## Overview
@@ -34,8 +34,8 @@ Parts: **pill container** (tinted surface + outline), **label**, optional
## Tokens used
-The legacy `.less` still uses `@grey-*` LESS variables (no `--om-*` yet); the
-modern core-components Badge maps to these semantic tokens.
+The legacy badge styles use `@grey-*` LESS variables backed by project tokens;
+the modern core-components Badge uses semantic theme tokens.
| Part | Token |
| --- | --- |
diff --git a/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md b/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
index 57fd5809b57e..ac59eb8fc75a 100644
--- a/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
+++ b/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
@@ -4,9 +4,9 @@
Master map of every **project (`--om-*`) token** — the tokens components reference. Each references the matching upstream `globals.css` token (or holds a raw value) and resolves to the value shown. Full layering: [../README.md](../README.md).
-Total project tokens: **837**.
+Total project tokens: **834**.
-## Spacing (60)
+## Spacing (57)
padding / margin / gap. See foundations/spacing.md.
@@ -27,9 +27,7 @@ padding / margin / gap. See foundations/spacing.md.
| `--om-space-8` | `8px` |
| `--om-space-9` | `9px` |
| `--om-space-10` | `10px` |
-| `--om-space-11` | `11px` |
| `--om-space-12` | `12px` |
-| `--om-space-13` | `13px` |
| `--om-space-14` | `14px` |
| `--om-space-15` | `15px` |
| `--om-space-16` | `16px` |
@@ -54,7 +52,6 @@ padding / margin / gap. See foundations/spacing.md.
| `--om-space-46` | `46px` |
| `--om-space-48` | `48px` |
| `--om-space-50` | `50px` |
-| `--om-space-54` | `54px` |
| `--om-space-56` | `56px` |
| `--om-space-60` | `60px` |
| `--om-space-64` | `64px` |
@@ -81,7 +78,7 @@ Shared viewport and shell-derived dimensions.
| --- | --- |
| `--om-page-height` | `calc(100vh - var(--ant-navbar-height, 80px))` |
-## Radius (24)
+## Radius (23)
border-radius. See foundations/radius.md.
@@ -94,7 +91,6 @@ border-radius. See foundations/radius.md.
| `--om-radius-3xl` | `24px` |
| `--om-radius-5` | `5px` |
| `--om-radius-7` | `7px` |
-| `--om-radius-9` | `9px` |
| `--om-radius-10` | `10px` |
| `--om-radius-13` | `13px` |
| `--om-radius-14` | `14px` |
@@ -112,7 +108,7 @@ border-radius. See foundations/radius.md.
| `--om-radius-xl` | `12px` |
| `--om-radius-xs` | `2px` |
-## Font size (26)
+## Font size (25)
font-size. See foundations/typography.md.
@@ -128,7 +124,6 @@ font-size. See foundations/typography.md.
| `--om-font-size-15` | `15px` |
| `--om-font-size-15_2` | `15.2px` |
| `--om-font-size-22` | `22px` |
-| `--om-font-size-26` | `26px` |
| `--om-font-size-32` | `32px` |
| `--om-font-size-42` | `42px` |
| `--om-font-size-55` | `55px` |
@@ -196,7 +191,7 @@ box-shadow. See foundations/elevation.md.
0px 8px 8px -4px rgba(10, 13, 18, 0.03)` |
| `--om-shadow-xs` | `0px 1px 2px rgba(10, 13, 18, 0.05)` |
-## z-index (32)
+## z-index (30)
stacking. Prefer the semantic ladder for new work.
@@ -205,7 +200,6 @@ stacking. Prefer the semantic ladder for new work.
| `--om-z-0` | `0` |
| `--om-z-1` | `1` |
| `--om-z-2` | `2` |
-| `--om-z-3` | `3` |
| `--om-z-4` | `4` |
| `--om-z-5` | `5` |
| `--om-z-6` | `6` |
@@ -218,7 +212,6 @@ stacking. Prefer the semantic ladder for new work.
| `--om-z-900` | `900` |
| `--om-z-999` | `999` |
| `--om-z-1001` | `1001` |
-| `--om-z-1050` | `1050` |
| `--om-z-1500` | `1500` |
| `--om-z-2000` | `2000` |
| `--om-z-9001` | `9001` |
@@ -268,7 +261,7 @@ transition / animation. See foundations/motion.md.
| `--om-ease-out` | `cubic-bezier(0, 0, 0.2, 1)` |
| `--om-ease-standard` | `cubic-bezier(0.4, 0, 0.2, 1)` |
-## Semantic colors (71)
+## Semantic colors (72)
Prefer these — they adapt to dark mode.
@@ -284,6 +277,7 @@ Prefer these — they adapt to dark mode.
| `--om-color-bg-overlay-surface` | `#ffffff` |
| `--om-color-bg-page` | `#ffffff` |
| `--om-color-bg-primary` | `#ffffff` |
+| `--om-color-bg-primary-hover` | `#fafafa` |
| `--om-color-bg-quaternary` | `#e9eaeb` |
| `--om-color-bg-raised` | `#ffffff` |
| `--om-color-bg-secondary` | `#fafafa` |
@@ -712,19 +706,17 @@ Fixed swatches; do NOT adapt to dark mode. Prefer semantic tokens.
-## Legacy colors (221)
+## Legacy colors (191)
Exact migrated one-offs (migration debt). Do not use in new code; re-express with a semantic token.
-Show all 221
+Show all 191
| Token | Value |
| --- | --- |
| `--om-legacy-color-0-0-0-0` | `rgba(0, 0, 0, 0)` |
| `--om-legacy-color-0-0-0-0-1` | `rgba(0, 0, 0, 0.1)` |
-| `--om-legacy-color-0-0-0-0-02` | `#00000005` |
| `--om-legacy-color-0-0-0-0-2` | `rgba(0, 0, 0, 0.2)` |
-| `--om-legacy-color-0-0-0-0-3` | `rgba(0, 0, 0, 0.3)` |
| `--om-legacy-color-0-0-0-0-04` | `rgba(0, 0, 0, 0.04)` |
| `--om-legacy-color-0-0-0-0-05` | `rgba(0, 0, 0, 0.05)` |
| `--om-legacy-color-0-0-0-0-5` | `rgba(0, 0, 0, 0.5)` |
@@ -736,15 +728,13 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-0-0-0-0-13` | `rgba(0, 0, 0, 0.13)` |
| `--om-legacy-color-0-0-0-0-15` | `rgba(0, 0, 0, 0.15)` |
| `--om-legacy-color-0-0-0-0-25` | `rgba(0, 0, 0, 0.25)` |
-| `--om-legacy-color-0-0-0-0-059` | `#0000000f` |
| `--om-legacy-color-0-0-0-0-102` | `#0000001a` |
| `--om-legacy-color-0c5460` | `#0c5460` |
-| `--om-legacy-color-0f141b` | `#0f141b` |
-| `--om-legacy-color-1d7c4d` | `#1d7c4d` |
| `--om-legacy-color-1f67e9` | `#1f67e9` |
+| `--om-legacy-color-1f242f` | `#1f242f` |
| `--om-legacy-color-2eaadc` | `#2eaadc` |
| `--om-legacy-color-2f74eb` | `#2f74eb` |
-| `--om-legacy-color-3ca2f4` | `#3ca2f4` |
+| `--om-legacy-color-3e4456` | `#3e4456` |
| `--om-legacy-color-4a4a4a` | `#4a4a4a` |
| `--om-legacy-color-5b8def` | `#5b8def` |
| `--om-legacy-color-5e5c58` | `#5e5c58` |
@@ -755,7 +745,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-008a00` | `#008a00` |
| `--om-legacy-color-8c8c8c` | `#8c8c8c` |
| `--om-legacy-color-9-80-197-0-05` | `rgba(9, 80, 197, 0.05)` |
-| `--om-legacy-color-10-13-18-0-1` | `rgba(10, 13, 18, 0.1)` |
| `--om-legacy-color-10-13-18-0-04` | `rgba(10, 13, 18, 0.04)` |
| `--om-legacy-color-10-13-18-0-05` | `rgba(10, 13, 18, 0.05)` |
| `--om-legacy-color-10-13-18-0-08` | `rgba(10, 13, 18, 0.08)` |
@@ -778,7 +767,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-40-167-69-0-059` | `#28a7450f` |
| `--om-legacy-color-40-167-69-0-302` | `#28a7454d` |
| `--om-legacy-color-41-41-41-0-1` | `rgba(41, 41, 41, 0.1)` |
-| `--om-legacy-color-43a047` | `#43a047` |
| `--om-legacy-color-46-170-220-0-15` | `rgba(46, 170, 220, 0.15)` |
| `--om-legacy-color-48-98-212-0-15` | `rgba(48, 98, 212, 0.15)` |
| `--om-legacy-color-52c41a` | `#52c41a` |
@@ -792,7 +780,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-113-71-232-0-6` | `#7147e899` |
| `--om-legacy-color-113-71-232-0-35` | `rgba(113, 71, 232, 0.35)` |
| `--om-legacy-color-113-71-232-0-102` | `#7147e81a` |
-| `--om-legacy-color-117-117-117-0-2` | `#75757533` |
| `--om-legacy-color-117-117-117-0-3` | `rgba(117, 117, 117, 0.3)` |
| `--om-legacy-color-203-36-49-0-059` | `#cb24310f` |
| `--om-legacy-color-209-236-241-0-867` | `#d1ecf1dd` |
@@ -803,7 +790,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-240-175-44-0-051` | `#f0af2c0d` |
| `--om-legacy-color-245-245-245-0-2` | `rgba(245, 245, 245, 0.2)` |
| `--om-legacy-color-248-215-218-0-867` | `#f8d7dadd` |
-| `--om-legacy-color-255-76-59-0-2` | `#ff4c3b33` |
| `--om-legacy-color-255-76-59-0-102` | `#ff4c3b1a` |
| `--om-legacy-color-255-190-14-0-149` | `#ffbe0e26` |
| `--om-legacy-color-255-193-67-0-15` | `rgba(255, 193, 67, 0.15)` |
@@ -817,17 +803,14 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-349dea` | `#349dea` |
| `--om-legacy-color-356fe7` | `rgb(53, 111, 231)` |
| `--om-legacy-color-373e44` | `#373e44` |
-| `--om-legacy-color-555f6d` | `#555f6d` |
| `--om-legacy-color-721c24` | `#721c24` |
| `--om-legacy-color-0950c5` | `#0950c5` |
| `--om-legacy-color-1677ff` | `#1677ff` |
| `--om-legacy-color-1890ff` | `#1890ff` |
| `--om-legacy-color-2563eb` | `#2563eb` |
-| `--om-legacy-color-3062d4` | `#3062d4` |
| `--om-legacy-color-3793ff` | `#3793ff` |
| `--om-legacy-color-7558c1` | `#7558c1` |
| `--om-legacy-color-9933ff` | `#93f` |
-| `--om-legacy-color-21263c` | `#21263c` |
| `--om-legacy-color-37352f` | `#37352f` |
| `--om-legacy-color-52525b` | `#52525b` |
| `--om-legacy-color-52607a` | `#52607a` |
@@ -836,7 +819,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-111827` | `#111827` |
| `--om-legacy-color-155724` | `#155724` |
| `--om-legacy-color-344054` | `#344054` |
-| `--om-legacy-color-444444` | `#444` |
| `--om-legacy-color-515151` | `#515151` |
| `--om-legacy-color-595959` | `#595959` |
| `--om-legacy-color-666666` | `#666666` |
@@ -845,7 +827,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-777777` | `#777` |
| `--om-legacy-color-856404` | `#856404` |
| `--om-legacy-color-a9efc5` | `#a9efc5` |
-| `--om-legacy-color-a84c3d` | `#a84c3d` |
| `--om-legacy-color-afafc1` | `#afafc1` |
| `--om-legacy-color-b0e7d4` | `#b0e7d4` |
| `--om-legacy-color-b1b1b7` | `rgb(177, 177, 183)` |
@@ -859,9 +840,7 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-c9d7ff` | `#c9d7ff` |
| `--om-legacy-color-c45296` | `#c45296` |
| `--om-legacy-color-cb2431` | `#cb2431` |
-| `--om-legacy-color-cb2531` | `#cb2531` |
| `--om-legacy-color-cccccc` | `#cccccc` |
-| `--om-legacy-color-cfd6dd` | `#cfd6dd` |
| `--om-legacy-color-d4d4d8` | `#d4d4d8` |
| `--om-legacy-color-d5d6d9` | `#d5d6d9` |
| `--om-legacy-color-d9ceee` | `#d9ceee` |
@@ -869,11 +848,8 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-d93025` | `#d93025` |
| `--om-legacy-color-d99600` | `#d99600` |
| `--om-legacy-color-dadada` | `#dadada` |
-| `--om-legacy-color-dadde6` | `#dadde6` |
-| `--om-legacy-color-dbe0e7` | `#dbe0e7` |
| `--om-legacy-color-dbe4ff` | `#dbe4ff` |
| `--om-legacy-color-dce3ec` | `rgb(220, 227, 236)` |
-| `--om-legacy-color-dcf6ff` | `#dcf6ff` |
| `--om-legacy-color-dddddd` | `#ddd` |
| `--om-legacy-color-dde3ea` | `#dde3ea` |
| `--om-legacy-color-e1e4e8` | `#e1e4e8` |
@@ -885,7 +861,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-e6fffb` | `#e6fffb` |
| `--om-legacy-color-e8e8e8` | `#e8e8e8` |
| `--om-legacy-color-e8e8e9` | `#e8e8e9` |
-| `--om-legacy-color-e8e8ed` | `#e8e8ed` |
| `--om-legacy-color-e8ecf5` | `#e8ecf5` |
| `--om-legacy-color-e60000` | `#e60000` |
| `--om-legacy-color-eaecf0` | `#eaecf0` |
@@ -895,20 +870,16 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-ef6820` | `#ef6820` |
| `--om-legacy-color-efbdc1` | `#efbdc1` |
| `--om-legacy-color-f0f0f0` | `#f0f0f0` |
-| `--om-legacy-color-f0f1f3` | `#f0f1f3` |
| `--om-legacy-color-f0f5ff` | `#f0f5ff` |
| `--om-legacy-color-f1edfd` | `#f1edfd` |
| `--om-legacy-color-f1f1f1` | `#f1f1f1` |
| `--om-legacy-color-f1f5ff` | `#f1f5ff` |
-| `--om-legacy-color-f3f3f3` | `#f3f3f3` |
| `--om-legacy-color-f3f7fd` | `#f3f7fd` |
| `--om-legacy-color-f3f8ff` | `#f3f8ff` |
| `--om-legacy-color-f4f0fd` | `#f4f0fd` |
| `--om-legacy-color-f4f7f9` | `#f4f7f9` |
| `--om-legacy-color-f4f7ff` | `#f4f7ff` |
-| `--om-legacy-color-f4fbf7` | `#f4fbf7` |
| `--om-legacy-color-f5c6cb` | `#f5c6cb` |
-| `--om-legacy-color-f5f7f9` | `#f5f7f9` |
| `--om-legacy-color-f5f7fb` | `#f5f7fb` |
| `--om-legacy-color-f5f8fa` | `#f5f8fa` |
| `--om-legacy-color-f6f6f6` | `#f6f6f6` |
@@ -916,30 +887,63 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-f7f9fc` | `#f7f9fc` |
| `--om-legacy-color-f7fbf9` | `#f7fbf9` |
| `--om-legacy-color-f8f8f8` | `#f8f8f8` |
-| `--om-legacy-color-f8f8fa` | `#f8f8fa` |
| `--om-legacy-color-f8f9fa` | `#f8f9fa` |
| `--om-legacy-color-f8faff` | `#f8faff` |
| `--om-legacy-color-f9dbaf` | `#f9dbaf` |
| `--om-legacy-color-f9f5ff` | `#f9f5ff` |
-| `--om-legacy-color-f9fafc` | `#f9fafc` |
| `--om-legacy-color-f9816c` | `#f9816c` |
| `--om-legacy-color-f44336` | `#f44336` |
-| `--om-legacy-color-f59638` | `#f59638` |
| `--om-legacy-color-fbf8f8` | `#fbf8f8` |
| `--om-legacy-color-fbfaf9` | `#fbfaf9` |
| `--om-legacy-color-fbfcff` | `#fbfcff` |
-| `--om-legacy-color-fcf0f1` | `#fcf0f1` |
| `--om-legacy-color-fec0ac` | `#fec0ac` |
| `--om-legacy-color-fee39c` | `#fee39c` |
| `--om-legacy-color-fef6ee` | `#fef6ee` |
-| `--om-legacy-color-ff4e27` | `#ff4e27` |
-| `--om-legacy-color-ff7c50` | `#ff7c50` |
| `--om-legacy-color-ff9900` | `#f90` |
| `--om-legacy-color-ffab2a` | `#ffab2a` |
| `--om-legacy-color-ffe1a6` | `#ffe1a6` |
| `--om-legacy-color-ffeeba` | `#ffeeba` |
| `--om-legacy-color-fff3dc` | `#fff3dc` |
-| `--om-legacy-color-fff5eb` | `#fff5eb` |
| `--om-legacy-color-ffff00` | `#ff0` |
+
+## Legacy LESS aliases (33)
+
+Existing LESS palette names retain their light fallbacks; app.less supplies dark-mode overrides. Prefer semantic tokens for new work.
+
+| Token | Value |
+| --- | --- |
+| `--om-alert-error-icon-bg-1` | `#fee4e2` |
+| `--om-alert-warning-icon` | `#dc6803` |
+| `--om-alert-warning-icon-bg-1` | `#fef0c7` |
+| `--om-blue-13` | `#e0f2fe` |
+| `--om-blue-22` | `#f0f9ff` |
+| `--om-blue-29` | `#eff4fa` |
+| `--om-blue-30` | `#d2dbeb` |
+| `--om-blue-500` | `#0ba5ec` |
+| `--om-border-color-base` | `#d9d9d9` |
+| `--om-green-9` | `#ecfdf3` |
+| `--om-green-10` | `#027a48` |
+| `--om-green-12` | `#d1fadf` |
+| `--om-green-100` | `#dcfae6` |
+| `--om-green-500` | `#17b26a` |
+| `--om-grey-1` | `#f8f8f8` |
+| `--om-grey-2` | `#f2f2f2` |
+| `--om-grey-5` | `#fbfbfb` |
+| `--om-grey-6` | `#f9f9f9` |
+| `--om-grey-7` | `#9ca3af` |
+| `--om-grey-9` | `#f8f9fc` |
+| `--om-grey-15` | `#eaecf5` |
+| `--om-grey-25` | `#fdfdfd` |
+| `--om-grey-26` | `#fcfcfd` |
+| `--om-grey-38` | `#f2f3f9` |
+| `--om-grey-50` | `#fafafa` |
+| `--om-heading-color` | `rgba(0, 0, 0, 0.85)` |
+| `--om-purple-1` | `#f2edfd` |
+| `--om-red-9` | `#fef3f2` |
+| `--om-red-10` | `#b42318` |
+| `--om-red-22` | `#f04438` |
+| `--om-warning-100` | `#fef0c7` |
+| `--om-warning-400` | `#fdb022` |
+| `--om-yellow-10` | `#fffaeb` |
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 9452972c567e..24a03ca427cb 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
@@ -13,11 +13,12 @@
import {
Box,
Button,
+ Card,
+ Divider,
SkeletonParagraph,
Tooltip,
Typography,
} from '@openmetadata/ui-core-components';
-import { Card } from 'antd';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined, orderBy } from 'lodash';
@@ -64,9 +65,10 @@ const ActivityFeedEditorNew = withSuspenseFallback(
const POST_SKELETON_KEYS = ['first', 'second', 'third'];
-// Container threads top-align the avatar with the multi-line header.
-const getHeaderAlign = (showThread?: boolean, entityType?: string) =>
- showThread && entityType === EntityType.CONTAINER ? 'start' : 'center';
+// 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;
@@ -107,6 +109,8 @@ const getHeaderTagsClassName = (
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,
});
const computeFeedId = (feed?: Conversation, activity?: ActivityEvent): string =>
@@ -400,10 +404,29 @@ const ActivityFeedCardNew = ({
return isPost ? post?.message ?? '' : feed?.message ?? '';
}, [isActivityEvent, activity, isPost, feed, post]);
+ const headerTags = useMemo(
+ () => (
+
+
+ {feedHeaderText}
+
+
+ {renderEntityLink}
+
+ ),
+ [showThread, entityRef?.type, feedHeaderText, renderEntityLink]
+ );
+
const renderWidgetCard = () => (
-
-
-
+
+
-
-
-
- {!isPost && (
-
-
- {feedHeaderText}
-
-
- {renderEntityLink}
-
- )}
-
+
+ {!isPost && headerTags}
+
)}
-
-
-
- {feedActions}
+
+
+ {feedActions}
+
);
@@ -531,7 +533,7 @@ const ActivityFeedCardNew = ({
);
const renderCommentsSection = () => (
-
+
{(showActivityFeedEditor || isOpenInDrawer) && (
{t('label.comment-plural')}
@@ -550,16 +552,16 @@ const ActivityFeedCardNew = ({
onSave={onSave}
/>
) : (
-
+
@@ -572,17 +574,20 @@ const ActivityFeedCardNew = ({
onPress={() => setShowFeedEditor(true)}>
{t('message.input-placeholder')}
-
+
)}
{posts}
-
+
);
+ const isContainerThread =
+ showThread && entityRef?.type === EntityType.CONTAINER;
+
const renderFullCard = () => (
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
- {getEntityName(user)}
-
-
-
- {timestamp}
+ itemClassName={SPACE_ITEM_CLASS_NAME}>
+
+
+
+ {getEntityName(user)}
+
+
+
+
+ {timestamp}
+
+
+ {!isPost && (
+ {headerTags}
+ )}
- {!isPost && (
-
-
- {feedHeaderText}
-
-
- {renderEntityLink}
-
- )}
-
-
-
setIsEditPost(false)}
- onUpdate={onUpdate}
- />
+
- {!isActivityEvent && feed && (
-
+ setIsEditPost(false)}
+ onUpdate={onUpdate}
/>
+
+
+ {!isActivityEvent && feed && (
+
+
+
)}
{isActivityEvent && activity && (
-
+
)}
-
- {(showThread || isOpenInDrawer) && renderCommentsSection()}
- {feedActions}
+ {(showThread || isOpenInDrawer) && renderCommentsSection()}
+ {feedActions}
+
);
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 e47749dea416..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
@@ -12,8 +12,6 @@
*/
import { Box } from '@openmetadata/ui-core-components';
-import { getLayoutGutter } from '../../../../../utils/common/layout.utils';
-
import { useMemo } from 'react';
import { ReactComponent as AddIcon } from '../../../../../assets/svg/added-icon.svg';
import { ReactComponent as UpdatedIcon } from '../../../../../assets/svg/updated-icon.svg';
@@ -46,17 +44,14 @@ 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 6bd5d970b6bb..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
@@ -13,13 +13,10 @@
import {
Box,
- Grid,
Owner,
OwnerChip,
Typography,
} from '@openmetadata/ui-core-components';
-import { getLayoutGutter } from '../../../../../utils/common/layout.utils';
-
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { useCallback, useMemo } from 'react';
@@ -103,7 +100,7 @@ function ActivityOwnersFeed({
const getOwnerItems = useCallback(
(ownerList: EntityReference[]) => {
return ownerList.length <= maxVisibleOwners ? (
-
+
{ownerList.map((owner: EntityReference) =>
owner.type === EntityType.USER ? (
@@ -161,39 +158,33 @@ function ActivityOwnersFeed({
);
return (
-
+
{!isEmpty(updatedOwner) && (
-
-
-
-
-
- {t('label.owner-plural-with-colon')}
-
-
-
- {renderUpdatedOwner}
+
+
+
+
+ {t('label.owner-plural-with-colon')}
+
-
+
+ {renderUpdatedOwner}
+
)}
{!isEmpty(previousOwner) && (
-
-
-
-
-
-
- {t('label.owner-plural-with-colon')}
-
-
+
+
+
+
+
+ {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 3bf28d2c9750..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,9 +11,7 @@
* limitations under the License.
*/
-import { Box, Grid } from '@openmetadata/ui-core-components';
-import { getLayoutGutter } from '../../../../../utils/common/layout.utils';
-
+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';
@@ -62,42 +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 eb2730f55b02..3257e66df51a 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,11 +11,12 @@
* limitations under the License.
*/
-import { Grid } from '@openmetadata/ui-core-components';
-import { Button } 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';
@@ -34,6 +35,7 @@ function ActivityEventFooter({
isForFeedTab = false,
onActivityClick,
}: Readonly) {
+ const { t } = useTranslation();
const { updateActivityReaction } = useActivityFeedProvider();
const onReactionUpdate = useCallback(
@@ -46,33 +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 54093b1eac9c..736124ea2521 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, Grid } from '@openmetadata/ui-core-components';
-import { Button } 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,26 +78,37 @@ 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 (
-
-
+
+ direction="col">
-
+
{postLength > 0 && !isReply && (
(
+
-
-
+
+
);
}
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..28be148f6685
--- /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(screen.getByTestId('activity-feed-drawer')).toContainElement(dialog);
+ 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 7fafe1b9f120..ca38e3fcd49e 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,12 +11,11 @@
* limitations under the License.
*/
-import { Grid } from '@openmetadata/ui-core-components';
-import { Drawer } 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 { getLayoutGutter } from '../../../utils/common/layout.utils';
import withSuspenseFallback from '../../AppRouter/withSuspenseFallback';
import ActivityPanelBody from '../ActivityFeedPanel/ActivityPanelBody';
import ActivityPanelHeader from '../ActivityFeedPanel/ActivityPanelHeader';
@@ -26,7 +25,9 @@ import TaskPanelHeader from '../ActivityFeedPanel/TaskPanelHeader';
import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider';
import './activity-feed-drawer.less';
-const ACTIVITY_FEED_DRAWER_CLASS = 'activity-feed-drawer';
+const PANEL_HEADER_CLASS_NAME =
+ 'tw:px-4 tw:shrink-0 tw:text-md tw:font-medium tw:leading-snug tw:text-(color:--om-heading-color)';
+const DRAWER_WIDTH = 576;
const TaskTabNew = withSuspenseFallback(
lazy(() =>
@@ -45,6 +46,7 @@ const ActivityFeedDrawer: FC = ({
open,
className,
}) => {
+ const { t } = useTranslation();
const { hideDrawer, selectedThread, selectedTask, selectedActivity } =
useActivityFeedProvider();
@@ -56,102 +58,82 @@ 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}
+
+ {panel.body}
+
+
);
};
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..b9d494523bb2 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,3 @@
color: @primary-heading-color;
}
}
-.activity-feed-drawer {
- .ant-drawer-body {
- padding: 0;
- }
- .ant-drawer-header {
- padding: 0;
- }
-}
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 a9dcbffa4a13..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 { Box, Typography } from '@openmetadata/ui-core-components';
-import { Button } 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,19 +45,8 @@ const TaskPanelHeader: FC
= ({
return (
-
-
+
+
{`#${getTaskDisplayId(task.taskId)} `}
{t(taskTypeLabel)}
@@ -65,12 +57,7 @@ const TaskPanelHeader: FC = ({
)}
-
+
= ({
{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 c349109f2829..e13fe0747c4e 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,7 +21,7 @@
flex-wrap: wrap;
width: inherit;
- &.ant-card {
+ &.activity-feed-card-new-surface {
background: var(--tw-color-bg-surface);
border-radius: var(--om-radius-xl);
margin-bottom: var(--om-space-20);
@@ -29,13 +29,9 @@
box-shadow: var(--om-shadow-card);
}
- .ant-card-body {
+ .activity-feed-card-new-body {
width: inherit;
padding: var(--om-space-20);
-
- :is(.ant-space-item, .layout-space-item) {
- width: inherit;
- }
}
.header-link {
word-wrap: break-word;
@@ -56,9 +52,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 be8f4197b713..9c8b63526410 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);
-
- :is(.ant-space-item, .layout-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);
-
- :is(.ant-space-item, .layout-space-item) {
- width: inherit;
- }
}
.activity-feed-reply-card-message {
@@ -235,6 +227,23 @@
}
}
+// 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;
}
@@ -280,7 +289,7 @@
.activity-feed-editor-send-btn {
color: var(--tw-color-text-white);
- .ant-btn.send-button {
+ .send-button {
position: absolute;
bottom: 8px;
right: 8px;
@@ -453,7 +462,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:is(.ant-space, .layout-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 90854b321c6c..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
@@ -11,7 +11,6 @@
* limitations under the License.
*/
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,69 +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:is(.ant-space, .layout-space),
-.reply-card:hover .feed-actions:is(.ant-space, .layout-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:is(.ant-space, .layout-space) {
+.feed-actions {
position: absolute;
right: 10px;
top: 24px;
@@ -57,7 +55,7 @@
color: @text-color;
}
-.feed-actions:is(.ant-space, .layout-space)[dir='rtl'] {
+.feed-actions[dir='rtl'] {
right: auto;
left: 10px;
}
@@ -74,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:is(.ant-space, .layout-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 20da6010b879..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,11 +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 } from '@openmetadata/ui-core-components';
+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, 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';
@@ -29,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(() =>
@@ -38,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);
@@ -53,33 +55,36 @@ 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}
/>
@@ -88,16 +93,14 @@ export const MathEquationComponent: FC = ({
)}
{/* 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 da2e9050c2ed..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
@@ -12,7 +12,6 @@
*/
import { Box, Typography } from '@openmetadata/ui-core-components';
import { SuggestionProps } from '@tiptap/suggestion';
-
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react';
@@ -107,25 +106,19 @@ export default forwardRef>(
return (
+ id="mention-viewport">
{items.map((item, index) => (
selectItem(index)}>
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 e2794252abac..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
@@ -12,7 +12,6 @@
*/
import { Box, Typography } from '@openmetadata/ui-core-components';
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
-import { Image } from 'antd';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react';
@@ -116,41 +115,29 @@ export const SlashCommandList = forwardRef(
return (
+ id="editor-commands-viewport">
{filteredItems.map((item, index) => (
selectItem(index)}>
-
-
+
{item.title}
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 de497e4acffb..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,14 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Box } from '@openmetadata/ui-core-components';
-import { Button } 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;
@@ -31,34 +30,35 @@ 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 286183f9e5a9..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,9 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Box } from '@openmetadata/ui-core-components';
+import { Box, ButtonUtility } from '@openmetadata/ui-core-components';
import type { Editor } from '@tiptap/react';
-import { Button, 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';
@@ -159,59 +158,56 @@ 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 009c40eabb57..ecb42cde1718 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
@@ -363,15 +363,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);
@@ -524,10 +515,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/BulkEditEntity/BulkEditEntity.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BulkEditEntity/BulkEditEntity.component.tsx
index 81ed5daffc58..7c00f2b6fc5a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/BulkEditEntity/BulkEditEntity.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/BulkEditEntity/BulkEditEntity.component.tsx
@@ -346,8 +346,11 @@ const BulkEditEntity = ({
Updating store will trigger re-render of the component
This will cause the owner dropdown or full grid to re-render
*/
- const editDataGrid = useMemo(() => {
- const dataSourceWithOperations: Record[] =
+ // react-data-grid closes an open cell editor, discarding its draft, as soon as
+ // the row object it is editing is replaced. Keep these identities stable
+ // across re-renders that do not change the data (columns, handlers, ...).
+ const dataSourceWithOperations = useMemo[]>(
+ () =>
filteredDataSource.map((row, index) => {
const rowId = row.id ?? `${index}`;
const originalRow = initialRowById.get(row.id);
@@ -363,7 +366,11 @@ const BulkEditEntity = ({
[BULK_EDIT_OPERATION_KEY]: operation,
[BULK_EDIT_ORIGINAL_NAME_KEY]: getBulkEditRowName(originalRow),
};
- });
+ }),
+ [filteredDataSource, initialRowById, changedCellKeysByRowId, workflowMode]
+ );
+
+ const editDataGrid = useMemo(() => {
const operationColumn: Column> = {
key: BULK_EDIT_OPERATION_KEY,
name: t('label.operation'),
@@ -507,18 +514,16 @@ const BulkEditEntity = ({
}, [
changedCellKeysByRowId,
columns,
- filteredDataSource,
+ dataSourceWithOperations,
handleCopy,
handlePaste,
handleOnRowsChange,
handleRemoveRow,
highlightedRowId,
- initialRowById,
isImportWorkflow,
bulkEditConfig,
setGridRef,
t,
- workflowMode,
]);
const operationSummary = useMemo(() => {
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..a8845be9bf01 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
+
);
}}
-
-
-
+
+
+
);
}
}, [
@@ -882,45 +900,34 @@ export const AddTestCaseList = ({
);
return (
-
-
-
-
-
-
-
+
+
+
{items.length > 0 && (
-
+
-
+
@@ -936,10 +943,9 @@ export const AddTestCaseList = ({
<>
|
+ onPress={handleSelectAllMatchingTotal}>
{t('label.select-all-count-test-cases', {
count: totalCount,
})}
@@ -947,25 +953,23 @@ export const AddTestCaseList = ({
>
)}
-
+
)}
{renderList}
{showButton && (
-
-
+
+
{cancelText ?? t('label.cancel')}
+ isLoading={isLoading}
+ onPress={handleSubmit}>
{submitText ?? t('label.create')}
-
+
)}
-
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseListFilters.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseListFilters.component.tsx
index 84b17d62e4ec..9c969feeb037 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseListFilters.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddTestCaseList/AddTestCaseListFilters.component.tsx
@@ -12,7 +12,6 @@
*/
import { Box, Typography } from '@openmetadata/ui-core-components';
-
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import FilterSelectDropdown from '../../common/FilterSelectDropdown/FilterSelectDropdown';
@@ -58,12 +57,7 @@ const AddTestCaseListFilters = ({
);
return (
-
+
{t('label.filter-plural')}:
{filtersToShow.map((filter) => (
= ({
{ label: string; value: string; suite: TestSuite }[]
>([]);
const [optionsLoading, setOptionsLoading] = useState(false);
+ const [inputValue, setInputValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const selectedIds = useMemo(
@@ -98,10 +98,33 @@ const AddToBundleSuiteModal: React.FC = ({
useEffect(() => {
if (open) {
setSelectedSuiteId(undefined);
+ setInputValue('');
fetchLogicalSuites('');
}
}, [open, fetchLogicalSuites]);
+ const suiteItems = useMemo(
+ () => options.map((option) => ({ id: option.value, label: option.label })),
+ [options]
+ );
+
+ const handleInputChange = (value: string) => {
+ setInputValue(value);
+ setSelectedSuiteId(undefined);
+ debouncedSearch(value);
+ };
+
+ const handleSelectionChange = (key: Key | null) => {
+ const suiteId = key === null ? undefined : String(key);
+ setSelectedSuiteId(suiteId);
+ if (suiteId) {
+ debouncedSearch.cancel();
+ setInputValue(
+ options.find((option) => option.value === suiteId)?.label ?? ''
+ );
+ }
+ };
+
const handleOk = async () => {
if (selectedIds.length === 0) {
onCancel();
@@ -150,24 +173,25 @@ const AddToBundleSuiteModal: React.FC = ({
title={t('label.add-test-cases-to-bundle-suite')}
onClose={onCancel}>
-
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.search.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.search.test.tsx
new file mode 100644
index 000000000000..90e49ebf21e6
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.search.test.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 { act, fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MemoryRouter } from 'react-router-dom';
+import { MOCK_TEST_CASE } from '../../../mocks/TestSuite.mock';
+import {
+ addTestCasesToLogicalTestSuiteBulk,
+ getListTestSuitesBySearch,
+} from '../../../rest/testAPI';
+import AddToBundleSuiteModal from './AddToBundleSuiteModal.component';
+
+jest.mock('../../../rest/testAPI', () => ({
+ getListTestSuitesBySearch: jest.fn(),
+ addTestCasesToLogicalTestSuiteBulk: jest.fn(),
+}));
+
+const initialSuite = { id: 'suite-1', name: 'Shared suite' };
+const serverMatch = { id: 'suite-16', name: 'Shared suite archive' };
+const mockGetSuites = getListTestSuitesBySearch as jest.MockedFunction<
+ typeof getListTestSuitesBySearch
+>;
+const mockAddTestCases =
+ addTestCasesToLogicalTestSuiteBulk as jest.MockedFunction<
+ typeof addTestCasesToLogicalTestSuiteBulk
+ >;
+let user: ReturnType
;
+
+const renderModal = () =>
+ render(
+
+
+
+ );
+
+describe('bundle suite search with the core combobox', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+ mockGetSuites.mockImplementation(async ({ q } = {}) => ({
+ data: q ? [initialSuite, serverMatch] : [initialSuite],
+ paging: { total: q ? 2 : 16 },
+ }));
+ mockAddTestCases.mockResolvedValue(initialSuite);
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('searches an exact loaded label and exposes additional server matches', async () => {
+ renderModal();
+ const input = screen.getByRole('combobox');
+ await act(async () => user.click(input));
+
+ expect(
+ await screen.findByRole('option', { name: initialSuite.name })
+ ).toBeInTheDocument();
+
+ fireEvent.change(input, { target: { value: initialSuite.name } });
+ await act(async () => jest.advanceTimersByTime(400));
+
+ expect(
+ await screen.findByRole('option', { name: serverMatch.name })
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('add-button')).toBeDisabled();
+ });
+
+ it('selects an option without searching its label and submits that suite', async () => {
+ renderModal();
+ await act(async () => user.click(screen.getByRole('combobox')));
+ const option = await screen.findByRole('option', {
+ name: initialSuite.name,
+ });
+ await act(async () => user.click(option));
+ await act(async () => jest.advanceTimersByTime(400));
+
+ expect(getListTestSuitesBySearch).toHaveBeenCalledTimes(1);
+ expect(screen.getByRole('combobox')).toHaveValue(initialSuite.name);
+ expect(screen.getByTestId('add-button')).toBeEnabled();
+
+ await act(async () => user.click(screen.getByTestId('add-button')));
+
+ expect(addTestCasesToLogicalTestSuiteBulk).toHaveBeenCalledWith(
+ initialSuite.id,
+ expect.objectContaining({ includeIds: [MOCK_TEST_CASE[0].id] })
+ );
+ });
+
+ it('clears the selected suite when the user edits its label', async () => {
+ renderModal();
+ const input = screen.getByRole('combobox');
+ await act(async () => user.click(input));
+ await act(async () =>
+ user.click(screen.getByRole('option', { name: initialSuite.name }))
+ );
+ fireEvent.change(input, { target: { value: 'Another suite' } });
+ await act(async () => jest.advanceTimersByTime(400));
+
+ expect(input).toHaveValue('Another suite');
+ expect(screen.getByTestId('add-button')).toBeDisabled();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.test.tsx
index 45753f46e232..29635c73e300 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddToBundleSuiteModal/AddToBundleSuiteModal.test.tsx
@@ -25,9 +25,9 @@ import observabilityRouterClassBase from '../../../utils/ObservabilityRouterClas
import AddToBundleSuiteModal from './AddToBundleSuiteModal.component';
import { AddToBundleSuiteModalProps } from './AddToBundleSuiteModal.interface';
-type MockSelectProps = {
- onChange?: (value: string | undefined) => void;
- onSearch?: (value: string) => void;
+type MockComboBoxProps = {
+ onInputChange?: (value: string) => void;
+ onSelectionChange?: (key: string | null) => void;
'data-testid'?: string;
};
@@ -75,24 +75,6 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
-jest.mock('antd', () => ({
- ...jest.requireActual('antd'),
- Select: ({ onChange, onSearch, 'data-testid': testId }: MockSelectProps) => (
-
- onSearch?.(e.target.value)}
- />
- onChange?.('suite-1')}>
- select-option
-
-
- ),
-}));
-
jest.mock('@openmetadata/ui-core-components', () => {
const MockDialogContent = ({ children }: { children: React.ReactNode }) => (
{children}
@@ -126,7 +108,35 @@ jest.mock('@openmetadata/ui-core-components', () => {
}
);
+ const MockSelect = Object.assign(() => null, {
+ ComboBox: ({
+ onInputChange,
+ onSelectionChange,
+ 'data-testid': testId,
+ }: MockComboBoxProps) => (
+
+ onInputChange?.(e.target.value)}
+ />
+ onSelectionChange?.('suite-1')}>
+ select-option
+
+ onSelectionChange?.(null)}>
+ clear-option
+
+
+ ),
+ Item: () => null,
+ });
+
return {
+ Select: MockSelect,
Button: ({
children,
onPress,
@@ -300,6 +310,18 @@ describe('AddToBundleSuiteModal', () => {
expect(mockNavigate).toHaveBeenCalled();
});
+ it('should disable add button again once the selection is cleared', async () => {
+ render();
+
+ fireEvent.click(await screen.findByTestId('bundle-suite-select-option'));
+
+ expect(screen.getByTestId('add-button')).not.toBeDisabled();
+
+ fireEvent.click(screen.getByTestId('bundle-suite-select-clear'));
+
+ expect(screen.getByTestId('add-button')).toBeDisabled();
+ });
+
it('should search bundle suites on input', async () => {
jest.useFakeTimers();
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/BundleSuiteForm/BundleSuiteForm.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/BundleSuiteForm/BundleSuiteForm.interface.ts
index e3eabce58b57..95694fe191c1 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/BundleSuiteForm/BundleSuiteForm.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/BundleSuiteForm/BundleSuiteForm.interface.ts
@@ -11,14 +11,12 @@
* limitations under the License.
*/
-import { DrawerProps } from 'antd';
import { ReactNode } from 'react';
import { TestCase } from '../../../generated/tests/testCase';
import { TestSuite } from '../../../generated/tests/testSuite';
import { AddTestCaseListChangePayload } from '../AddTestCaseList/AddTestCaseList.interface';
export interface BundleSuiteFormProps {
- drawerProps?: DrawerProps;
className?: string;
onCancel?: () => void;
onSuccess?: (testSuite: TestSuite) => void;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.component.tsx
index 12c4145a835d..d0e880754ee9 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.component.tsx
@@ -52,7 +52,7 @@ const Severity = ({
const onCancel = useCallback(() => setIsEditSeverity(false), []);
const handleSubmit = useCallback(
- async (data: Severities) => {
+ async (data?: Severities) => {
await onSubmit?.(data);
onCancel();
},
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.interface.ts
index 5f9ee18ecbbf..cbfcff881d7b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/Severity.interface.ts
@@ -28,7 +28,7 @@ export interface SeverityProps extends SeverityFormProps {
export interface SeverityModalProps {
initialSeverity?: Severities;
onCancel: () => void;
- onSubmit: (severity: Severities) => Promise;
+ onSubmit: (severity?: Severities) => Promise;
}
export interface InlineSeverityProps {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.component.tsx
index c68c9d98f931..7e94f3185fde 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.component.tsx
@@ -11,69 +11,76 @@
* limitations under the License.
*/
-import { Form, Modal, Select } from 'antd';
-import { FormProps, useForm } from 'antd/lib/form/Form';
-import { keys, startCase } from 'lodash';
+import { Form, Select, SimpleModal } from '@openmetadata/ui-core-components';
+import { startCase } from 'lodash';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Severities } from '../../../../generated/tests/testCaseResolutionStatus';
import { SeverityModalProps } from './Severity.interface';
+const NO_SEVERITY = 'none';
+
const SeverityModal = ({
initialSeverity,
onCancel,
onSubmit,
}: SeverityModalProps) => {
- const [form] = useForm();
const { t } = useTranslation();
+ const [severity, setSeverity] = useState(
+ initialSeverity
+ );
const [isLoading, setIsLoading] = useState(false);
- const handleFormSubmit: FormProps['onFinish'] = (data) => {
+ // The leading "No severity" item stands in for antd's allowClear.
+ const severityItems = [
+ {
+ id: NO_SEVERITY,
+ label: t('label.no-entity', { entity: t('label.severity') }),
+ },
+ ...Object.values(Severities).map((value) => ({
+ id: value,
+ label: startCase(value),
+ })),
+ ];
+
+ const handleSubmit = () => {
setIsLoading(true);
- onSubmit(data.severity).finally(() => {
+ onSubmit(severity).finally(() => {
setIsLoading(false);
});
};
return (
- form.submit()}>
-
-
-
+ onOk={handleSubmit}>
+
-
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.test.tsx
index b3cc19d1cf14..8fc97a15afb8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/Severity/SeverityModal.test.tsx
@@ -20,21 +20,47 @@ const mockProps = {
onSubmit: jest.fn().mockResolvedValue([]),
};
+const press = (element: HTMLElement) => {
+ fireEvent.pointerDown(element, {
+ button: 0,
+ pointerId: 1,
+ pointerType: 'mouse',
+ });
+ fireEvent.pointerUp(element, {
+ button: 0,
+ pointerId: 1,
+ pointerType: 'mouse',
+ });
+ fireEvent.click(element);
+};
+
+const pickSeverity = async (optionName: string) => {
+ await act(async () => {
+ press(screen.getByRole('button', { name: /label.severity/ }));
+ });
+ await act(async () => {
+ press(screen.getByRole('option', { name: optionName }));
+ });
+};
+
describe('SeverityModal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('Should render component', async () => {
render();
- const form = await screen.findByTestId('severity-form');
- const severitySelect = await screen.findByTestId('severity-form');
-
- expect(form).toBeInTheDocument();
- expect(severitySelect).toBeInTheDocument();
+ expect(await screen.findByTestId('severity-form')).toBeInTheDocument();
+ expect(await screen.findByTestId('severity-select')).toBeInTheDocument();
});
it('Initial value should be visible', async () => {
render();
- expect(await screen.findByText('Severity 1')).toBeInTheDocument();
+ expect(
+ await screen.findByRole('button', { name: /Severity 1/ })
+ ).toBeInTheDocument();
});
it('onCancel should work', async () => {
@@ -54,4 +80,31 @@ describe('SeverityModal', () => {
expect(mockProps.onSubmit).toHaveBeenCalledWith(Severities.Severity1);
});
+
+ it('should submit the newly picked severity', async () => {
+ render();
+
+ await pickSeverity('Severity 3');
+ await act(async () => {
+ fireEvent.click(screen.getByText('label.save'));
+ });
+
+ expect(mockProps.onSubmit).toHaveBeenCalledWith(Severities.Severity3);
+ });
+
+ it('should submit undefined once the severity is cleared', async () => {
+ render();
+
+ await pickSeverity('label.no-entity');
+
+ expect(
+ screen.getByRole('button', { name: /label.please-select-entity/ })
+ ).toBeInTheDocument();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('label.save'));
+ });
+
+ expect(mockProps.onSubmit).toHaveBeenCalledWith(undefined);
+ });
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component.tsx
index 3b45f2fe9f31..f779dca7b008 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseStatus/TestCaseIncidentManagerStatus.component.tsx
@@ -13,9 +13,9 @@
import {
Box,
+ Tooltip,
Typography as CoreTypography,
} from '@openmetadata/ui-core-components';
-import { Tooltip } from 'antd';
import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -107,14 +107,16 @@ const TestCaseIncidentManagerStatus = ({
/>
)}
-
+
+ gap={2}>
-
-
+ gap={2}>
+
+
+
+
{hasEditPermission && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCaseStatusModal/TestCaseStatusModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCaseStatusModal/TestCaseStatusModal.component.tsx
index 28dd4166b5d8..6f91c0b743d1 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCaseStatusModal/TestCaseStatusModal.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCaseStatusModal/TestCaseStatusModal.component.tsx
@@ -10,13 +10,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Form, Modal, Select } from 'antd';
+import {
+ Box,
+ Button,
+ FormField,
+ HintText,
+ HookForm,
+ Label,
+ Select,
+ SimpleModal,
+} from '@openmetadata/ui-core-components';
import { AxiosError } from 'axios';
import { startCase, unionBy } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { EntityType } from '../../../enums/entity.enum';
-import { CreateTestCaseResolutionStatus } from '../../../generated/api/tests/createTestCaseResolutionStatus';
import { TestCaseFailureReasonType } from '../../../generated/tests/resolved';
import { TestCaseResolutionStatusTypes } from '../../../generated/tests/testCaseResolutionStatus';
import Assignees from '../../../pages/TasksPage/shared/Assignees';
@@ -34,16 +43,21 @@ import {
} from '../../../utils/TaskAssigneeUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
-import {
- PAGE_SIZE_MEDIUM,
- VALIDATION_MESSAGES,
-} from '../../../constants/constants';
+import { PAGE_SIZE_MEDIUM } from '../../../constants/constants';
import { TEST_CASE_RESOLUTION_STATUS_LABELS } from '../../../constants/TestSuite.constant';
import { EntityReference } from '../../../generated/tests/testCase';
-import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface';
import { getUsers } from '../../../rest/userAPI';
-import { generateFormFields } from '../../../utils/formUtils';
-import { TestCaseStatusModalProps } from './TestCaseStatusModal.interface';
+import RichTextEditor from '../../common/RichTextEditor/RichTextEditor';
+import {
+ TestCaseStatusFormValues,
+ TestCaseStatusModalProps,
+} from './TestCaseStatusModal.interface';
+
+const FORM_ID = 'update-status-form';
+
+const FAILURE_REASON_ITEMS = Object.values(TestCaseFailureReasonType).map(
+ (value) => ({ id: value, label: startCase(value) })
+);
export const TestCaseStatusModal = ({
open,
@@ -53,7 +67,17 @@ export const TestCaseStatusModal = ({
onCancel,
}: TestCaseStatusModalProps) => {
const { t } = useTranslation();
- const [form] = Form.useForm();
+ const form = useForm({
+ defaultValues: {
+ testCaseResolutionStatusType: data?.testCaseResolutionStatusType,
+ testCaseResolutionStatusDetails: {
+ testCaseFailureReason:
+ data?.testCaseResolutionStatusDetails?.testCaseFailureReason,
+ testCaseFailureComment:
+ data?.testCaseResolutionStatusDetails?.testCaseFailureComment,
+ },
+ },
+ });
const [isLoading, setIsLoading] = useState(false);
const [options, setOptions] = useState