mirror of
https://github.com/apache/superset.git
synced 2026-05-13 11:55:16 +00:00
Compare commits
2 Commits
fix-flakey
...
ts6-migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0793a8e231 | ||
|
|
187bb416e7 |
@@ -95,8 +95,11 @@ class FakeMessageChannel {
|
||||
const port2 = new FakeMessagePort();
|
||||
port1.otherPort = port2;
|
||||
port2.otherPort = port1;
|
||||
this.port1 = port1;
|
||||
this.port2 = port2;
|
||||
// FakeMessagePort only implements the subset of MessagePort that
|
||||
// Switchboard exercises; cast at the boundary so the fake satisfies
|
||||
// the consumer signature without weakening the production type.
|
||||
this.port1 = port1 as unknown as MessagePort;
|
||||
this.port2 = port2 as unknown as MessagePort;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ function isError(message: Message): message is ErrorMessage {
|
||||
* Calling methods on the switchboard causes messages to be sent through the channel.
|
||||
*/
|
||||
export class Switchboard {
|
||||
port: MessagePort;
|
||||
port!: MessagePort;
|
||||
|
||||
name = '';
|
||||
|
||||
@@ -97,9 +97,9 @@ export class Switchboard {
|
||||
// used to make unique ids
|
||||
incrementor = 1;
|
||||
|
||||
debugMode: boolean;
|
||||
debugMode = false;
|
||||
|
||||
private isInitialised: boolean;
|
||||
private isInitialised = false;
|
||||
|
||||
constructor(params?: Params) {
|
||||
if (!params) {
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import { Button, Checkbox, Table } from '../core';
|
||||
|
||||
const BULK_SELECT_SELECTORS = {
|
||||
CONTROLS: '[data-test="bulk-select-controls"]',
|
||||
ACTION: '[data-test="bulk-select-action"]',
|
||||
HEADER_TOGGLE: '[data-test="header-toggle-all"]',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -57,17 +56,10 @@ export class BulkSelect {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables bulk selection mode by clicking the toggle button.
|
||||
*
|
||||
* Waits for the bulk-select column header checkbox to render so the next
|
||||
* row interaction does not race the table re-render that adds the
|
||||
* checkbox column.
|
||||
* Enables bulk selection mode by clicking the toggle button
|
||||
*/
|
||||
async enable(): Promise<void> {
|
||||
await this.getToggleButton().click();
|
||||
await this.page
|
||||
.locator(BULK_SELECT_SELECTORS.HEADER_TOGGLE)
|
||||
.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,27 +72,19 @@ export class BulkSelect {
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a row's checkbox in bulk select mode.
|
||||
* Asserts the checkbox is checked afterwards so any state-update race
|
||||
* surfaces here rather than as a missing bulk-action button later.
|
||||
* Selects a row's checkbox in bulk select mode
|
||||
* @param rowName - The name/text identifying the row to select
|
||||
*/
|
||||
async selectRow(rowName: string): Promise<void> {
|
||||
const checkbox = this.getRowCheckbox(rowName);
|
||||
await checkbox.check();
|
||||
await expect(checkbox.element).toBeChecked();
|
||||
await this.getRowCheckbox(rowName).check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects a row's checkbox in bulk select mode.
|
||||
* Mirrors selectRow: asserts the unchecked state so any lingering selection
|
||||
* surfaces here rather than as a stale bulk-action count later.
|
||||
* Deselects a row's checkbox in bulk select mode
|
||||
* @param rowName - The name/text identifying the row to deselect
|
||||
*/
|
||||
async deselectRow(rowName: string): Promise<void> {
|
||||
const checkbox = this.getRowCheckbox(rowName);
|
||||
await checkbox.uncheck();
|
||||
await expect(checkbox.element).not.toBeChecked();
|
||||
await this.getRowCheckbox(rowName).uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +107,10 @@ export class BulkSelect {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a bulk action button by name (e.g., "Export", "Delete").
|
||||
* Clicks a bulk action button by name (e.g., "Export", "Delete")
|
||||
* @param actionName - The name of the bulk action to click
|
||||
*/
|
||||
async clickAction(actionName: string): Promise<void> {
|
||||
const button = this.getActionButton(actionName);
|
||||
await button.click();
|
||||
await this.getActionButton(actionName).click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
import { Modal, Input } from '../core';
|
||||
|
||||
/**
|
||||
@@ -28,8 +27,7 @@ import { Modal, Input } from '../core';
|
||||
*/
|
||||
export class DeleteConfirmationModal extends Modal {
|
||||
private static readonly SELECTORS = {
|
||||
CONFIRMATION_INPUT: '[data-test="delete-modal-input"]',
|
||||
CONFIRM_BUTTON: '[data-test="modal-confirm-button"]',
|
||||
CONFIRMATION_INPUT: 'input[type="text"]',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,16 +36,12 @@ export class DeleteConfirmationModal extends Modal {
|
||||
private get confirmationInput(): Input {
|
||||
return new Input(
|
||||
this.page,
|
||||
this.element.locator(
|
||||
DeleteConfirmationModal.SELECTORS.CONFIRMATION_INPUT,
|
||||
),
|
||||
this.body.locator(DeleteConfirmationModal.SELECTORS.CONFIRMATION_INPUT),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the confirmation input with the specified text.
|
||||
* Waits for the input to be visible before filling so callers don't race
|
||||
* with the modal's open animation / focus effect.
|
||||
*
|
||||
* @param confirmationText - The text to type
|
||||
* @param options - Optional fill options (timeout, force)
|
||||
@@ -63,25 +57,11 @@ export class DeleteConfirmationModal extends Modal {
|
||||
confirmationText: string,
|
||||
options?: { timeout?: number; force?: boolean },
|
||||
): Promise<void> {
|
||||
await this.confirmationInput.element.waitFor({
|
||||
state: 'visible',
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
await this.confirmationInput.fill(confirmationText, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the Delete button in the footer.
|
||||
*
|
||||
* Targets the confirm button by data-test rather than going through
|
||||
* Modal.clickFooterButton, which finds buttons by their visible text. The
|
||||
* button label is i18n'd ("Delete" / "Supprimer" / …) so name-based lookups
|
||||
* break in non-English locales.
|
||||
*
|
||||
* Also waits for the button to become enabled before clicking: it is
|
||||
* disabled until the confirmation text matches "DELETE", and React's state
|
||||
* update from fillConfirmationInput is asynchronous, so an immediate click
|
||||
* can race the disabled→enabled transition.
|
||||
* Clicks the Delete button in the footer
|
||||
*
|
||||
* @param options - Optional click options (timeout, force, delay)
|
||||
*/
|
||||
@@ -90,10 +70,6 @@ export class DeleteConfirmationModal extends Modal {
|
||||
force?: boolean;
|
||||
delay?: number;
|
||||
}): Promise<void> {
|
||||
const confirmButton = this.element.locator(
|
||||
DeleteConfirmationModal.SELECTORS.CONFIRM_BUTTON,
|
||||
);
|
||||
await expect(confirmButton).toBeEnabled({ timeout: options?.timeout });
|
||||
await confirmButton.click(options);
|
||||
await this.clickFooterButton('Delete', options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
expectStatusOneOf,
|
||||
expectValidExportZip,
|
||||
} from '../../helpers/api/assertions';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Extend testWithAssets with chartListPage navigation (beforeEach equivalent).
|
||||
@@ -63,11 +62,8 @@ test('should delete a chart with confirmation', async ({
|
||||
await chartListPage.goto();
|
||||
await chartListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created chart appears.
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify chart is visible in list
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible();
|
||||
|
||||
// Click delete action button
|
||||
await chartListPage.clickDeleteAction(chartName);
|
||||
@@ -85,12 +81,12 @@ test('should delete a chart with confirmation', async ({
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify chart is removed from list (deleted rows leave the DOM)
|
||||
await expect(chartListPage.getChartRow(chartName)).toHaveCount(0);
|
||||
// Verify chart is removed from list
|
||||
await expect(chartListPage.getChartRow(chartName)).not.toBeVisible();
|
||||
|
||||
// Backend verification: API returns 404
|
||||
await expectDeleted(page, ENDPOINTS.CHART, chartId, {
|
||||
@@ -115,11 +111,8 @@ test('should edit chart name via properties modal', async ({
|
||||
await chartListPage.goto();
|
||||
await chartListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created chart appears.
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify chart is visible in list
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible();
|
||||
|
||||
// Click edit action to open properties modal
|
||||
await chartListPage.clickEditAction(chartName);
|
||||
@@ -144,7 +137,7 @@ test('should edit chart name via properties modal', async ({
|
||||
// Modal should close
|
||||
await propertiesModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
@@ -171,11 +164,8 @@ test('should export a chart as a zip file', async ({
|
||||
await chartListPage.goto();
|
||||
await chartListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created chart appears.
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify chart is visible in list
|
||||
await expect(chartListPage.getChartRow(chartName)).toBeVisible();
|
||||
|
||||
// Set up API response intercept for export endpoint
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.CHART_EXPORT);
|
||||
@@ -212,14 +202,9 @@ test('should bulk delete multiple charts', async ({
|
||||
await chartListPage.goto();
|
||||
await chartListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created charts appear.
|
||||
await expect(chartListPage.getChartRow(chart1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(chartListPage.getChartRow(chart2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both charts are visible in list
|
||||
await expect(chartListPage.getChartRow(chart1.name)).toBeVisible();
|
||||
await expect(chartListPage.getChartRow(chart2.name)).toBeVisible();
|
||||
|
||||
// Enable bulk select mode
|
||||
await chartListPage.clickBulkSelectButton();
|
||||
@@ -244,13 +229,13 @@ test('should bulk delete multiple charts', async ({
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify both charts are removed from list (deleted rows leave the DOM)
|
||||
await expect(chartListPage.getChartRow(chart1.name)).toHaveCount(0);
|
||||
await expect(chartListPage.getChartRow(chart2.name)).toHaveCount(0);
|
||||
// Verify both charts are removed from list
|
||||
await expect(chartListPage.getChartRow(chart1.name)).not.toBeVisible();
|
||||
await expect(chartListPage.getChartRow(chart2.name)).not.toBeVisible();
|
||||
|
||||
// Backend verification: Both return 404
|
||||
for (const chart of [chart1, chart2]) {
|
||||
@@ -274,11 +259,8 @@ test('should edit chart name from card view', async ({ page, testAssets }) => {
|
||||
await cardListPage.gotoCardView();
|
||||
await cardListPage.waitForCardLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created chart card appears.
|
||||
await expect(cardListPage.getChartCard(chartName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify chart card is visible
|
||||
await expect(cardListPage.getChartCard(chartName)).toBeVisible();
|
||||
|
||||
// Open card dropdown and click edit
|
||||
await cardListPage.clickCardEditAction(chartName);
|
||||
@@ -303,16 +285,13 @@ test('should edit chart name from card view', async ({ page, testAssets }) => {
|
||||
// Modal should close
|
||||
await propertiesModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify the renamed card appears in card view and old name is gone
|
||||
// (the old card name is removed from the DOM after the rename re-render).
|
||||
await expect(cardListPage.getChartCard(newName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(cardListPage.getChartCard(chartName)).toHaveCount(0);
|
||||
await expect(cardListPage.getChartCard(newName)).toBeVisible();
|
||||
await expect(cardListPage.getChartCard(chartName)).not.toBeVisible();
|
||||
|
||||
// Backend verification: API returns updated name
|
||||
const response = await apiGetChart(page, chartId);
|
||||
@@ -325,11 +304,6 @@ test('should bulk export multiple charts', async ({
|
||||
chartListPage,
|
||||
testAssets,
|
||||
}) => {
|
||||
// Chains create×2 → refresh → bulk select → export. Matches the
|
||||
// sibling bulk-delete test's budget so the export response wait below
|
||||
// can exceed the 30s default without hitting the test timeout.
|
||||
test.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
|
||||
// Create 2 throwaway charts for bulk export
|
||||
const [chart1, chart2] = await Promise.all([
|
||||
createTestChart(page, testAssets, test.info(), {
|
||||
@@ -344,14 +318,9 @@ test('should bulk export multiple charts', async ({
|
||||
await chartListPage.goto();
|
||||
await chartListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created charts appear.
|
||||
await expect(chartListPage.getChartRow(chart1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(chartListPage.getChartRow(chart2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both charts are visible in list
|
||||
await expect(chartListPage.getChartRow(chart1.name)).toBeVisible();
|
||||
await expect(chartListPage.getChartRow(chart2.name)).toBeVisible();
|
||||
|
||||
// Enable bulk select mode
|
||||
await chartListPage.clickBulkSelectButton();
|
||||
@@ -360,12 +329,8 @@ test('should bulk export multiple charts', async ({
|
||||
await chartListPage.selectChartCheckbox(chart1.name);
|
||||
await chartListPage.selectChartCheckbox(chart2.name);
|
||||
|
||||
// Set up API response intercept BEFORE the click that triggers it.
|
||||
// Exports of multiple charts can take longer than 30s under load,
|
||||
// so use SLOW_TEST instead of the default test-timeout-bound budget.
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.CHART_EXPORT, {
|
||||
timeout: TIMEOUT.SLOW_TEST,
|
||||
});
|
||||
// Set up API response intercept for export endpoint
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.CHART_EXPORT);
|
||||
|
||||
// Click bulk export action
|
||||
await chartListPage.clickBulkAction('Export');
|
||||
|
||||
@@ -68,34 +68,33 @@ test('should delete a dashboard with confirmation', async ({
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dashboard appears.
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dashboard is visible in list
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toBeVisible();
|
||||
|
||||
// Click delete action button
|
||||
await dashboardListPage.clickDeleteAction(dashboardName);
|
||||
|
||||
// Delete confirmation modal should appear
|
||||
const deleteModal = new DeleteConfirmationModal(page);
|
||||
await deleteModal.waitForReady();
|
||||
await deleteModal.waitForVisible();
|
||||
|
||||
// Type "DELETE" to confirm
|
||||
await deleteModal.fillConfirmationInput('DELETE');
|
||||
|
||||
// Click the Delete button (waits for it to become enabled)
|
||||
// Click the Delete button
|
||||
await deleteModal.clickDelete();
|
||||
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify dashboard is removed from list
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toHaveCount(0);
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboardName),
|
||||
).not.toBeVisible();
|
||||
|
||||
// Backend verification: API returns 404
|
||||
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboardId, {
|
||||
@@ -120,11 +119,8 @@ test('should export a dashboard as a zip file', async ({
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dashboard appears.
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dashboard is visible in list
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toBeVisible();
|
||||
|
||||
// Set up API response intercept for export endpoint
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.DASHBOARD_EXPORT);
|
||||
@@ -161,14 +157,13 @@ test('should bulk delete multiple dashboards', async ({
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dashboards appear.
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both dashboards are visible in list
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard1.name),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard2.name),
|
||||
).toBeVisible();
|
||||
|
||||
// Enable bulk select mode
|
||||
await dashboardListPage.clickBulkSelectButton();
|
||||
@@ -193,17 +188,17 @@ test('should bulk delete multiple dashboards', async ({
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify both dashboards are removed from list (deleted rows leave the DOM)
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard1.name)).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard2.name)).toHaveCount(
|
||||
0,
|
||||
);
|
||||
// Verify both dashboards are removed from list
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard1.name),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard2.name),
|
||||
).not.toBeVisible();
|
||||
|
||||
// Backend verification: Both return 404
|
||||
for (const dashboard of [dashboard1, dashboard2]) {
|
||||
@@ -218,11 +213,6 @@ test('should bulk export multiple dashboards', async ({
|
||||
dashboardListPage,
|
||||
testAssets,
|
||||
}) => {
|
||||
// Chains create×2 → refresh → bulk select → export. Matches the
|
||||
// sibling bulk-delete test's budget so the export response wait below
|
||||
// can exceed the 30s default without hitting the test timeout.
|
||||
test.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
|
||||
// Create 2 throwaway dashboards for bulk export
|
||||
const [dashboard1, dashboard2] = await Promise.all([
|
||||
createTestDashboard(page, testAssets, test.info(), {
|
||||
@@ -237,30 +227,25 @@ test('should bulk export multiple dashboards', async ({
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dashboards appear.
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(dashboardListPage.getDashboardRow(dashboard2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both dashboards are visible in list
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard1.name),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboard2.name),
|
||||
).toBeVisible();
|
||||
|
||||
// Enable bulk select mode (waits for the checkbox column to render)
|
||||
// Enable bulk select mode
|
||||
await dashboardListPage.clickBulkSelectButton();
|
||||
|
||||
// Select both dashboards (each call asserts the checkbox is checked)
|
||||
// Select both dashboards
|
||||
await dashboardListPage.selectDashboardCheckbox(dashboard1.name);
|
||||
await dashboardListPage.selectDashboardCheckbox(dashboard2.name);
|
||||
|
||||
// Set up API response intercept BEFORE the click that triggers it.
|
||||
// Exports of multiple dashboards can take longer than 30s under load,
|
||||
// so use SLOW_TEST instead of the default test-timeout-bound budget.
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.DASHBOARD_EXPORT, {
|
||||
timeout: TIMEOUT.SLOW_TEST,
|
||||
});
|
||||
// Set up API response intercept for export endpoint
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.DASHBOARD_EXPORT);
|
||||
|
||||
// Click bulk export action (waits for the action button to render)
|
||||
// Click bulk export action
|
||||
await dashboardListPage.clickBulkAction('Export');
|
||||
|
||||
// Wait for export API response and validate zip contains both dashboards
|
||||
@@ -308,12 +293,12 @@ test.describe('import dashboard', () => {
|
||||
label: `Dashboard ${dashboardId}`,
|
||||
});
|
||||
|
||||
// Refresh to confirm dashboard is no longer in the list (deleted rows leave the DOM)
|
||||
// Refresh to confirm dashboard is no longer in the list
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboardName),
|
||||
).not.toBeVisible();
|
||||
|
||||
// Click the import button
|
||||
await dashboardListPage.clickImportButton();
|
||||
@@ -365,7 +350,7 @@ test.describe('import dashboard', () => {
|
||||
// Modal should close on success
|
||||
await importModal.waitForHidden({ timeout: TIMEOUT.FILE_IMPORT });
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
@@ -373,11 +358,10 @@ test.describe('import dashboard', () => {
|
||||
await dashboardListPage.goto();
|
||||
await dashboardListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-imported dashboard appears.
|
||||
await expect(dashboardListPage.getDashboardRow(dashboardName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dashboard appears in list
|
||||
await expect(
|
||||
dashboardListPage.getDashboardRow(dashboardName),
|
||||
).toBeVisible();
|
||||
|
||||
// Track for cleanup: look up the reimported dashboard by title
|
||||
const reimported = await getDashboardByName(page, dashboardName);
|
||||
|
||||
@@ -107,11 +107,8 @@ test('should delete a dataset with confirmation', async ({
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dataset appears.
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dataset is visible in list
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
|
||||
|
||||
// Click delete action button
|
||||
await datasetListPage.clickDeleteAction(datasetName);
|
||||
@@ -129,13 +126,14 @@ test('should delete a dataset with confirmation', async ({
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears with correct message.
|
||||
// Verify success toast appears with correct message
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
const successToast = toast.getSuccess();
|
||||
await expect(successToast).toBeVisible();
|
||||
await expect(toast.getMessage()).toContainText('Deleted');
|
||||
|
||||
// Verify dataset is removed from list (deleted rows leave the DOM)
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toHaveCount(0);
|
||||
// Verify dataset is removed from list
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
|
||||
|
||||
// Verify via API that dataset no longer exists (404)
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, datasetId, {
|
||||
@@ -157,13 +155,10 @@ test('should duplicate a dataset with new name', async ({
|
||||
);
|
||||
const duplicateName = `duplicate_${Date.now()}_${test.info().parallelIndex}`;
|
||||
|
||||
// Navigate to list and verify original dataset is visible.
|
||||
// The list query is asynchronous; allow extra time on slow CI.
|
||||
// Navigate to list and verify original dataset is visible
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
await expect(datasetListPage.getDatasetRow(originalName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(datasetListPage.getDatasetRow(originalName)).toBeVisible();
|
||||
|
||||
// Set up response intercept to capture duplicate dataset ID
|
||||
const duplicateResponsePromise = waitForPost(
|
||||
@@ -206,14 +201,9 @@ test('should duplicate a dataset with new name', async ({
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// duplicate appears alongside the original.
|
||||
await expect(datasetListPage.getDatasetRow(originalName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(datasetListPage.getDatasetRow(duplicateName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both datasets exist in list
|
||||
await expect(datasetListPage.getDatasetRow(originalName)).toBeVisible();
|
||||
await expect(datasetListPage.getDatasetRow(duplicateName)).toBeVisible();
|
||||
|
||||
// API Verification: Fetch both datasets via detail API for consistent comparison
|
||||
// (list API may return undefined for fields that detail API returns as null)
|
||||
@@ -266,11 +256,6 @@ test('should export multiple datasets via bulk select action', async ({
|
||||
datasetListPage,
|
||||
testAssets,
|
||||
}) => {
|
||||
// Chains create×2 → refresh → bulk select → export. Matches the
|
||||
// sibling bulk-delete test's budget so the export response wait below
|
||||
// can exceed the 30s default without hitting the test timeout.
|
||||
test.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
|
||||
// Create 2 throwaway datasets for bulk export
|
||||
const [dataset1, dataset2] = await Promise.all([
|
||||
createTestDataset(page, testAssets, test.info(), {
|
||||
@@ -285,14 +270,9 @@ test('should export multiple datasets via bulk select action', async ({
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created datasets appear.
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both datasets are visible in list
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).toBeVisible();
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).toBeVisible();
|
||||
|
||||
// Enable bulk select mode
|
||||
await datasetListPage.clickBulkSelectButton();
|
||||
@@ -301,12 +281,8 @@ test('should export multiple datasets via bulk select action', async ({
|
||||
await datasetListPage.selectDatasetCheckbox(dataset1.name);
|
||||
await datasetListPage.selectDatasetCheckbox(dataset2.name);
|
||||
|
||||
// Set up API response intercept BEFORE the click that triggers it.
|
||||
// Exports of multiple datasets can take longer than 30s under load,
|
||||
// so use SLOW_TEST instead of the default test-timeout-bound budget.
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.DATASET_EXPORT, {
|
||||
timeout: TIMEOUT.SLOW_TEST,
|
||||
});
|
||||
// Set up API response intercept for export endpoint
|
||||
const exportResponsePromise = waitForGet(page, ENDPOINTS.DATASET_EXPORT);
|
||||
|
||||
// Click bulk export action
|
||||
await datasetListPage.clickBulkAction('Export');
|
||||
@@ -336,11 +312,8 @@ test('should edit dataset name via modal', async ({
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created dataset appears.
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dataset is visible in list
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
|
||||
|
||||
// Click edit action to open modal
|
||||
await datasetListPage.clickEditAction(datasetName);
|
||||
@@ -375,7 +348,7 @@ test('should edit dataset name via modal', async ({
|
||||
// Modal should close
|
||||
await editModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
@@ -404,14 +377,9 @@ test('should bulk delete multiple datasets', async ({
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-created datasets appear.
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify both datasets are visible in list
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).toBeVisible();
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).toBeVisible();
|
||||
|
||||
// Enable bulk select mode
|
||||
await datasetListPage.clickBulkSelectButton();
|
||||
@@ -436,13 +404,13 @@ test('should bulk delete multiple datasets', async ({
|
||||
// Modal should close
|
||||
await deleteModal.waitForHidden();
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible();
|
||||
|
||||
// Verify both datasets are removed from list (deleted rows leave the DOM)
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).toHaveCount(0);
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).toHaveCount(0);
|
||||
// Verify both datasets are removed from list
|
||||
await expect(datasetListPage.getDatasetRow(dataset1.name)).not.toBeVisible();
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).not.toBeVisible();
|
||||
|
||||
// Verify via API that datasets no longer exist (404)
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, dataset1.id, {
|
||||
@@ -487,10 +455,10 @@ test.describe('import dataset', () => {
|
||||
label: `Dataset ${datasetId}`,
|
||||
});
|
||||
|
||||
// Refresh to confirm dataset is no longer in the list (deleted rows leave the DOM)
|
||||
// Refresh to confirm dataset is no longer in the list
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toHaveCount(0);
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
|
||||
|
||||
// Click the import button
|
||||
await datasetListPage.clickImportButton();
|
||||
@@ -539,7 +507,7 @@ test.describe('import dataset', () => {
|
||||
// Modal should close on success
|
||||
await importModal.waitForHidden({ timeout: TIMEOUT.FILE_IMPORT });
|
||||
|
||||
// Verify success toast appears.
|
||||
// Verify success toast appears
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
@@ -547,11 +515,8 @@ test.describe('import dataset', () => {
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// The list query is asynchronous; allow extra time on slow CI before the
|
||||
// freshly-imported dataset appears.
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible({
|
||||
timeout: TIMEOUT.API_RESPONSE,
|
||||
});
|
||||
// Verify dataset appears in list
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
|
||||
|
||||
// Track for cleanup: the dataset import API returns {"message": "OK"}
|
||||
// with no ID, so look up the reimported dataset by name.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 { ValueGetterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import htmlTextFilterValueGetter, {
|
||||
htmlTextComparator,
|
||||
} from './htmlTextFilterValueGetter';
|
||||
|
||||
const makeParams = (value: unknown): ValueGetterParams =>
|
||||
({
|
||||
data: { foo: value },
|
||||
colDef: { field: 'foo' },
|
||||
}) as unknown as ValueGetterParams;
|
||||
|
||||
test('htmlTextFilterValueGetter extracts visible text from HTML anchor', () => {
|
||||
expect(
|
||||
htmlTextFilterValueGetter(
|
||||
makeParams(
|
||||
'<a href="https://jira.example.com/123/S18_3232">S18_3232</a>',
|
||||
),
|
||||
),
|
||||
).toBe('S18_3232');
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter strips nested HTML markup', () => {
|
||||
expect(
|
||||
htmlTextFilterValueGetter(
|
||||
makeParams('<div><strong>Hello</strong> <em>World</em></div>'),
|
||||
),
|
||||
).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter passes plain strings through', () => {
|
||||
expect(htmlTextFilterValueGetter(makeParams('plain value'))).toBe(
|
||||
'plain value',
|
||||
);
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter passes non-string values through', () => {
|
||||
expect(htmlTextFilterValueGetter(makeParams(42))).toBe(42);
|
||||
expect(htmlTextFilterValueGetter(makeParams(null))).toBeNull();
|
||||
expect(htmlTextFilterValueGetter(makeParams(undefined))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('htmlTextComparator orders by visible text, not raw HTML', () => {
|
||||
// URL prefixes (zzz vs bbb) would flip the order under raw-HTML sort,
|
||||
// but the visible labels (S700_4002 vs S72_3212) sort the other way.
|
||||
const left = '<a href="https://jira.example.com/zzz/S700_4002">S700_4002</a>';
|
||||
const right = '<a href="https://jira.example.com/bbb/S72_3212">S72_3212</a>';
|
||||
expect(htmlTextComparator(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test('htmlTextComparator handles nulls and numbers', () => {
|
||||
expect(htmlTextComparator(null, null)).toBe(0);
|
||||
expect(htmlTextComparator(null, 'x')).toBeLessThan(0);
|
||||
expect(htmlTextComparator('x', null)).toBeGreaterThan(0);
|
||||
expect(htmlTextComparator(1, 2)).toBeLessThan(0);
|
||||
expect(htmlTextComparator(2, 1)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('htmlTextComparator preserves default codepoint ordering for plain strings', () => {
|
||||
// AG Grid's default string comparator orders by codepoint, so 'Z' (90)
|
||||
// sorts before 'a' (97). A locale-aware comparator would flip this —
|
||||
// verify we match the default so plain string columns are unaffected.
|
||||
expect(htmlTextComparator('Z', 'a')).toBeLessThan(0);
|
||||
expect(htmlTextComparator('a', 'Z')).toBeGreaterThan(0);
|
||||
expect(htmlTextComparator('apple', 'banana')).toBeLessThan(0);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 { isProbablyHTML, sanitizeHtml } from '@superset-ui/core';
|
||||
import { ValueGetterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
|
||||
const stripHtmlToText = (html: string): string => {
|
||||
const doc = new DOMParser().parseFromString(sanitizeHtml(html), 'text/html');
|
||||
return (doc.body.textContent || '').trim();
|
||||
};
|
||||
|
||||
// Cache the comparator-ready form per raw string. Both the HTML-detection
|
||||
// step (`isProbablyHTML`, which itself invokes DOMParser for HTML-looking
|
||||
// values) and the extraction step (`stripHtmlToText`, also DOMParser) are
|
||||
// expensive; sort runs `O(n log n)` comparator calls against the same set
|
||||
// of cell values. Memoizing the combined detection + extraction means each
|
||||
// unique cell value pays the cost once per session. Module-level scope;
|
||||
// bounded by the count of unique string cell values seen.
|
||||
const comparableTextCache = new Map<string, string>();
|
||||
|
||||
const toComparableText = (raw: string): string => {
|
||||
const cached = comparableTextCache.get(raw);
|
||||
if (cached !== undefined) return cached;
|
||||
const normalized = isProbablyHTML(raw) ? stripHtmlToText(raw) : raw;
|
||||
comparableTextCache.set(raw, normalized);
|
||||
return normalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the visible-text representation of an HTML cell value so AG Grid
|
||||
* filters and sort operate on what the user sees, not the underlying markup.
|
||||
* Pass-through for non-HTML values.
|
||||
*/
|
||||
const htmlTextFilterValueGetter = (params: ValueGetterParams) => {
|
||||
const raw = params.data?.[params.colDef.field as string];
|
||||
return typeof raw === 'string' ? toComparableText(raw) : raw;
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparator that mirrors AG Grid's default string comparator (codepoint
|
||||
* order, nulls first), but extracts visible text from HTML values first
|
||||
* so HTML cells sort by their displayed label. Plain (non-HTML) values
|
||||
* pass through unchanged, preserving default ordering — e.g. 'Z' still
|
||||
* sorts before 'a' as it does under the default comparator.
|
||||
*/
|
||||
export const htmlTextComparator = (a: unknown, b: unknown): number => {
|
||||
const toText = (v: unknown) =>
|
||||
typeof v === 'string' ? toComparableText(v) : v;
|
||||
const aT = toText(a);
|
||||
const bT = toText(b);
|
||||
if (aT == null && bT == null) return 0;
|
||||
if (aT == null) return -1;
|
||||
if (bT == null) return 1;
|
||||
if (typeof aT === 'number' && typeof bT === 'number') return aT - bT;
|
||||
if (aT === bT) return 0;
|
||||
return aT < bT ? -1 : 1;
|
||||
};
|
||||
|
||||
export default htmlTextFilterValueGetter;
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
} from '../types';
|
||||
import getCellClass from './getCellClass';
|
||||
import filterValueGetter from './filterValueGetter';
|
||||
import htmlTextFilterValueGetter, {
|
||||
htmlTextComparator,
|
||||
} from './htmlTextFilterValueGetter';
|
||||
import dateFilterComparator from './dateFilterComparator';
|
||||
import DateWithFormatter from './DateWithFormatter';
|
||||
import { getAggFunc } from './getAggFunc';
|
||||
@@ -317,6 +320,24 @@ export const useColDefs = ({
|
||||
...(isPercentMetric && {
|
||||
filterValueGetter,
|
||||
}),
|
||||
...(dataType === GenericDataType.String &&
|
||||
!serverPagination && {
|
||||
// HTML cells (e.g. anchor markup) are rendered by TextCellRenderer
|
||||
// via dangerouslySetInnerHTML; without these the filter and sort
|
||||
// operate on raw HTML so the URL inside the markup dictates order
|
||||
// and the "Contains" filter matches against the raw HTML string.
|
||||
//
|
||||
// Gated on !serverPagination: in server-pagination mode sort and
|
||||
// filter are both delegated to the backend (which sees raw HTML
|
||||
// in the database), so applying the visible-text getter only on
|
||||
// the client would create a mismatch where the typed filter
|
||||
// value is stripped client-side but the server query still
|
||||
// operates on the raw HTML. Server-paginated tables with HTML
|
||||
// columns are out of scope for this fix and would require
|
||||
// server-side handling.
|
||||
filterValueGetter: htmlTextFilterValueGetter,
|
||||
comparator: htmlTextComparator,
|
||||
}),
|
||||
...(dataType === GenericDataType.Temporal && {
|
||||
// Use dateFilterValueGetter so AG Grid correctly identifies null dates for blank filter
|
||||
filterValueGetter: dateFilterValueGetter,
|
||||
|
||||
Reference in New Issue
Block a user