Compare commits

...

43 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
e44fdcadf1 chore(frontend): dedupe package-lock to hoist maplibre-gl
The merge of master into the React 18 branch regenerated the lockfile
via npm install, which once again left maplibre-gl only installed
inside the deck.gl and plugin-chart-point-cluster-map workspace
node_modules. Webpack imports @vis.gl/react-maplibre from the root,
so it could not resolve maplibre-gl and the production build failed
with two "Module not found: Can't resolve 'maplibre-gl'" errors,
breaking every cypress and playwright job on the PR.

Run npm dedupe so maplibre-gl is hoisted to the root node_modules,
matching how master resolves it. Same fix as 92d219ad34 — the
post-merge install regression keeps recurring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:02:35 +03:00
Mehmet Salih Yavuz
a81b8fc68a Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean 2026-04-29 20:36:26 +03:00
Mehmet Salih Yavuz
5b1dfdd85a test(css-templates): cover Edit/Add CSS template modal editor render
Adds Jest coverage that mounts CssTemplateModal in Edit, Add, and
closed->open transition flows and asserts the Ace editor element is
present. Investigation in response to a #38563 review comment ("only
the label, no css input field") could not reproduce the regression from
static analysis or in jsdom; this guards the editor from disappearing
under future renders/refactors regardless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:34:54 +03:00
Mehmet Salih Yavuz
1de8019f1c fix(explore): keep first keystroke after focus in editable title
Reported as: "Weird behaviour for dashboard name edit as sometimes you
can't delete or add any letters to it." When the input received focus
without going through the onClick handler first (tab focus, autofocus,
or a batched click+keystroke event group), the very first change event
fired with `isEditing` still false. The pre-existing guard
`if (!canEdit || !isEditing) return` in `handleChange` silently dropped
the keystroke. Because the input is controlled, antd's internal
`useMergedState` then re-synced the DOM value back to the stale
`props.value`, so the user saw their typed character or backspace
disappear — the symptom of an "unresponsive" input.

The guard was unnecessary: `disabled={!canEdit}` already prevents
changes when editing isn't allowed, so the only reason a change event
fires is that the user actually typed. Drop the `!isEditing` half of
the guard and flip `isEditing` on the first change, so a typed
keystroke both enters edit mode and is preserved.

Add a regression suite covering rapid type+backspace, the
focus-without-click case (the actual reproducer), and a re-render
mid-edit that should not clobber unsaved typing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:18:14 +03:00
Mehmet Salih Yavuz
d31a546653 fix(explore): eliminate type-only flicker in editable title
The dashboard/chart title flickered when typing letters but not when
deleting them. Root cause: the auto-grow width was driven by an
async `ResizeObserver` (via `useResizeDetector`) on the hidden sizer
span. After each keystroke React committed the new value with the
stale width, the browser painted that intermediate state, then the
ResizeObserver delivered a layout callback asynchronously and a
second commit corrected the width — visible as a flicker. Deleting
didn't flicker because the input never needs to shrink mid-edit.

Replace the async ResizeObserver on the sizer with a synchronous
measurement: read `offsetWidth` inside the same `useLayoutEffect`
that mirrors the value into the sizer. The width update now lands
in the same paint cycle as the value change.

Also fix two pre-existing dead-code paths exposed by this work:
the `sizerRef.current.setSelectionRange` cursor effect and the
`sizerRef.current?.input` overflow check both targeted a `<span>`
and were no-ops. Wire a proper `InputRef` to the actual input so
cursor positioning on edit and the overflow tooltip work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:14:16 +03:00
Mehmet Salih Yavuz
301610faca perf(explore): memoize ExploreChartHeader props and PageHeaderWithActions
Stabilize prop identity for the chart title editor so unrelated parent
re-renders don't bubble through the entire header subtree:

- ExploreChartHeader: wrap default export in memo; useMemo all props
  passed to PageHeaderWithActions (editableTitleProps, certificatied-
  BadgeProps, faveStarProps, titlePanelAdditionalItems, rightPanel-
  AdditionalItems, menuDropdownProps) instead of building fresh object
  literals on each render.
- PageHeaderWithActions: wrap in memo so it skips re-renders when its
  parent re-renders with stable props. The chart title commit path
  already keeps typed text in DynamicEditableTitle's local state and
  only dispatches on Enter/blur, but unstable parent prop identity was
  forcing the memoized DynamicEditableTitle subtree to re-render
  anyway.
- Header.test.tsx: add a test that asserts typing in the dashboard
  title only dispatches updateDashboardTitle/onChange once on commit,
  not per keystroke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:33:46 +03:00
Mehmet Salih Yavuz
8e3a61ef3d fix(frontend): restore node_modules exclusion for React Refresh
The dev build's `ReactRefreshWebpackPlugin` was passed `exclude: /service-worker/`,
which silently overrode the plugin's default `exclude: /node_modules/`. As a
result, pre-bundled ESM packages (e.g. react-checkbox-tree) had the refresh
loader injected into their nested webpack runtime, where `$Refresh$` is not
defined — producing `__webpack_require__.$Refresh$ is undefined` when loading a
dashboard via the legacy filterscope tree.

Pass both regexes as an array so the node_modules default is preserved
alongside the service-worker exclusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:43:18 +03:00
Mehmet Salih Yavuz
8f71ff2e78 style(dashboard): apply prettier formatting to state.test.ts
Wrap the multi-import line and the cast expression so the file matches
prettier's print width.
2026-04-27 12:28:26 +03:00
Mehmet Salih Yavuz
92d219ad34 chore(frontend): dedupe package-lock to hoist maplibre-gl
The merge regenerated the lockfile via npm install --legacy-peer-deps,
which left maplibre-gl only installed inside the deck.gl and
plugin-chart-point-cluster-map workspace node_modules. Webpack imports
@vis.gl/react-maplibre from the root, so it could not resolve
maplibre-gl and the production build failed with two
"Module not found: Can't resolve 'maplibre-gl'" errors.

Run npm dedupe so maplibre-gl is hoisted to the root node_modules,
matching how master resolves it.
2026-04-27 12:27:06 +03:00
Mehmet Salih Yavuz
3fb4c08966 test(dashboard): migrate state.test.ts off @testing-library/react-hooks
The state.test.ts file came in via a master merge but kept its
@testing-library/react-hooks import. That package is incompatible with
React 18 and is no longer installed in this branch. Switch to
renderHook from @testing-library/react and add an explicit
QueryObjectFilterClause cast so the implicit-any check passes.
2026-04-27 12:08:17 +03:00
Mehmet Salih Yavuz
7ba2ea65bf chore(frontend): sync package-lock.json with package.json
The previous merge commit regenerated package-lock.json with
--legacy-peer-deps, which silently omitted peer dependencies (react-ace,
@deck.gl/widgets, preact, nyc, @react-spring/*, @fontsource/inter, etc.)
from the lockfile. This caused npm ci to fail across CI (frontend-build,
docker-build, cypress, playwright, frontend-check-translations,
pre-commit) with EUSAGE: out-of-sync lockfile.

Regenerate the lockfile with default resolution so peer deps are
included.
2026-04-27 12:04:22 +03:00
Mehmet Salih Yavuz
c9a2b1d907 fix(modal): keep close button above sticky body content
Modal close button could be visually covered (and click-blocked) by
sticky-positioned body children that establish a higher stacking
context. The DatabaseModal's StyledStickyHeader uses
position: sticky with z-index: zIndexPopupBase (1000), which sits
on top of .ant-modal-close (no explicit z-index) once content scrolls.

Promote .ant-modal-close above zIndexPopupBase so the X stays
clickable when error alerts (or any content) push the modal body
into a scrollable state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:17:53 +03:00
Mehmet Salih Yavuz
eb67530d4a Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean
Conflicts:
- superset-frontend/package.json: kept React 18, took master's other deps
- superset-frontend/package-lock.json: regenerated with npm install --legacy-peer-deps
- superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.test.ts: replaced @testing-library/react-hooks with @testing-library/react act/waitFor (React 18 pattern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:57:05 +03:00
Mehmet Salih Yavuz
156af9a3e8 style: prettier format ColumnElement.test.tsx
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:26:58 +03:00
Mehmet Salih Yavuz
3efb2c5743 Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean
# Conflicts:
#	superset-frontend/package-lock.json
#	superset-frontend/package.json
2026-04-15 15:54:38 +03:00
Mehmet Salih Yavuz
556aeeb7b0 Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean
Resolved package.json conflict (kept React 18 type pins, took newer
@types/node from master). Regenerated package-lock.json. Also fixed
inherited test issues that surfaced under the merged state:

- ColumnElement.test.tsx: drop unused mockedActions prop (Record of
  jest.Mock isn't a valid ReactNode under the stricter actions type)
- DrillByModal.test.tsx: a prior merge had reintroduced pagination
  assertions that master removed when SingleQueryResultPane switched
  from TableView to GridTable (which uses ag-grid with no Next Page
  button). Realigned with master's expectations.
- useDownloadScreenshot.test.ts: migrate from removed
  @testing-library/react-hooks to renderHook from @testing-library/react

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:32:56 +03:00
Mehmet Salih Yavuz
8d110cd3db style: prettier format SqlEditor/index.tsx
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:12:01 +03:00
Mehmet Salih Yavuz
e3f3212644 fix(frontend): address review feedback on react 18 PR
- UserListModal: drop buggy onChange (isActive vs active field name
  mismatch); antd FormItem valuePropName binds checkbox automatically
- AgGridTable: allow null sortDir through handleColumnHeaderClick so
  clear-sort events are handled (was dropping null sortDir, leaving
  stale server sort state)
- QueryTable: add user|db to QueryTableQuery Omit list so JSX
  assignments to q.user/q.db satisfy the declared type
- TableView: skip client-side slicing when serverPagination=true
  (server already returns the current page)
- ReportModal: render cronError.description instead of casting the
  CronError object to ReactNode ([object Object] display bug)
- RoleListEditModal.test: replace no-op waitFor(() => Promise.resolve())
  with observable findByText waits on hydrated user labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:06:15 +03:00
Mehmet Salih Yavuz
9309b769dd chore: regenerate package-lock.json after master merge
Resolves npm ci failures causing pre-commit, frontend-build, playwright,
and cypress jobs to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:32:35 +03:00
Mehmet Salih Yavuz
fc6d783578 Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean 2026-04-14 13:59:09 +03:00
Mehmet Salih Yavuz
210d9dbdf2 fix: manage pagination state locally with useState 2026-03-14 00:13:32 +03:00
Mehmet Salih Yavuz
742af2f1d8 fix: format 2026-03-13 21:42:40 +03:00
Mehmet Salih Yavuz
ffe14568cc fix: types 2026-03-13 21:37:17 +03:00
Mehmet Salih Yavuz
e460120983 fix: pre-commit 2026-03-13 21:23:47 +03:00
Mehmet Salih Yavuz
f0c809bad2 Merge branch 'master' into msyavuz/chore/react-18-clean 2026-03-13 21:18:37 +03:00
Mehmet Salih Yavuz
0b2fca90ba fix: ci 2026-03-13 21:18:09 +03:00
Mehmet Salih Yavuz
28ae7f67dc fix: ci 2026-03-13 19:52:38 +03:00
Mehmet Salih Yavuz
55fca52ecf fix: lock 2026-03-13 18:30:44 +03:00
Mehmet Salih Yavuz
ab1b079818 Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:19:52 +03:00
Mehmet Salih Yavuz
6b0b16310e fix: ci 2026-03-13 18:12:30 +03:00
Mehmet Salih Yavuz
2f1b082cec fix: regenerate package-lock.json for npm ci compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:51:24 +03:00
Mehmet Salih Yavuz
85cb87e413 fix: DatabaseModal test timeout from async tabpanel query
The findByRole('tabpanel', {name: /advanced/i}) was hanging in React 18
because antd v5 doesn't label tabpanels the same way. Use synchronous
queries matching the pattern from adjacent passing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:21:20 +03:00
Mehmet Salih Yavuz
dd7a2f153a Merge remote-tracking branch 'origin/master' into msyavuz/chore/react-18-clean 2026-03-12 16:03:20 +03:00
Mehmet Salih Yavuz
2479f0fc18 fix: ci 2026-03-12 14:56:07 +03:00
Mehmet Salih Yavuz
8a7964dabd fix: ci 2026-03-12 14:56:02 +03:00
Mehmet Salih Yavuz
8b6f7b3583 fix: types 2026-03-11 14:03:51 +03:00
Mehmet Salih Yavuz
9b1b3b3e45 fix: testing library version 2026-03-11 12:21:21 +03:00
Evan Rusackas
353c222292 Merge branch 'master' into msyavuz/chore/react-18-clean 2026-03-10 19:16:17 -04:00
Mehmet Salih Yavuz
80cd613dc4 fix: ci 2026-03-11 00:16:32 +03:00
Mehmet Salih Yavuz
2360702292 fix: lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:08:16 +03:00
Mehmet Salih Yavuz
50aa59067a fix: lock 2026-03-10 22:50:00 +03:00
Mehmet Salih Yavuz
09b341f7c2 fix(frontend): resolve React 18 type errors
- Add children prop to components that lost implicit children (React.FC)
- Fix ReactNode type narrowing (Date, bigint, object, function types)
- Replace waitForNextUpdate with waitFor in renderHook tests (RTL 14)
- Add @ts-expect-error for react-dnd DndProvider children prop
- Forward ref in CustomListItem for dnd-kit sortable
- Fix icon prop types to use ReactNode
- Update ui-overrides root.context.provider type for children

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:54:36 +03:00
Mehmet Salih Yavuz
51b47f9400 chore(frontend): upgrade React 17 to React 18
- Bump react, react-dom to ^18.2.0
- Bump @types/react, @types/react-dom to ^18.2.0
- Migrate ReactDOM.render() to createRoot() in all entry points
- Upgrade @testing-library/react to ^14.0.0
- Remove @testing-library/react-hooks (merged into RTL 14)
- Migrate 54 test files from @testing-library/react-hooks imports
- Replace react-sortable-hoc with @dnd-kit/sortable (CollectionControl)
- Remove @cypress/react (incompatible with React 18, Cypress deprecated)
- Update all sub-package peerDependencies to ^18.2.0
- Migrate ChartLayer (cartodiagram) from ReactDOM.render to createRoot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:20:50 +03:00
182 changed files with 5058 additions and 7523 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -194,13 +194,13 @@
"pretty-ms": "^9.3.0",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -211,7 +211,6 @@
"react-reverse-portal": "^2.3.0",
"react-router-dom": "^5.3.4",
"react-search-input": "^0.11.3",
"react-sortable-hoc": "^2.0.0",
"react-split": "^2.0.9",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
@@ -251,7 +250,6 @@
"@babel/runtime": "^7.29.2",
"@babel/runtime-corejs3": "^7.29.2",
"@babel/types": "^7.28.6",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
@@ -273,10 +271,9 @@
"@swc/core": "^1.15.32",
"@swc/plugin-emotion": "^14.8.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -286,8 +283,8 @@
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",

View File

@@ -81,10 +81,9 @@
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.24",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -98,8 +97,8 @@
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",

View File

@@ -18,7 +18,7 @@
*/
import userEvent from '@testing-library/user-event';
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import '@testing-library/jest-dom';
import { themeObject } from './theme';
@@ -33,7 +33,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: Providers, ...options });
): RenderResult => render(ui, { wrapper: Providers, ...options });
export {
createEvent,

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { ThemeProvider } from '@emotion/react';
import { theme as antdTheme } from 'antd';
import {

View File

@@ -33,17 +33,16 @@
"@ant-design/icons": "^5.6.1",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^17.0.2",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^17.0.2"
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -91,10 +91,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -102,8 +101,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},

View File

@@ -17,19 +17,15 @@
* under the License.
*/
import type { ReactElement } from 'react';
import {
Tooltip,
type TooltipPlacement,
type IconType,
} from '@superset-ui/core/components';
import type { ReactElement, ReactNode } from 'react';
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
export interface ActionProps {
label: string;
tooltip?: string | ReactElement;
placement?: TooltipPlacement;
icon: IconType;
icon: ReactNode;
onClick: () => void;
}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {

View File

@@ -16,16 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
useEffect,
useState,
RefObject,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from 'react';
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
import { Loading } from '../Loading';
import type { PlaceholderProps } from './types';
@@ -93,15 +84,16 @@ export function AsyncEsmComponent<
return promise;
}
type AsyncComponent = ForwardRefExoticComponent<
PropsWithoutRef<FullProps> & RefAttributes<ComponentType<FullProps>>
type AsyncComponent = React.ForwardRefExoticComponent<
React.PropsWithoutRef<FullProps> & React.RefAttributes<unknown>
> & {
preload?: typeof waitForPromise;
};
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref: RefObject<ComponentType<FullProps>>,
ref,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -24,7 +24,6 @@ import type {
ButtonVariantType,
ButtonColorType,
} from 'antd/es/button';
import { IconType } from '@superset-ui/core/components/Icons/types';
import type { TooltipPlacement } from '../Tooltip/types';
export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
@@ -49,5 +48,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
buttonStyle?: ButtonStyle;
cta?: boolean;
showMarginRight?: boolean;
icon?: IconType;
icon?: ReactNode;
};

View File

@@ -73,7 +73,7 @@ export const Component = (props: DropdownContainerProps) => {
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
const containerRef = useRef<DropdownRef>(null);
const onOverflowingStateChange = useCallback(
value => {
(value: OverflowingState) => {
if (!isEqual(overflowingState, value)) {
setItems(generateItems(value));
setOverflowingState(value);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: IconType;
dropdownTriggerIcon?: ReactNode;
/**
* Text of the dropdown trigger.
*/

View File

@@ -0,0 +1,80 @@
/**
* 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 { fireEvent, render, screen, userEvent } from '@superset-ui/core/spec';
import { useState } from 'react';
import { DynamicEditableTitle } from '.';
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
const [title, setTitle] = useState(initialTitle);
return (
<DynamicEditableTitle
title={title}
placeholder="placeholder"
canEdit
label="Title"
onSave={setTitle}
/>
);
};
test('rapid typing then backspacing keeps every keystroke', async () => {
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'abc', { delay: 1 });
expect(input.value).toBe('Originalabc');
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
delay: 1,
});
expect(input.value).toBe('Original');
});
test('a change event that arrives before isEditing flips is not dropped', () => {
// Reproduces the regression: the input is focused but `isEditing` is still
// false because no click has been registered yet (e.g. focus arrived via
// tab, autofocus, or programmatic focus). The pre-fix `handleChange`
// bailed out with `!isEditing`, dropping the keystroke. Because the
// input is controlled, antd's internal `useMergedState` then resyncs the
// DOM value back to the (stale) `props.value`, so the user sees their
// typed character disappear. This test fires a raw change event so it
// doesn't go through userEvent's implicit click.
const onSave = jest.fn();
render(
<DynamicEditableTitle
title="Foo"
placeholder="placeholder"
canEdit
label="Title"
onSave={onSave}
/>,
);
const input = screen.getByRole('textbox') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'FooX' } });
expect(input.value).toBe('FooX');
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
const { rerender } = render(<Harness initialTitle="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<Harness initialTitle="Foo" />);
expect(input.value).toBe('FooX');
});

View File

@@ -23,6 +23,7 @@ import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
@@ -30,6 +31,7 @@ import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { useResizeDetector } from 'react-resize-detector';
import { Tooltip } from '../Tooltip';
import { Input } from '../Input';
import type { InputRef } from '../Input';
import type { DynamicEditableTitleProps } from './types';
const titleStyles = (theme: SupersetTheme) => css`
@@ -75,8 +77,10 @@ export const DynamicEditableTitle = memo(
const [isEditing, setIsEditing] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
const [inputWidth, setInputWidth] = useState<number>(0);
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
const sizerRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<InputRef>(null);
const { width: containerWidth, ref: containerRef } = useResizeDetector({
refreshMode: 'debounce',
});
@@ -85,27 +89,33 @@ export const DynamicEditableTitle = memo(
setCurrentTitle(title);
}, [title]);
useEffect(() => {
if (isEditing && sizerRef?.current) {
if (isEditing) {
// move cursor and scroll to the end
if (sizerRef.current.setSelectionRange) {
const { length } = sizerRef.current.value;
sizerRef.current.setSelectionRange(length, length);
sizerRef.current.scrollLeft = sizerRef.current.scrollWidth;
const inputElement = inputRef.current?.input;
if (inputElement) {
const { length } = inputElement.value;
inputElement.setSelectionRange(length, length);
inputElement.scrollLeft = inputElement.scrollWidth;
}
}
}, [isEditing]);
// a trick to make the input grow when user types text
// we make additional span component, place it somewhere out of view and copy input
// then we can measure the width of that span to resize the input element
// we make an additional span component, place it somewhere out of view and
// mirror the input value, then measure the span synchronously (pre-paint)
// to resize the input element. Reading offsetWidth in a useLayoutEffect
// forces a sync layout, so the input width updates in the same commit as
// the value change — preventing a flicker frame where the input is shown
// with new value but stale width.
useLayoutEffect(() => {
if (sizerRef?.current) {
if (sizerRef.current) {
sizerRef.current.textContent = currentTitle || placeholder;
setInputWidth(sizerRef.current.offsetWidth);
}
}, [currentTitle, placeholder, sizerRef]);
}, [currentTitle, placeholder]);
useEffect(() => {
const inputElement = sizerRef.current?.input;
const inputElement = inputRef.current?.input;
if (inputElement) {
if (inputElement.scrollWidth > inputElement.clientWidth) {
@@ -137,9 +147,17 @@ export const DynamicEditableTitle = memo(
const handleChange = useCallback(
(ev: ChangeEvent<HTMLInputElement>) => {
if (!canEdit || !isEditing) {
if (!canEdit) {
return;
}
// Any change implies the user is editing. Ensure isEditing is true
// even if the change event arrives before the click handler has
// committed (e.g. focus via tab, autofocus, or batched click+type
// events). Otherwise the keystroke would be dropped and the
// controlled input would revert to the previous value.
if (!isEditing) {
setIsEditing(true);
}
setCurrentTitle(ev.target.value);
},
[canEdit, isEditing],
@@ -168,6 +186,7 @@ export const DynamicEditableTitle = memo(
}
>
<Input
ref={inputRef}
data-test="editable-title-input"
variant="borderless"
aria-label={label ?? t('Title')}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { ReactNode, SyntheticEvent } from 'react';
import type { IconType } from '@superset-ui/core/components';
export type EmptyStateSize = 'small' | 'medium' | 'large';
@@ -26,7 +25,7 @@ export type EmptyStateProps = {
description?: ReactNode;
image?: ReactNode | string;
buttonText?: ReactNode;
buttonIcon?: IconType;
buttonIcon?: ReactNode;
buttonAction?: (event: SyntheticEvent) => void;
/** Controls image size. Defaults to 'medium'. */
size?: EmptyStateSize;

View File

@@ -20,7 +20,7 @@ import { Form as AntdForm } from 'antd';
import { FormProps } from './types';
function CustomForm(props: FormProps) {
return <AntdForm {...props} />;
return <AntdForm {...(props as any)} />;
}
export const Form = Object.assign(CustomForm, {

View File

@@ -41,7 +41,6 @@ test('renders with monospace prop', () => {
// test stories from the storybook!
test('renders all the storybook gallery variants', () => {
// @ts-expect-error: Suppress TypeScript error for LabelGallery usage
const { container } = render(<LabelGallery />);
const nonInteractiveLabelCount = 4;
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;

View File

@@ -21,6 +21,7 @@ import type { BackgroundPosition } from './ImageLoader';
export interface LinkProps {
to: string;
children?: ReactNode;
}
export interface ListViewCardProps {

View File

@@ -194,7 +194,7 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
}
const onResize = useCallback(
width => {
(width: number | undefined) => {
// Calculates the breakpoint width to collapse the bar.
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
const breakpoint =

View File

@@ -54,7 +54,7 @@ export function FormModal({
}, [onSave, resetForm]);
const handleFormSubmit = useCallback(
async values => {
async (values: object) => {
try {
setIsSaving(true);
await formSubmitHandler(values);

View File

@@ -104,6 +104,9 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
right: 0;
display: flex;
justify-content: center;
// Keep the close button clickable when modal body content uses
// position: sticky with elevated z-index (e.g. DatabaseModal header).
z-index: ${theme.zIndexPopupBase + 1};
}
.ant-modal-close:hover {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import type { CSSProperties, ReactNode } from 'react';
import type { ModalFuncProps } from 'antd';
import type { FormInstance, ModalFuncProps } from 'antd';
import type { ResizableProps } from 're-resizable';
import type { DraggableProps } from 'react-draggable';
import { ButtonStyle } from '../Button/types';
@@ -68,7 +68,8 @@ export interface StyledModalProps {
export type { ModalFuncProps };
export interface FormModalProps extends ModalProps {
export interface FormModalProps extends Omit<ModalProps, 'children'> {
children: ReactNode | ((form: FormInstance) => ReactNode);
initialValues?: object;
formSubmitHandler: (values: object) => Promise<void>;
onSave: () => void;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, ReactElement } from 'react';
import { ReactNode, ReactElement, memo } from 'react';
import { t } from '@apache-superset/core/translation';
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
@@ -118,62 +118,64 @@ export type PageHeaderWithActionsProps = {
};
};
export const PageHeaderWithActions = ({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
export const PageHeaderWithActions = memo(
({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
)}
</div>
</div>
</div>
</div>
);
};
);
},
);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render, screen, fireEvent } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { TableInstance, useTable } from 'react-table';
import TableCollection from '.';

View File

@@ -91,7 +91,7 @@ export function mapColumns<T extends object>(
return columns.map(column => {
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
return {
title: column.Header,
title: column.Header as ReactNode,
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden,
key: column.id,
@@ -121,7 +121,7 @@ export function mapColumns<T extends object>(
column,
});
}
return val;
return val as ReactNode;
},
className: column.className,
};

View File

@@ -19,6 +19,14 @@
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { TableView, TableViewProps } from '.';
// Mock window.scrollTo to prevent jsdom "Not implemented" errors
beforeAll(() => {
window.scrollTo = jest.fn();
});
afterAll(() => {
jest.restoreAllMocks();
});
const mockedProps: TableViewProps = {
columns: [
{
@@ -125,27 +133,25 @@ test('should change page when pagination is clicked', async () => {
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('Kate')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
const page1 = screen.getByRole('listitem', { name: '1' });
await userEvent.click(page1);
await userEvent.click(screen.getByTitle('Previous Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
test('should sort by age', async () => {
@@ -240,8 +246,7 @@ test('should handle server-side pagination', async () => {
render(<TableView {...serverPaginationProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(onServerPagination).toHaveBeenCalledWith({
@@ -301,9 +306,7 @@ test('should scroll to top when scrollTopOnPagination is true', async () => {
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
@@ -324,9 +327,7 @@ test('should NOT scroll to top when scrollTopOnPagination is false', async () =>
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getByText('321')).toBeInTheDocument();

View File

@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
import { memo, useEffect, useRef, useMemo, useCallback, useState } from 'react';
import { isEqual } from 'lodash';
import { styled } from '@apache-superset/core/theme';
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
import { useFilters, useSortBy, useTable } from 'react-table';
import { Empty } from '@superset-ui/core/components';
import TableCollection from '@superset-ui/core/components/TableCollection';
import { TableSize } from '@superset-ui/core/components/Table';
@@ -117,43 +117,45 @@ const RawTableView = ({
...props
}: TableViewProps) => {
const tableRef = useRef<HTMLTableElement>(null);
const effectivePageSize = initialPageSize ?? DEFAULT_PAGE_SIZE;
const [pageIndex, setPageIndex] = useState(initialPageIndex ?? 0);
const initialState = useMemo(
() => ({
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageIndex: initialPageIndex ?? 0,
pageSize: effectivePageSize,
pageIndex: 0,
sortBy: initialSortBy,
}),
[initialPageSize, initialPageIndex, initialSortBy],
[effectivePageSize, initialSortBy],
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
rows,
prepareRow,
gotoPage,
setSortBy,
state: { pageIndex, sortBy },
state: { sortBy },
} = useTable(
{
columns,
data,
initialState,
manualPagination: serverPagination,
manualPagination: true,
manualSortBy: serverPagination,
pageCount: serverPagination
? Math.ceil(totalCount / initialState.pageSize)
: undefined,
autoResetSortBy: false,
},
useFilters,
useSortBy,
...(withPagination ? [usePagination] : []),
);
const content = useMemo(() => {
if (!withPagination || serverPagination) return rows;
const start = pageIndex * effectivePageSize;
return rows.slice(start, start + effectivePageSize);
}, [withPagination, serverPagination, rows, pageIndex, effectivePageSize]);
const EmptyWrapperComponent = useMemo(() => {
switch (emptyWrapperType) {
case EmptyWrapperType.Small:
@@ -164,11 +166,6 @@ const RawTableView = ({
}
}, [emptyWrapperType]);
const content = useMemo(
() => (withPagination ? page : rows),
[withPagination, page, rows],
);
const isEmpty = useMemo(
() => !loading && content.length === 0,
[loading, content.length],
@@ -192,10 +189,9 @@ const RawTableView = ({
const handlePageChange = useCallback(
(p: number) => {
if (scrollTopOnPagination) handleScrollToTop();
gotoPage(p);
setPageIndex(p);
},
[scrollTopOnPagination, handleScrollToTop, gotoPage],
[scrollTopOnPagination, handleScrollToTop],
);
const paginationProps = useMemo(() => {
@@ -211,7 +207,7 @@ const RawTableView = ({
if (serverPagination) {
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount,
onPageChange: handlePageChange,
};
@@ -219,7 +215,7 @@ const RawTableView = ({
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount: data.length,
onPageChange: handlePageChange,
};
@@ -227,28 +223,28 @@ const RawTableView = ({
withPagination,
serverPagination,
pageIndex,
initialPageSize,
effectivePageSize,
totalCount,
data.length,
handlePageChange,
]);
useEffect(() => {
if (serverPagination && pageIndex !== initialState.pageIndex) {
if (serverPagination && pageIndex !== (initialPageIndex ?? 0)) {
onServerPagination({
pageIndex,
});
}
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
}, [initialPageIndex, onServerPagination, pageIndex, serverPagination]);
useEffect(() => {
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
if (serverPagination && !isEqual(sortBy, initialSortBy)) {
onServerPagination({
pageIndex: 0,
sortBy,
});
}
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
}, [initialSortBy, onServerPagination, serverPagination, sortBy]);
return (
<TableViewStyles {...props} ref={tableRef}>

View File

@@ -97,8 +97,8 @@ const StyledPlus = styled.span`
export default function TruncatedList<ListItemType>({
items,
renderVisibleItem = item => item,
renderTooltipItem = item => item,
renderVisibleItem = item => item as ReactNode,
renderTooltipItem = item => item as ReactNode,
getKey = item => item as unknown as Key,
maxLinks = 20,
}: TruncatedListProps<ListItemType>) {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useChangeEffect } from './useChangeEffect';
test('call callback the first time with undefined and value', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidMount } from './useComponentDidMount';
test('the effect should only be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidUpdate } from './useComponentDidUpdate';
test('the effect should not be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { useElementOnScreen } from './useElementOnScreen';
const observeMock = jest.fn();
@@ -46,10 +46,9 @@ test('should return isSticky as true when intersectionRatio < 1', async () => {
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
});
expect(hook.result.current[1]).toEqual(true);
});
@@ -58,10 +57,9 @@ test('should return isSticky as false when intersectionRatio >= 1', async () =>
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 1 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 1 }]);
});
expect(hook.result.current[1]).toEqual(false);
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { usePrevious } from './usePrevious';
test('get undefined on the first render when initialValue is not defined', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import useCSSTextTruncation from './useCSSTextTruncation';
afterEach(() => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { RefObject } from 'react';
import useChildElementTruncation from './useChildElementTruncation';

View File

@@ -249,7 +249,8 @@ export type Extensions = Partial<{
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
'navbar.right': ComponentType;
'report-modal.dropdown.item.icon': ComponentType;
'root.context.provider': ComponentType;
'root.context.provider': ComponentType<{ children?: ReactNode }>;
'welcome.message': ComponentType;
'welcome.banner': ComponentType;
'welcome.main.replacement': ComponentType;

View File

@@ -143,7 +143,7 @@ describe('SuperChart', () => {
);
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
expect(CustomFallbackComponent).toHaveBeenCalled();
});
test('call onErrorBoundary', async () => {
expectedErrors = 1;

View File

@@ -33,7 +33,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -36,6 +36,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -32,9 +32,9 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -32,7 +32,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,6 +39,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -43,6 +43,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -39,14 +39,13 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -247,7 +247,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[serverPagination, debouncedSearch, searchId],
);
const handleColSort = (colId: string, sortDir: string) => {
const handleColSort = (colId: string, sortDir: string | null) => {
const isSortable = shouldSort({
colId,
sortDir,
@@ -301,10 +301,12 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
};
const handleColumnHeaderClick = useCallback(
params => {
(params: { column?: { colId?: string; sort?: string | null } }) => {
const colId = params?.column?.colId;
const sortDir = params?.column?.sort;
handleColSort(colId, sortDir);
if (colId && sortDir !== undefined) {
handleColSort(colId, sortDir);
}
},
[serverPagination, gridInitialState, percentMetrics, onSortChange],
);

View File

@@ -147,7 +147,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
]);
const handleColumnStateChange = useCallback(
agGridState => {
(agGridState: Record<string, unknown>) => {
if (onChartStateChange) {
onChartStateChange(agGridState);
}

View File

@@ -70,5 +70,9 @@ export const TextCellRenderer = (params: CellRendererProps) => {
}
}
return <div>{valueFormatted ?? value}</div>;
return (
<div>
{valueFormatted ?? (value instanceof Date ? value.toISOString() : value)}
</div>
);
};

View File

@@ -43,7 +43,7 @@ export const shouldSort = ({
gridInitialState,
}: {
colId: string;
sortDir: string;
sortDir: string | null;
percentMetrics: string[];
serverPagination: boolean;
gridInitialState: GridState;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { GenericDataType } from '@apache-superset/core/common';
import {
supersetTheme,

View File

@@ -47,7 +47,7 @@
"geostyler-wfs-parser": "^3.0.1",
"ol": "^10.8.0",
"polished": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@@ -19,7 +19,7 @@
import Layer from 'ol/layer/Layer';
import { FrameState } from 'ol/Map';
import { apply as applyTransform } from 'ol/transform';
import ReactDOM from 'react-dom';
import { createRoot, Root } from 'react-dom/client';
import { SupersetTheme } from '@apache-superset/core/theme';
import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types';
import { createChartComponent } from '../util/chartUtil';
@@ -31,7 +31,14 @@ import Loader from '../images/loading.gif';
* Custom OpenLayers layer that displays charts on given locations.
*/
export class ChartLayer extends Layer {
charts: any[] = [];
charts: {
htmlElement: HTMLDivElement;
root: Root;
coordinate: number[];
width: number;
height: number;
feature: any;
}[] = [];
chartConfigs: ChartConfig = {
type: 'FeatureCollection',
@@ -166,7 +173,7 @@ export class ChartLayer extends Layer {
*/
removeAllChartElements() {
this.charts.forEach(chart => {
ReactDOM.unmountComponentAtNode(chart.htmlElement);
chart.root.unmount();
chart.htmlElement.remove();
});
this.charts = [];
@@ -191,10 +198,12 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, container);
const root = createRoot(container);
root.render(chartComponent);
return {
htmlElement: container,
root,
coordinate: getProjectedCoordinateFromPointGeoJson(feature.geometry),
width: chartWidth,
height: chartHeight,
@@ -227,7 +236,7 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, chart.htmlElement);
chart.root.render(chartComponent);
return {
...chart,

View File

@@ -41,6 +41,11 @@ describe('ChartLayer', () => {
chartLayer.charts = [
{
htmlElement: document.createElement('div'),
root: { render: jest.fn(), unmount: jest.fn() } as any,
coordinate: [0, 0],
width: 100,
height: 100,
feature: {},
},
];

View File

@@ -38,7 +38,7 @@
"dayjs": "^1.11.19",
"echarts": "*",
"memoize-one": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -57,7 +57,7 @@ export default function EchartsMixedTimeseries({
);
const getCrossFilterDataMask = useCallback(
(seriesName, seriesIndex) => {
(seriesName: string, seriesIndex: number) => {
const selected: string[] = Object.values(selectedValues || {});
let values: string[];
if (selected.includes(seriesName)) {

View File

@@ -26,7 +26,7 @@ import {
import { useCallback } from 'react';
import Echart from '../components/Echart';
import { NULL_STRING } from '../constants';
import { EventHandlers } from '../types';
import { EventHandlers, TreePathInfo } from '../types';
import { extractTreePathInfo } from './constants';
import { TreemapTransformedProps } from './types';
import { formatSeriesName } from '../utils/series';
@@ -46,7 +46,7 @@ export default function EchartsTreemap({
coltypeMapping,
}: TreemapTransformedProps) {
const getCrossFilterDataMask = useCallback(
(data, treePathInfo) => {
(data: Record<string, unknown>, treePathInfo: TreePathInfo[]) => {
if (data?.children) {
return undefined;
}
@@ -96,7 +96,7 @@ export default function EchartsTreemap({
);
const handleChange = useCallback(
(data, treePathInfo) => {
(data: Record<string, unknown>, treePathInfo: TreePathInfo[]) => {
if (!emitCrossFilters || groupby.length === 0) {
return;
}

View File

@@ -39,9 +39,9 @@
"handlebars": "^4.7.8",
"lodash": "^4.18.1",
"dayjs": "^1.11.19",
"react": "^17.0.2",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^17.0.2"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",

View File

@@ -71,7 +71,7 @@ ${helperDescriptions
<div>
<ControlHeader>
<div>
{props.label}
{typeof props.label === 'function' ? null : props.label}
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={<SafeMarkdown source={helpersTooltipContent} />}

View File

@@ -49,7 +49,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
<div>
<ControlHeader>
<div>
{props.label}
{typeof props.label === 'function' ? null : props.label}
{htmlSanitization && (
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}

View File

@@ -33,8 +33,8 @@
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"prop-types": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/types": "^7.29.0",

View File

@@ -36,8 +36,8 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^17.0.2 || ^19.0.0",
"react-dom": "^17.0.2 || ^19.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,15 +39,14 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,7 +39,7 @@
"@superset-ui/core": "*",
"@types/lodash": "*",
"@types/react": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"devDependencies": {
"@types/d3-cloud": "^1.2.9"

View File

@@ -67,8 +67,8 @@
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"mapbox-gl": ">=1.0.0",
"react": "^17.0.2 || ^19.0.0",
"react-dom": "^17.0.2 || ^19.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependenciesMeta": {
"mapbox-gl": {

View File

@@ -139,7 +139,8 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
const { current } = containerRef;
if (current) {
current.setTooltip(tooltip);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(current as any).setTooltip(tooltip);
}
}, []);

View File

@@ -194,5 +194,5 @@ export const DeckGLContainerStyledWrapper = styled(DeckGLContainer)`
`;
export type DeckGLContainerHandle = typeof DeckGLContainer & {
setTooltip: (tooltip: ReactNode) => void;
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
};

View File

@@ -97,10 +97,10 @@ describe('getAggFunc', () => {
});
describe('commonLayerProps', () => {
const mockSetTooltip = jest.fn();
const mockSetTooltip = jest.fn() as any;
const mockSetTooltipContent = jest.fn(
() => (o: JsonObject) => `Tooltip for ${o}`,
);
) as any;
const mockOnSelect = jest.fn();
test('returns correct props when js_tooltip is provided', () => {

View File

@@ -97,6 +97,7 @@ export function createWrapper(options?: Options) {
}
if (useDnd) {
// @ts-expect-error react-dnd types not updated for React 18
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
}

View File

@@ -65,7 +65,7 @@ const ContentWrapper = styled.div`
overflow: auto;
`;
const AppLayout: React.FC = ({ children }) => {
const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const queryEditorId = useSelector<SqlLabRootState, string>(
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);

View File

@@ -19,16 +19,14 @@
import { isValidElement } from 'react';
import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures';
import { table } from 'src/SqlLab/fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ColumnElement', () => {
const mockedProps = {
actions: mockedActions,
column: table.columns[0],
};
test('is valid with props', () => {
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
expect(isValidElement(<ColumnElement column={table.columns[0]} />)).toBe(
true,
);
});
test('renders a proper primary key', () => {
const { container } = render(<ColumnElement column={table.columns[0]} />);

View File

@@ -116,19 +116,31 @@ describe('EditorWrapper', () => {
);
});
test('skips rerendering for updating cursor position', () => {
test('skips rerendering for updating cursor position', async () => {
const store = createStore(initialState, reducerIndex);
setup(defaultQueryEditor, store);
expect(MockEditorHost).toHaveBeenCalled();
const renderCount = MockEditorHost.mock.calls.length;
await waitFor(() => expect(MockEditorHost).toHaveBeenCalled());
const renderCountBeforeCursor = MockEditorHost.mock.calls.length;
const updatedCursorPosition = { row: 1, column: 9 };
store.dispatch(
queryEditorSetCursorPosition(defaultQueryEditor, updatedCursorPosition),
act(() => {
store.dispatch(
queryEditorSetCursorPosition(defaultQueryEditor, updatedCursorPosition),
);
});
// Cursor position change should NOT trigger a re-render
expect(MockEditorHost).toHaveBeenCalledTimes(renderCountBeforeCursor);
const renderCountBeforeDb = MockEditorHost.mock.calls.length;
act(() => {
store.dispatch(queryEditorSetDb(defaultQueryEditor, 2));
});
// DB change SHOULD trigger a re-render
await waitFor(() =>
expect(MockEditorHost.mock.calls.length).toBeGreaterThan(
renderCountBeforeDb,
),
);
expect(MockEditorHost).toHaveBeenCalledTimes(renderCount);
store.dispatch(queryEditorSetDb(defaultQueryEditor, 2));
expect(MockEditorHost).toHaveBeenCalledTimes(renderCount + 1);
});
test('clears selectedText when selection becomes empty', async () => {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { COMMON_ERR_MESSAGES } from '@superset-ui/core';
import {
createWrapper,
@@ -121,7 +121,7 @@ test('skips fetching validation if validator is undefined', () => {
});
test('returns validation if validator is configured', async () => {
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(() =>
expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(
1,
@@ -143,7 +143,7 @@ test('returns server error description', async () => {
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([
@@ -164,7 +164,7 @@ test('returns session expire description when CSRF token expired', async () => {
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { getExtensionsRegistry } from '@superset-ui/core';
import {
createWrapper,
@@ -104,7 +104,7 @@ test('returns keywords including fetched function_names data', async () => {
const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectDbId}/function_names/`;
fetchMock.get(dbFunctionNamesApiRoute, fakeFunctionNamesApiResult);
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',
@@ -241,7 +241,7 @@ test('returns column keywords among selected tables', async () => {
);
});
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: expectQueryEditorId,
@@ -302,7 +302,7 @@ test('returns long keywords with detail', async () => {
),
);
});
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FC } from 'react';
import { FC, ReactNode } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, css } from '@apache-superset/core/theme';
import { ModalTrigger } from '@superset-ui/core/components';
@@ -92,7 +92,7 @@ const ShortcutCode = styled.code`
padding: ${({ theme }) => `${theme.sizeUnit}px ${theme.sizeUnit * 2}px`};
`;
const KeyboardShortcutButton: FC<{}> = ({ children }) => (
const KeyboardShortcutButton: FC<{ children?: ReactNode }> = ({ children }) => (
<ModalTrigger
modalTitle={t('Keyboard shortcuts')}
modalBody={

View File

@@ -39,7 +39,9 @@ import getBootstrapData from 'src/utils/getBootstrapData';
const SQL_LAB_URL = '/sqllab';
const PopEditorTab: React.FC = ({ children }) => {
const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [queryEditorId, setQueryEditorId] = useState<string>();
const { requestedQuery } = useLocationState();

View File

@@ -50,14 +50,23 @@ import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from './styles';
interface QueryTableQuery extends Omit<
QueryResponse,
'state' | 'sql' | 'progress' | 'results' | 'duration' | 'started'
| 'state'
| 'sql'
| 'progress'
| 'results'
| 'duration'
| 'started'
| 'user'
| 'db'
> {
state?: Record<string, any>;
sql?: Record<string, any>;
progress?: Record<string, any>;
results?: Record<string, any>;
state?: ReactNode;
sql?: ReactNode;
progress?: ReactNode;
results?: ReactNode;
duration?: ReactNode;
started?: ReactNode;
user?: ReactNode;
db?: ReactNode;
}
interface QueryTableProps {
@@ -249,7 +258,7 @@ const QueryTable = ({
return queries
.map(query => {
const { state, sql, progress, ...rest } = query;
const { state, sql, progress, results: _results, ...rest } = query;
const q = rest as QueryTableQuery;
const status = statusAttributes[state] || statusAttributes.error;
@@ -265,7 +274,7 @@ const QueryTable = ({
buttonStyle="link"
onClick={() => onUserClicked(q.userId)}
>
{q.user}
{q.user as ReactNode}
</Button>
);
q.db = (
@@ -274,7 +283,7 @@ const QueryTable = ({
buttonStyle="link"
onClick={() => onDbClicked(q.dbId)}
>
{q.db}
{q.db as ReactNode}
</Button>
);
q.started = (

View File

@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, FC, ReactElement } from 'react';
import { useMemo, FC, ReactElement, type ReactNode } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, useTheme, SupersetTheme } from '@apache-superset/core/theme';
import { Button, DropdownButton } from '@superset-ui/core/components';
import { IconType, Icons } from '@superset-ui/core/components/Icons';
import { Icons } from '@superset-ui/core/components/Icons';
import { detectOS } from 'src/utils/common';
import { QueryButtonProps } from 'src/SqlLab/types';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
@@ -45,9 +45,9 @@ const buildTextAndIcon = (
shouldShowStopButton: boolean,
selectedText: string | undefined,
theme: SupersetTheme,
): { text: string; icon?: IconType } => {
): { text: string; icon?: ReactNode } => {
let text = t('Run');
let icon: IconType | undefined = <Icons.CaretRightOutlined />;
let icon: ReactNode = <Icons.CaretRightOutlined />;
if (selectedText) {
text = t('Run selection');
icon = <Icons.StepForwardOutlined />;

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import * as reactRedux from 'react-redux';
import { act } from 'react';
import {
cleanup,
fireEvent,
@@ -136,7 +137,7 @@ describe('SaveDatasetModal', () => {
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
userEvent.click(overwriteRadioBtn);
await userEvent.click(overwriteRadioBtn);
// Overwrite confirmation button should be disabled at this point
const overwriteConfirmationBtn = screen.getByRole('button', {
@@ -146,15 +147,21 @@ describe('SaveDatasetModal', () => {
// Click the overwrite select component
const select = screen.getByRole('combobox', { name: /existing dataset/i })!;
userEvent.click(select);
await userEvent.click(select);
await waitFor(() =>
expect(screen.queryByText('Loading...')).not.toBeVisible(),
);
// Advance timers to flush debounced fetches in AsyncSelect
await act(async () => {
jest.runAllTimers();
});
await waitFor(() => {
const loading = screen.queryByText('Loading...');
expect(loading === null || !loading.checkVisibility()).toBe(true);
});
// Select the first "existing dataset" from the listbox
const option = screen.getAllByText('coolest table 0')[1];
userEvent.click(option);
await userEvent.click(option);
// Overwrite button should now be enabled
expect(overwriteConfirmationBtn).toBeEnabled();
@@ -168,25 +175,31 @@ describe('SaveDatasetModal', () => {
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
userEvent.click(overwriteRadioBtn);
await userEvent.click(overwriteRadioBtn);
// Click the overwrite select component
const select = screen.getByRole('combobox', { name: /existing dataset/i });
userEvent.click(select);
await userEvent.click(select);
await waitFor(() =>
expect(screen.queryByText('Loading...')).not.toBeVisible(),
);
// Advance timers to flush debounced fetches in AsyncSelect
await act(async () => {
jest.runAllTimers();
});
await waitFor(() => {
const loading = screen.queryByText('Loading...');
expect(loading === null || !loading.checkVisibility()).toBe(true);
});
// Select the first "existing dataset" from the listbox
const option = screen.getAllByText('coolest table 0')[1];
userEvent.click(option);
await userEvent.click(option);
// Click the overwrite button to access the confirmation screen
const overwriteConfirmationBtn = screen.getByRole('button', {
name: /overwrite/i,
});
userEvent.click(overwriteConfirmationBtn);
await userEvent.click(overwriteConfirmationBtn);
// Overwrite screen text
expect(screen.getByText(/save or overwrite dataset/i)).toBeInTheDocument();

View File

@@ -140,7 +140,7 @@ const SouthPane = ({
logAction(LOG_ACTIONS_SQLLAB_SWITCH_SOUTH_PANE_TAB, { tab: id });
};
const removeTable = useCallback(
(key, action) => {
(key: string, action: string) => {
if (action === 'remove') {
const table = pinnedTables.find(
({ dbId, catalog, schema, name }) =>

View File

@@ -292,7 +292,10 @@ const SqlEditor: FC<Props> = ({
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
const startQuery = useCallback(
(ctasArg = false, ctas_method = CtasEnum.Table) => {
(
ctasArg = false,
ctas_method: (typeof CtasEnum)[keyof typeof CtasEnum] = CtasEnum.Table,
) => {
if (!database) {
return;
}

View File

@@ -18,7 +18,7 @@
*/
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { createWrapper } from 'spec/helpers/testing-library';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import * as localStorageHelpers from 'src/utils/localStorageHelpers';

View File

@@ -19,7 +19,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { createWrapper } from 'spec/helpers/testing-library';
import useQueryEditor from '.';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { SupersetClient } from '@superset-ui/core';
import { useDeckLayerMetadata } from './useDeckLayerMetadata';
@@ -52,15 +52,14 @@ test('fetches layer metadata successfully', async () => {
};
mockSupersetClientGet.mockResolvedValue(mockResponse);
const { result, waitForNextUpdate } = renderHook(() =>
useDeckLayerMetadata([1, 2]),
);
const { result } = renderHook(() => useDeckLayerMetadata([1, 2]));
expect(result.current.isLoading).toBe(true);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.layers).toEqual([
{ sliceId: 1, name: 'Layer 1', type: 'deck_scatter' },
{ sliceId: 2, name: 'Layer 2', type: 'deck_arc' },
@@ -75,13 +74,12 @@ test('handles API error and returns fallback layers', async () => {
const errorMessage = 'Network error';
mockSupersetClientGet.mockRejectedValue(new Error(errorMessage));
const { result, waitForNextUpdate } = renderHook(() =>
useDeckLayerMetadata([1, 2, 3]),
);
const { result } = renderHook(() => useDeckLayerMetadata([1, 2, 3]));
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(errorMessage);
expect(result.current.layers).toEqual([
{ sliceId: 1, name: 'Layer 1', type: 'unknown' },
@@ -93,13 +91,12 @@ test('handles API error and returns fallback layers', async () => {
test('handles non-Error object rejection', async () => {
mockSupersetClientGet.mockRejectedValue('String error');
const { result, waitForNextUpdate } = renderHook(() =>
useDeckLayerMetadata([1]),
);
const { result } = renderHook(() => useDeckLayerMetadata([1]));
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe('Unknown error');
expect(result.current.layers).toEqual([
{ sliceId: 1, name: 'Layer 1', type: 'unknown' },
@@ -125,22 +122,25 @@ test('refetches when sliceIds change', async () => {
.mockResolvedValueOnce(mockResponse1)
.mockResolvedValueOnce(mockResponse2);
const { result, rerender, waitForNextUpdate } = renderHook(
const { result, rerender } = renderHook(
({ ids }) => useDeckLayerMetadata(ids),
{
initialProps: { ids: [1] },
},
);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.layers).toHaveLength(1);
expect(result.current.layers[0].sliceId).toBe(1);
rerender({ ids: [2, 3] });
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.layers).toHaveLength(2);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.layers).toHaveLength(2);
@@ -157,13 +157,12 @@ test('handles empty result from API', async () => {
};
mockSupersetClientGet.mockResolvedValue(mockResponse);
const { result, waitForNextUpdate } = renderHook(() =>
useDeckLayerMetadata([1, 2]),
);
const { result } = renderHook(() => useDeckLayerMetadata([1, 2]));
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.layers).toEqual([]);
expect(result.current.error).toBe(null);
});
@@ -176,16 +175,17 @@ test('clears isLoading when sliceIds transitions from non-empty to empty', async
};
mockSupersetClientGet.mockResolvedValue(mockResponse);
const { result, rerender, waitForNextUpdate } = renderHook(
const { result, rerender } = renderHook(
({ ids }) => useDeckLayerMetadata(ids),
{
initialProps: { ids: [1] },
},
);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.layers).toHaveLength(1);
act(() => {
@@ -204,16 +204,16 @@ test('does not refetch when sliceIds array has same values', async () => {
};
mockSupersetClientGet.mockResolvedValue(mockResponse);
const { result, rerender, waitForNextUpdate } = renderHook(
const { result, rerender } = renderHook(
({ ids }) => useDeckLayerMetadata(ids),
{
initialProps: { ids: [1] },
},
);
await waitForNextUpdate();
expect(result.current.isLoading).toBe(false);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
const callCount = mockSupersetClientGet.mock.calls.length;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent } from 'react';
import { ErrorInfo, PureComponent } from 'react';
import { logging } from '@apache-superset/core/utils';
import { t } from '@apache-superset/core/translation';
import {
@@ -235,16 +235,13 @@ class Chart extends PureComponent<ChartProps, {}> {
);
}
handleRenderContainerFailure(
error: Error,
info: { componentStack: string } | null,
) {
handleRenderContainerFailure(error: Error, info: ErrorInfo) {
const { actions, chartId } = this.props;
logging.warn(error);
actions.chartRenderingFailed(
error.toString(),
chartId,
info ? info.componentStack : null,
info?.componentStack ?? null,
);
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {

View File

@@ -18,7 +18,7 @@
*/
import { FeatureFlag, VizType } from '@superset-ui/core';
import { render, screen } from 'spec/helpers/testing-library';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import mockState from 'spec/fixtures/mockState';
import { sliceId } from 'spec/fixtures/mockChartQueries';
import { noOp } from 'src/utils/common';
@@ -95,7 +95,9 @@ beforeEach(() => {
test('Context menu renders', () => {
const result = setup();
expect(screen.queryByTestId(CONTEXT_MENU_TEST_ID)).not.toBeInTheDocument();
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByTestId(CONTEXT_MENU_TEST_ID)).toBeInTheDocument();
expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
@@ -106,7 +108,9 @@ test('Context menu contains all displayed items only', () => {
const result = setup({
displayedItems: [ContextMenuItem.DrillToDetail, ContextMenuItem.DrillBy],
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Add cross-filter')).not.toBeInTheDocument();
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
expect(screen.getByText('Drill by')).toBeInTheDocument();
@@ -122,7 +126,9 @@ test('Context menu shows "Drill by" with `can_drill`, `can_write` & `can_get_dri
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByText('Drill by')).toBeInTheDocument();
});
@@ -137,7 +143,9 @@ test('Context menu shows "Drill by" with `can_drill`, `can_get_drill_info` & `ca
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByText('Drill by')).toBeInTheDocument();
});
@@ -147,7 +155,9 @@ test('Context menu does not show "Drill by" with neither of required perms', ()
Admin: [['invalid_permission', 'Dashboard']],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
});
@@ -157,7 +167,9 @@ test('Context menu does not show "Drill by" with just `can_dril` perm', () => {
Admin: [['can_drill', 'Dashboard']],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
});
@@ -170,7 +182,9 @@ test('Context menu does not show "Drill by" with just `can_dril` & `can_write` p
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
});
@@ -184,7 +198,9 @@ test('Context menu does not show "Drill by" with just `can_drill`, `can_explore`
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
});
@@ -198,7 +214,9 @@ test('Context menu shows "Drill to detail" with `can_samples`, `can_explore` & `
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -212,7 +230,9 @@ test('Context menu shows "Drill to detail" with `can_drill`, `can_samples` & `ca
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -227,7 +247,9 @@ test('Context menu shows "Drill to detail" with `can_drill`, `can_get_drill_info
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
});
@@ -237,7 +259,9 @@ test('Context menu does not show "Drill to detail" with neither of required perm
Admin: [['invalid_permission', 'Dashboard']],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -247,7 +271,9 @@ test('Context menu does not show "Drill to detail" with just `can_drill` perm',
Admin: [['can_drill', 'Dashboard']],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -260,7 +286,9 @@ test('Context menu does not show "Drill to detail" with just `can_drill` & `can_
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -273,7 +301,9 @@ test('Context menu does not show "Drill to detail" with `can_samples` & `can_exp
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -287,7 +317,9 @@ test('Context menu does not show "Drill to detail" with `can_drill`, `can_explor
],
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
});
@@ -303,7 +335,9 @@ test('Dataset drill info API call is made when user has drill permissions', asyn
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
await new Promise(resolve => setTimeout(resolve, 0));
@@ -321,7 +355,9 @@ test('Dataset drill info API call is not made when user lacks drill permissions'
},
});
result.current.onContextMenu(0, 0, {});
act(() => {
result.current.onContextMenu(0, 0, {});
});
await new Promise(resolve => setTimeout(resolve, 0));

View File

@@ -57,7 +57,7 @@ export interface DrillBySubmenuProps {
drillByConfig?: ContextMenuFilters['drillBy'];
formData: BaseFormData & { [key: string]: any };
onSelection?: (...args: any) => void;
onClick?: (event: MouseEvent) => void;
onClick?: (event: React.MouseEvent) => void;
onCloseMenu?: () => void;
openNewModal?: boolean;
excludedColumns?: Column[];
@@ -93,8 +93,8 @@ export const DrillBySubmenu = ({
const showSearch = columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
const handleSelection = useCallback(
(event, column) => {
onClick(event as MouseEvent);
(event: React.MouseEvent, column: Column) => {
onClick(event);
onSelection(column, drillByConfig);
if (openNewModal && onDrillBy && dataset) {
onDrillBy(column, dataset);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import {
render,
screen,

View File

@@ -74,7 +74,7 @@ export default function TableControls({
);
const removeFilter = useCallback(
colName => {
(colName: string) => {
const updatedFilterMap = { ...filterMap };
delete updatedFilterMap[colName];
setFilters(Object.values(updatedFilterMap));
@@ -125,7 +125,7 @@ export default function TableControls({
>
{colName}
</span>
<strong data-test="filter-val">{val}</strong>
<strong data-test="filter-val">{String(val)}</strong>
</Tag>
))}
</div>

View File

@@ -116,7 +116,7 @@ export const useDrillDetailMenuItems = ({
);
const openModal = useCallback(
(filters, event) => {
(filters: BinaryQueryObjectFilterClause[], event: MouseEvent) => {
onClick(event);
onSelection();
setFilters(filters);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import {
act,
render,
screen,
waitFor,
@@ -120,11 +119,9 @@ describe('DatasourceModal', () => {
onDatasourceSave as unknown as typeof mockedProps.onDatasourceSave,
});
const saveButton = screen.getByTestId('datasource-modal-save');
await act(async () => {
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
okButton.click();
});
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
fireEvent.click(okButton);
await waitFor(() => {
expect(onDatasourceSave).toHaveBeenCalled();
});
@@ -135,18 +132,14 @@ describe('DatasourceModal', () => {
.spyOn(SupersetClient, 'put')
.mockRejectedValue(new Error('Something went wrong'));
await act(async () => {
const saveButton = screen.getByTestId('datasource-modal-save');
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
okButton.click();
});
const saveButton = screen.getByTestId('datasource-modal-save');
fireEvent.click(saveButton);
const okButton = await screen.findByRole('button', { name: 'OK' });
fireEvent.click(okButton);
await act(async () => {
const errorElements = await screen.findAllByText('Error saving dataset');
const errorDiv = errorElements.find(el => el.closest('div'));
expect(errorDiv).toBeInTheDocument();
});
const errorElements = await screen.findAllByText('Error saving dataset');
const errorDiv = errorElements.find(el => el.closest('div'));
expect(errorDiv).toBeInTheDocument();
putSpy.mockRestore();
});

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import type { DragStartEvent } from '@dnd-kit/core';
import { FlattenedTreeItem } from '../constants';
import { FoldersEditorItemType } from '../../types';

View File

@@ -741,7 +741,7 @@ function ColumnCollectionTable({
{v}
</StyledLabelWrapper>
),
type: d => (d ? <Label>{d}</Label> : null),
type: d => (d ? <Label>{String(d)}</Label> : null),
advanced_data_type: d => <Label>{d as string}</Label>,
is_dttm: checkboxGenerator,
filterable: checkboxGenerator,
@@ -770,7 +770,7 @@ function ColumnCollectionTable({
{v}
</StyledLabelWrapper>
),
type: d => (d ? <Label>{d}</Label> : null),
type: d => (d ? <Label>{String(d)}</Label> : null),
is_dttm: checkboxGenerator,
filterable: checkboxGenerator,
groupby: checkboxGenerator,

View File

@@ -52,7 +52,7 @@ export default function Field<V>({
errorMessage,
}: FieldProps<V>) {
const onControlChange = useCallback(
newValue => {
(newValue: V) => {
onChange(fieldKey, newValue);
},
[onChange, fieldKey],

View File

@@ -16,7 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useContext, useEffect, useReducer, createContext, FC } from 'react';
import {
useContext,
useEffect,
useReducer,
createContext,
FC,
ReactNode,
} from 'react';
import {
ChartMetadata,
@@ -122,7 +129,9 @@ const sharedModules = {
'@superset-ui/core': () => import('@superset-ui/core'),
};
export const DynamicPluginProvider: FC = ({ children }) => {
export const DynamicPluginProvider: FC<{ children?: ReactNode }> = ({
children,
}) => {
const [pluginState, dispatch] = useReducer(
pluginContextReducer,
dummyPluginContext,

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, useCallback, memo } from 'react';
import { useMemo, useCallback, useRef, memo } from 'react';
import { GridSize } from 'src/components/GridTable/constants';
import { GridTable } from 'src/components/GridTable';
import { type ColDef } from 'src/components/GridTable/types';
@@ -109,15 +109,15 @@ export const FilterableTable = ({
[orderedColumnKeys, allowHTML, getCellContent],
);
const keywordFilter = useCallback(
node => {
if (filterText && node.data) {
return hasMatch(filterText, node.data);
}
return true;
},
[filterText],
);
const keyword = useRef<string | undefined>(filterText);
keyword.current = filterText;
const keywordFilter = useCallback((node: { data: Datum }) => {
if (keyword.current && node.data) {
return hasMatch(keyword.current, node.data);
}
return true;
}, []);
return (
<div className="filterable-table-container" data-test="table-container">

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useCellContentParser } from './useCellContentParser';
test('should return NULL for null cell data', () => {

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import {
type MouseEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, useTheme } from '@apache-superset/core/theme';
import type { Column, GridApi } from 'ag-grid-community';
@@ -98,7 +104,7 @@ export const Header: React.FC<Params> = ({
const [currentSort, setCurrentSort] = useState<string | null>(null);
const [sortIndex, setSortIndex] = useState<number | null>();
const onSort = useCallback(
event => {
(event: MouseEvent) => {
sortOption.current = (sortOption.current + 1) % SORT_DIRECTION.length;
const sort = SORT_DIRECTION[sortOption.current];
setSort(sort, event.shiftKey);

Some files were not shown because too many files have changed in this diff Show More