Compare commits

...

19 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
4e83afad78 feat: dnd kit migration start 2025-11-25 13:42:15 +03:00
Mehmet Salih Yavuz
ab19bc2f01 fix: update swc plugins 2025-11-24 17:44:25 +03:00
Mehmet Salih Yavuz
dbe27d599a Merge branch 'master' into msyavuz/chore/react-18 2025-11-24 12:05:19 +03:00
Mehmet Salih Yavuz
eac612552b refactor(DragDroppable): typescript migration 2025-09-13 16:22:40 +03:00
Mehmet Salih Yavuz
92c062a6da revert: DragDroppable 2025-09-13 16:22:40 +03:00
Mehmet Salih Yavuz
cffd58a319 Merge branch 'master' into msyavuz/chore/react-18 2025-09-13 02:35:38 +03:00
Mehmet Salih Yavuz
cacd1b1638 fix: webpack errors and linting 2025-09-07 17:31:13 +03:00
Mehmet Salih Yavuz
bb7b875d40 fix: createRoot and react-dnd 2025-09-07 16:46:40 +03:00
Mehmet Salih Yavuz
ddd82a05bb fix: type errors 2025-09-07 16:27:33 +03:00
Mehmet Salih Yavuz
87d1210033 chore: refactor a lot of stuff 2025-09-07 14:51:25 +03:00
Mehmet Salih Yavuz
cd573ea7c6 Merge branch 'master' into msyavuz/chore/react-18 2025-09-07 13:48:10 +03:00
Mehmet Salih Yavuz
0980c525c9 fix: types and versions 2025-07-06 19:36:14 +03:00
Mehmet Salih Yavuz
dc1bbda946 fix: types 2025-07-06 18:32:51 +03:00
Mehmet Salih Yavuz
5078dc3613 fix: update webpack 2025-07-06 15:25:22 +03:00
Mehmet Salih Yavuz
d4a53a1b13 Merge branch 'master' into msyavuz/chore/react-18 2025-07-06 15:22:18 +03:00
Mehmet Salih Yavuz
0863d17598 fix: rendering for 18 2025-07-06 15:17:57 +03:00
Mehmet Salih Yavuz
b11ae1ecab refactor: initial upgrade 2025-06-07 16:50:41 +03:00
Mehmet Salih Yavuz
fa92bb90db Merge branch 'master' into msyavuz/chore/react-18 2025-06-07 14:00:03 +03:00
Mehmet Salih Yavuz
c9012bc591 chore: update react 2025-06-07 13:59:35 +03:00
150 changed files with 7255 additions and 22599 deletions

View File

@@ -54,7 +54,6 @@ module.exports = {
['@babel/plugin-transform-runtime', { corejs: 3 }], ['@babel/plugin-transform-runtime', { corejs: 3 }],
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx // only used in packages/superset-ui-core/src/chart/components/reactify.tsx
['babel-plugin-typescript-to-proptypes', { loose: true }], ['babel-plugin-typescript-to-proptypes', { loose: true }],
'react-hot-loader/babel',
[ [
'@emotion/babel-plugin', '@emotion/babel-plugin',
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -138,7 +138,7 @@
"@visx/xychart": "^3.5.1", "@visx/xychart": "^3.5.1",
"ag-grid-community": "34.2.0", "ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0", "ag-grid-react": "34.2.0",
"antd": "^5.26.0", "antd": "^5.26.3",
"chrono-node": "^2.7.8", "chrono-node": "^2.7.8",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"content-disposition": "^0.5.4", "content-disposition": "^0.5.4",
@@ -179,15 +179,17 @@
"ol": "^7.5.2", "ol": "^7.5.2",
"polished": "^4.3.1", "polished": "^4.3.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^7.1.3",
"re-resizable": "^6.10.1", "re-resizable": "^6.10.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-ace": "^10.1.0",
"react-checkbox-tree": "^1.8.0", "react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0", "react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^11.1.3", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^11.1.3", "react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.20.0", "react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1", "react-lines-ellipsis": "^0.16.1",
@@ -237,10 +239,8 @@
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@babel/runtime-corejs3": "^7.28.2", "@babel/runtime-corejs3": "^7.28.2",
"@babel/types": "^7.26.9", "@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.13.0", "@emotion/jest": "^11.13.0",
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1", "@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1", "@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.56.0", "@playwright/test": "^1.56.0",
@@ -256,12 +256,11 @@
"@storybook/react-webpack5": "8.1.11", "@storybook/react-webpack5": "8.1.11",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@swc/core": "^1.14.0", "@swc/core": "^1.14.0",
"@swc/plugin-emotion": "^12.0.0", "@swc/plugin-emotion": "^13.1.0",
"@swc/plugin-transform-imports": "^10.0.0", "@swc/plugin-transform-imports": "^11.1.0",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9", "@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.7",
@@ -271,8 +270,9 @@
"@types/math-expression-evaluator": "^1.3.3", "@types/math-expression-evaluator": "^1.3.3",
"@types/mousetrap": "^1.6.15", "@types/mousetrap": "^1.6.15",
"@types/node": "^24.8.1", "@types/node": "^24.8.1",
"@types/react": "^17.0.83", "@types/react": "^18.2.0",
"@types/react-dom": "^17.0.26", "@types/react-dom": "^18.2.0",
"@types/react-gravatar": "^2.6.14",
"@types/react-json-tree": "^0.13.0", "@types/react-json-tree": "^0.13.0",
"@types/react-loadable": "^5.5.11", "@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10", "@types/react-redux": "^7.1.10",
@@ -378,6 +378,10 @@
"npm": "^10.8.1" "npm": "^10.8.1"
}, },
"overrides": { "overrides": {
"react-sortable-hoc": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"core-js": "^3.38.1", "core-js": "^3.38.1",
"d3-color": "^3.1.0", "d3-color": "^3.1.0",
"puppeteer": "^22.4.1", "puppeteer": "^22.4.1",

View File

@@ -23,8 +23,7 @@
"@types/lodash": "^4.17.20", "@types/lodash": "^4.17.20",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*", "@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*", "@testing-library/user-event": "*",
"@types/react": "*", "@types/react": "*",
"@types/react-loadable": "*", "@types/react-loadable": "*",
@@ -32,12 +31,12 @@
"@types/tinycolor2": "*" "@types/tinycolor2": "*"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.2.0",
"@emotion/cache": "^11.4.0", "@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"react": "^17.0.2", "react-dom": "^18.2.0",
"react-dom": "^17.0.2",
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
"tinycolor2": "*", "tinycolor2": "*",
"@fontsource/fira-code": "^5.2.6", "@fontsource/fira-code": "^5.2.6",

View File

@@ -35,15 +35,14 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*", "@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*", "@testing-library/user-event": "*",
"ace-builds": "^1.4.14", "ace-builds": "^1.4.14",
"brace": "^0.11.1", "brace": "^0.11.1",
"memoize-one": "^5.1.1", "memoize-one": "^5.1.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-dom": "^17.0.2" "react-dom": "^18.2.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -97,16 +97,15 @@
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*", "@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*", "@testing-library/user-event": "*",
"@types/react": "*", "@types/react": "*",
"@types/react-loadable": "*", "@types/react-loadable": "*",
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
"@types/tinycolor2": "*", "@types/tinycolor2": "*",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
"tinycolor2": "*" "tinycolor2": "*"
}, },

View File

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

View File

@@ -19,12 +19,12 @@
import { import {
useEffect, useEffect,
useState, useState,
RefObject,
forwardRef, forwardRef,
ComponentType, ComponentType,
ForwardRefExoticComponent, ForwardRefExoticComponent,
PropsWithoutRef, PropsWithoutRef,
RefAttributes, RefAttributes,
ForwardedRef,
} from 'react'; } from 'react';
import { Loading } from '../Loading'; import { Loading } from '../Loading';
@@ -54,7 +54,7 @@ function DefaultPlaceholder({
* first (if provided) and re-render once import is complete. * first (if provided) and re-render once import is complete.
*/ */
export function AsyncEsmComponent< export function AsyncEsmComponent<
P = PlaceholderProps, P = Record<string, unknown>,
M = ComponentType<P> | { default: ComponentType<P> }, M = ComponentType<P> | { default: ComponentType<P> },
>( >(
/** /**
@@ -98,8 +98,8 @@ export function AsyncEsmComponent<
}; };
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent( const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps, props: PropsWithoutRef<FullProps>,
ref: RefObject<ComponentType<FullProps>>, ref: ForwardedRef<ComponentType<FullProps>>,
) { ) {
const [loaded, setLoaded] = useState(component !== undefined); const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,17 +16,4 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { Form as AntdForm } from 'antd'; export { Form } from 'antd';
import { FormProps } from './types';
function CustomForm(props: FormProps) {
return <AntdForm {...props} />;
}
export const Form = Object.assign(CustomForm, {
useForm: AntdForm.useForm,
Item: AntdForm.Item,
List: AntdForm.List,
ErrorList: AntdForm.ErrorList,
Provider: AntdForm.Provider,
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
export type { FormProps, FormInstance, FormItemProps } from 'antd/es/form'; export type { FormProps, FormInstance, FormItemProps } from 'antd';
export interface LabeledErrorBoundInputProps { export interface LabeledErrorBoundInputProps {
label?: string; label?: string;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { FC } from 'react'; import { FC, PropsWithChildren } from 'react';
import { styled, useTheme, css } from '@apache-superset/core/ui'; import { styled, useTheme, css } from '@apache-superset/core/ui';
import { Skeleton } from '../Skeleton'; import { Skeleton } from '../Skeleton';
import { Card } from '../Card'; import { Card } from '../Card';
@@ -134,7 +134,7 @@ const ThinSkeleton = styled(Skeleton)`
const paragraphConfig = { rows: 1, width: 150 }; const paragraphConfig = { rows: 1, width: 150 };
const AnchorLink: FC<LinkProps> = ({ to, children }) => ( const AnchorLink: FC<PropsWithChildren<LinkProps>> = ({ to, children }) => (
<a href={to}>{children}</a> <a href={to}>{children}</a>
); );

View File

@@ -16,7 +16,12 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import type { ReactNode, ComponentType, ReactElement } from 'react'; import type {
ReactNode,
ComponentType,
ReactElement,
PropsWithChildren,
} from 'react';
import type { BackgroundPosition } from './ImageLoader'; import type { BackgroundPosition } from './ImageLoader';
export interface LinkProps { export interface LinkProps {
@@ -27,7 +32,7 @@ export interface ListViewCardProps {
title?: ReactNode; title?: ReactNode;
subtitle?: ReactNode; subtitle?: ReactNode;
url?: string; url?: string;
linkComponent?: ComponentType<LinkProps>; linkComponent?: ComponentType<PropsWithChildren<LinkProps>>;
imgURL?: string | null; imgURL?: string | null;
imgFallbackURL?: string; imgFallbackURL?: string;
imgPosition?: BackgroundPosition; imgPosition?: BackgroundPosition;

View File

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

View File

@@ -54,7 +54,7 @@ export function FormModal({
}, [onSave, resetForm]); }, [onSave, resetForm]);
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
async values => { async (values: Object) => {
try { try {
setIsSaving(true); setIsSaving(true);
await formSubmitHandler(values); await formSubmitHandler(values);
@@ -113,7 +113,7 @@ export function FormModal({
onValuesChange={onFormChange} onValuesChange={onFormChange}
onFieldsChange={onFormChange} onFieldsChange={onFormChange}
> >
{typeof children === 'function' ? children(form) : children} {children}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { render, screen, fireEvent } from '@superset-ui/core/spec'; 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 { TableInstance, useTable } from 'react-table';
import TableCollection from '.'; import TableCollection from '.';

View File

@@ -33,6 +33,7 @@ import {
UseResizeColumnsColumnOptions, UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps, UseResizeColumnsColumnProps,
} from 'react-table'; } from 'react-table';
import { ColumnsType } from 'antd/es/table';
import { SortOrder } from '../Table'; import { SortOrder } from '../Table';
@@ -87,11 +88,11 @@ export function mapColumns<T extends object>(
columns: EnhancedColumnInstance<T>[], columns: EnhancedColumnInstance<T>[],
headerGroups: EnhancedHeaderGroup<T>[], headerGroups: EnhancedHeaderGroup<T>[],
columnsForWrapText?: string[], columnsForWrapText?: string[],
) { ): ColumnsType<object> {
return columns.map(column => { return columns.map(column => {
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id); const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
return { return {
title: column.Header, title: column.Header as ReactNode,
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id, dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden, hidden: column.hidden,
key: column.id, key: column.id,
@@ -121,7 +122,7 @@ export function mapColumns<T extends object>(
column, column,
}); });
} }
return val; return String(val);
}, },
className: column.className, className: column.className,
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import {
MouseEventHandler, MouseEventHandler,
ReactElement, ReactElement,
ComponentType, ComponentType,
PropsWithChildren,
} from 'react'; } from 'react';
import type { Editor } from 'brace'; import type { Editor } from 'brace';
import type { QueryData } from '../chart/types/QueryResponse'; import type { QueryData } from '../chart/types/QueryResponse';
@@ -249,7 +250,7 @@ export type Extensions = Partial<{
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>; 'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
'navbar.right': ComponentType; 'navbar.right': ComponentType;
'report-modal.dropdown.item.icon': ComponentType; 'report-modal.dropdown.item.icon': ComponentType;
'root.context.provider': ComponentType; 'root.context.provider': ComponentType<PropsWithChildren>;
'welcome.message': ComponentType; 'welcome.message': ComponentType;
'welcome.banner': ComponentType; 'welcome.banner': ComponentType;
'welcome.main.replacement': ComponentType; 'welcome.main.replacement': ComponentType;

View File

@@ -46,8 +46,8 @@
"gh-pages": "^6.3.0", "gh-pages": "^6.3.0",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
"react-resizable": "^3.0.5" "react-resizable": "^3.0.5"
}, },

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@apache-superset/core": "*", "@apache-superset/core": "*",
"mapbox-gl": "*", "mapbox-gl": "*",
"react": "^17.0.2" "react": "^18.2.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

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

View File

@@ -35,7 +35,7 @@
"peerDependencies": { "peerDependencies": {
"@superset-ui/chart-controls": "*", "@superset-ui/chart-controls": "*",
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@apache-superset/core": "*", "react": "^18.2.0",
"react": "^17.0.2" "@apache-superset/core": "*"
} }
} }

View File

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

View File

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

View File

@@ -38,7 +38,7 @@
"peerDependencies": { "peerDependencies": {
"@superset-ui/chart-controls": "*", "@superset-ui/chart-controls": "*",
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@apache-superset/core": "*", "react": "^18.2.0",
"react": "^17.0.2" "@apache-superset/core": "*"
} }
} }

View File

@@ -64,8 +64,8 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@apache-superset/core": "*", "@apache-superset/core": "*",
"mapbox-gl": "*", "mapbox-gl": "*",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-map-gl": "^6.1.19" "react-map-gl": "^6.1.19"
}, },
"publishConfig": { "publishConfig": {

View File

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

View File

@@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react';
import { PickingInfo } from '@deck.gl/core'; import { PickingInfo } from '@deck.gl/core';
import { JsonObject, QueryFormData } from '@superset-ui/core'; import { JsonObject, QueryFormData } from '@superset-ui/core';
import { import {
@@ -98,9 +99,9 @@ describe('getAggFunc', () => {
describe('commonLayerProps', () => { describe('commonLayerProps', () => {
const mockSetTooltip = jest.fn(); const mockSetTooltip = jest.fn();
const mockSetTooltipContent = jest.fn( const mockSetTooltipContent = jest
() => (o: JsonObject) => `Tooltip for ${o}`, .fn()
); .mockReturnValue((o: JsonObject) => `Tooltip for ${o}` as React.ReactNode);
const mockOnSelect = jest.fn(); const mockOnSelect = jest.fn();
it('returns correct props when js_tooltip is provided', () => { it('returns correct props when js_tooltip is provided', () => {

View File

@@ -42,7 +42,7 @@
"peerDependencies": { "peerDependencies": {
"@superset-ui/chart-controls": "*", "@superset-ui/chart-controls": "*",
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@apache-superset/core": "*", "react": "^18.2.0",
"react": "^17.0.2" "@apache-superset/core": "*"
} }
} }

View File

@@ -42,14 +42,13 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*", "@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*", "@testing-library/user-event": "*",
"@types/classnames": "*", "@types/classnames": "*",
"@types/react": "*", "@types/react": "*",
"match-sorter": "^6.3.3", "match-sorter": "^6.3.3",
"react": "^17.0.2", "react": "^18.0.0",
"react-dom": "^17.0.2" "react-dom": "^18.0.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -37,10 +37,8 @@ import {
type ColDef, type ColDef,
type ColumnState, type ColumnState,
ModuleRegistry, ModuleRegistry,
GridReadyEvent,
GridState, GridState,
CellClickedEvent, GridReadyEvent,
IMenuActionParams,
} from '@superset-ui/core/components/ThemedAgGridReact'; } from '@superset-ui/core/components/ThemedAgGridReact';
import { import {
AgGridChartState, AgGridChartState,
@@ -74,7 +72,7 @@ export interface AgGridTableProps {
gridHeight?: number; gridHeight?: number;
updateInterval?: number; updateInterval?: number;
data?: any[]; data?: any[];
onGridReady?: (params: GridReadyEvent) => void; onGridReady?: (params: any) => void;
colDefsFromProps: any[]; colDefsFromProps: any[];
includeSearch: boolean; includeSearch: boolean;
allowRearrangeColumns: boolean; allowRearrangeColumns: boolean;
@@ -93,7 +91,7 @@ export interface AgGridTableProps {
percentMetrics: string[]; percentMetrics: string[];
serverPageLength: number; serverPageLength: number;
hasServerPageLengthChanged: boolean; hasServerPageLengthChanged: boolean;
handleCrossFilter: (event: CellClickedEvent | IMenuActionParams) => void; handleCrossFilter: (event: any) => void;
isActiveFilterValue: (key: string, val: DataRecordValue) => boolean; isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
renderTimeComparisonDropdown: () => JSX.Element | null; renderTimeComparisonDropdown: () => JSX.Element | null;
cleanedTotals: DataRecord; cleanedTotals: DataRecord;
@@ -279,7 +277,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
}; };
const handleColumnHeaderClick = useCallback( const handleColumnHeaderClick = useCallback(
params => { (params: any) => {
const colId = params?.column?.colId; const colId = params?.column?.colId;
const sortDir = params?.column?.sort; const sortDir = params?.column?.sort;
handleColSort(colId, sortDir); handleColSort(colId, sortDir);
@@ -423,8 +421,8 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
rowData={rowData} rowData={rowData}
headerHeight={36} headerHeight={36}
rowHeight={30} rowHeight={30}
columnDefs={colDefsFromProps} columnDefs={colDefsFromProps as any}
defaultColDef={defaultColDef} defaultColDef={defaultColDef as any}
onColumnGroupOpened={params => params.api.sizeColumnsToFit()} onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
rowSelection="multiple" rowSelection="multiple"
animateRows animateRows

View File

@@ -172,7 +172,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
); );
const timestampFormatter = useCallback( const timestampFormatter = useCallback(
value => getTimeFormatterForGranularity(timeGrain)(value), (value: any) => getTimeFormatterForGranularity(timeGrain)(value),
[timeGrain], [timeGrain],
); );

View File

@@ -67,5 +67,5 @@ export const TextCellRenderer = (params: CellRendererProps) => {
} }
} }
return <div>{valueFormatted ?? value}</div>; return <div>{String(valueFormatted ?? value)}</div>;
}; };

View File

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

View File

@@ -19,7 +19,7 @@
import Layer from 'ol/layer/Layer'; import Layer from 'ol/layer/Layer';
import { FrameState } from 'ol/Map'; import { FrameState } from 'ol/Map';
import { apply as applyTransform } from 'ol/transform'; import { apply as applyTransform } from 'ol/transform';
import ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client';
import { SupersetTheme } from '@apache-superset/core/ui'; import { SupersetTheme } from '@apache-superset/core/ui';
import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types'; import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types';
import { createChartComponent } from '../util/chartUtil'; import { createChartComponent } from '../util/chartUtil';
@@ -33,6 +33,8 @@ import Loader from '../images/loading.gif';
export class ChartLayer extends Layer { export class ChartLayer extends Layer {
charts: any[] = []; charts: any[] = [];
chartRoots: Map<HTMLElement, any> = new Map();
chartConfigs: ChartConfig = { chartConfigs: ChartConfig = {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [], features: [],
@@ -166,7 +168,11 @@ export class ChartLayer extends Layer {
*/ */
removeAllChartElements() { removeAllChartElements() {
this.charts.forEach(chart => { this.charts.forEach(chart => {
ReactDOM.unmountComponentAtNode(chart.htmlElement); const root = this.chartRoots.get(chart.htmlElement);
if (root) {
root.unmount();
this.chartRoots.delete(chart.htmlElement);
}
chart.htmlElement.remove(); chart.htmlElement.remove();
}); });
this.charts = []; this.charts = [];
@@ -191,7 +197,9 @@ export class ChartLayer extends Layer {
this.theme, this.theme,
this.locale, this.locale,
); );
ReactDOM.render(chartComponent, container); const root = createRoot(container);
this.chartRoots.set(container, root);
root.render(chartComponent);
return { return {
htmlElement: container, htmlElement: container,
@@ -227,7 +235,10 @@ export class ChartLayer extends Layer {
this.theme, this.theme,
this.locale, this.locale,
); );
ReactDOM.render(chartComponent, chart.htmlElement); const root = this.chartRoots.get(chart.htmlElement);
if (root) {
root.render(chartComponent);
}
return { return {
...chart, ...chart,

View File

@@ -35,7 +35,7 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"echarts": "*", "echarts": "*",
"memoize-one": "*", "memoize-one": "*",
"react": "^17.0.2" "react": "^18.2.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

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

View File

@@ -46,7 +46,7 @@ export default function EchartsTreemap({
coltypeMapping, coltypeMapping,
}: TreemapTransformedProps) { }: TreemapTransformedProps) {
const getCrossFilterDataMask = useCallback( const getCrossFilterDataMask = useCallback(
(data, treePathInfo) => { (data: any, treePathInfo: any) => {
if (data?.children) { if (data?.children) {
return undefined; return undefined;
} }
@@ -96,7 +96,7 @@ export default function EchartsTreemap({
); );
const handleChange = useCallback( const handleChange = useCallback(
(data, treePathInfo) => { (data: any, treePathInfo: any) => {
if (!emitCrossFilters || groupby.length === 0) { if (!emitCrossFilters || groupby.length === 0) {
return; return;
} }

View File

@@ -27,6 +27,7 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"currencyformatter.js": "^1.0.5",
"handlebars-group-by": "^1.0.1", "handlebars-group-by": "^1.0.1",
"just-handlebars-helpers": "^1.0.19" "just-handlebars-helpers": "^1.0.19"
}, },
@@ -35,12 +36,12 @@
"@apache-superset/core": "*", "@apache-superset/core": "*",
"@superset-ui/core": "*", "@superset-ui/core": "*",
"ace-builds": "^1.4.14", "ace-builds": "^1.4.14",
"dayjs": "^1.11.13",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"dayjs": "^1.11.13", "react": "^18.2.0",
"react": "^17.0.2",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-dom": "^17.0.2" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",

View File

@@ -70,7 +70,7 @@ ${helperDescriptions
<div> <div>
<ControlHeader> <ControlHeader>
<div> <div>
{props.label} {props.label as any}
<InfoTooltip <InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }} iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={<SafeMarkdown source={helpersTooltipContent} />} tooltip={<SafeMarkdown source={helpersTooltipContent} />}

View File

@@ -47,7 +47,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
<div> <div>
<ControlHeader> <ControlHeader>
<div> <div>
{props.label} {props.label as any}
<InfoTooltip <InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }} iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={t('You need to configure HTML sanitization to use CSS')} tooltip={t('You need to configure HTML sanitization to use CSS')}

View File

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

View File

@@ -42,14 +42,13 @@
"@superset-ui/core": "*", "@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1", "@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*", "@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*", "@testing-library/user-event": "*",
"@types/classnames": "*", "@types/classnames": "*",
"@types/react": "*", "@types/react": "*",
"match-sorter": "^6.3.3", "match-sorter": "^6.3.3",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2" "react-dom": "^18.2.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -347,7 +347,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
); );
const timestampFormatter = useCallback( const timestampFormatter = useCallback(
value => getTimeFormatterForGranularity(timeGrain)(value), (value: any) => getTimeFormatterForGranularity(timeGrain)(value),
[timeGrain], [timeGrain],
); );
const [tableSize, setTableSize] = useState<TableSize>({ const [tableSize, setTableSize] = useState<TableSize>({

View File

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

View File

@@ -35,8 +35,7 @@ import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
import { ThemeController } from 'src/theme/ThemeController'; import { ThemeController } from 'src/theme/ThemeController';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { DndProvider } from 'react-dnd'; import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { HTML5Backend } from 'react-dnd-html5-backend';
import reducerIndex from 'spec/helpers/reducerIndex'; import reducerIndex from 'spec/helpers/reducerIndex';
import { QueryParamProvider } from 'use-query-params'; import { QueryParamProvider } from 'use-query-params';
import { configureStore, Store } from '@reduxjs/toolkit'; import { configureStore, Store } from '@reduxjs/toolkit';
@@ -99,7 +98,17 @@ export function createWrapper(options?: Options) {
} }
if (useDnd) { if (useDnd) {
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>; const DndWrapper = ({ children }: { children: ReactNode }) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
);
return <DndContext sensors={sensors}>{children}</DndContext>;
};
result = <DndWrapper>{result}</DndWrapper>;
} }
if (useRedux || store) { if (useRedux || store) {

View File

@@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import fetchMock from 'fetch-mock'; 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 { COMMON_ERR_MESSAGES } from '@superset-ui/core';
import { import {
createWrapper, createWrapper,
@@ -119,7 +119,7 @@ test('skips fetching validation if validator is undefined', () => {
}); });
test('returns validation if validator is configured', async () => { test('returns validation if validator is configured', async () => {
const { result, waitFor } = initialize(true); const { result } = initialize(true);
await waitFor(() => await waitFor(() =>
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1), expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1),
); );
@@ -142,7 +142,7 @@ test('returns server error description', async () => {
}, },
{ overwriteRoutes: true }, { overwriteRoutes: true },
); );
const { result, waitFor } = initialize(true); const { result } = initialize(true);
await waitFor( await waitFor(
() => () =>
expect(result.current.data).toEqual([ expect(result.current.data).toEqual([
@@ -166,7 +166,7 @@ test('returns session expire description when CSRF token expired', async () => {
}, },
{ overwriteRoutes: true }, { overwriteRoutes: true },
); );
const { result, waitFor } = initialize(true); const { result } = initialize(true);
await waitFor( await waitFor(
() => () =>
expect(result.current.data).toEqual([ expect(result.current.data).toEqual([

View File

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

View File

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

View File

@@ -48,14 +48,25 @@ import { StaticPosition, StyledTooltip } from './styles';
interface QueryTableQuery interface QueryTableQuery
extends Omit< extends Omit<
QueryResponse, QueryResponse,
'state' | 'sql' | 'progress' | 'results' | 'duration' | 'started' | 'state'
| 'sql'
| 'progress'
| 'results'
| 'duration'
| 'started'
| 'user'
| 'db'
| 'querylink'
> { > {
state?: Record<string, any>; state?: Record<string, any>;
sql?: Record<string, any>; sql?: ReactNode;
progress?: Record<string, any>; progress?: Record<string, any>;
results?: Record<string, any>; results?: Record<string, any>;
duration?: ReactNode; duration?: ReactNode;
started?: ReactNode; started?: ReactNode;
user?: ReactNode;
db?: ReactNode;
querylink?: ReactNode;
} }
interface QueryTableProps { interface QueryTableProps {
@@ -235,7 +246,7 @@ const QueryTable = ({
return queries return queries
.map(query => { .map(query => {
const { state, sql, progress, ...rest } = query; const { state, sql, progress, ...rest } = query;
const q = rest as QueryTableQuery; const q = { ...rest } as unknown as QueryTableQuery;
const status = statusAttributes[state] || statusAttributes.error; const status = statusAttributes[state] || statusAttributes.error;

View File

@@ -166,7 +166,7 @@ const RunQueryActionButton = ({
} }
: { : {
buttonStyle: shouldShowStopBtn ? 'danger' : 'primary', buttonStyle: shouldShowStopBtn ? 'danger' : 'primary',
icon, icon: icon as any,
})} })}
> >
{text} {text}

View File

@@ -130,7 +130,7 @@ const SouthPane = ({
dispatch(setActiveSouthPaneTab(id)); dispatch(setActiveSouthPaneTab(id));
}; };
const removeTable = useCallback( const removeTable = useCallback(
(key, action) => { (key: any, action: any) => {
if (action === 'remove') { if (action === 'remove') {
const table = pinnedTables.find( const table = pinnedTables.find(
({ dbId, catalog, schema, name }) => ({ dbId, catalog, schema, name }) =>

View File

@@ -580,7 +580,7 @@ const SqlEditor: FC<Props> = ({
}; };
const setQueryEditorAndSaveSql = useCallback( const setQueryEditorAndSaveSql = useCallback(
sql => { (sql: any) => {
dispatch(queryEditorSetAndSaveSql(queryEditor, sql)); dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
}, },
[dispatch, queryEditor], [dispatch, queryEditor],

View File

@@ -95,7 +95,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
new: isNewQuery, new: isNewQuery,
...urlParams ...urlParams
} = { } = {
...this.context.requestedQuery, ...(this.context as any).requestedQuery,
...bootstrapData.requested_query, ...bootstrapData.requested_query,
...queryParameters, ...queryParameters,
} as Record<string, string>; } as Record<string, string>;
@@ -135,7 +135,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
schema, schema,
autorun, autorun,
sql, sql,
isDataset: this.context.isDataset, isDataset: (this.context as any).isDataset,
}; };
this.props.actions.addQueryEditor(newQueryEditor); this.props.actions.addQueryEditor(newQueryEditor);
} }

View File

@@ -19,7 +19,7 @@
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures'; 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 { createWrapper } from 'spec/helpers/testing-library';
import useQueryEditor from '.'; import useQueryEditor from '.';

View File

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

View File

@@ -18,7 +18,7 @@
*/ */
import { FeatureFlag, VizType } from '@superset-ui/core'; import { FeatureFlag, VizType } from '@superset-ui/core';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react';
import mockState from 'spec/fixtures/mockState'; import mockState from 'spec/fixtures/mockState';
import { sliceId } from 'spec/fixtures/mockChartQueries'; import { sliceId } from 'spec/fixtures/mockChartQueries';
import { noOp } from 'src/utils/common'; import { noOp } from 'src/utils/common';

View File

@@ -0,0 +1,274 @@
/**
* 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 {
CSSProperties,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Menu } from '@superset-ui/core/components/Menu';
import {
BaseFormData,
Behavior,
Column,
ContextMenuFilters,
css,
ensureIsArray,
getChartMetadataRegistry,
t,
useTheme,
} from '@superset-ui/core';
import { Constants, Input, Loading } from '@superset-ui/core/components';
import { debounce } from 'lodash';
import { FixedSizeList as List } from 'react-window';
import { Icons } from '@superset-ui/core/components/Icons';
import { InputRef } from 'antd';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { getSubmenuYOffset } from '../utils';
import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
import { Dataset } from '../types';
const SUBMENU_HEIGHT = 200;
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
const SEARCH_INPUT_HEIGHT = 48;
export interface DrillByMenuItemsProps {
drillByConfig?: ContextMenuFilters['drillBy'];
formData: BaseFormData & { [key: string]: any };
contextMenuY?: number;
submenuIndex?: number;
onSelection?: (...args: any) => void;
onClick?: (event: MouseEvent) => void;
onCloseMenu?: () => void;
openNewModal?: boolean;
excludedColumns?: Column[];
open: boolean;
onDrillBy?: (column: Column, dataset: Dataset) => void;
dataset?: Dataset;
isLoadingDataset?: boolean;
}
export const DrillByMenuItems = ({
drillByConfig,
formData,
contextMenuY = 0,
submenuIndex = 0,
onSelection = () => {},
onClick = () => {},
onCloseMenu = () => {},
excludedColumns,
openNewModal = true,
open,
onDrillBy,
dataset,
isLoadingDataset = false,
...rest
}: DrillByMenuItemsProps) => {
const theme = useTheme();
const [searchInput, setSearchInput] = useState('');
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
const ref = useRef<InputRef>(null);
const columns = dataset ? ensureIsArray(dataset.drillable_columns) : [];
const showSearch = columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
const handleSelection = useCallback(
(event: any, column: any) => {
onClick(event);
onSelection(column, drillByConfig);
if (openNewModal && onDrillBy && dataset) {
onDrillBy(column, dataset);
}
onCloseMenu();
},
[drillByConfig, onClick, onSelection, openNewModal, onDrillBy, dataset],
);
useEffect(() => {
if (open) {
ref.current?.input?.focus({ preventScroll: true });
} else {
// Reset search input when menu is closed
setSearchInput('');
setDebouncedSearchInput('');
}
}, [open]);
const hasDrillBy = drillByConfig?.groupbyFieldName;
const handlesDimensionContextMenu = useMemo(
() =>
getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DrillBy),
[formData.viz_type],
);
const debouncedSetSearchInput = useMemo(
() =>
debounce((value: string) => {
setDebouncedSearchInput(value);
}, Constants.FAST_DEBOUNCE),
[],
);
const handleInput = (value: string) => {
setSearchInput(value);
debouncedSetSearchInput(value);
};
const filteredColumns = useMemo(
() =>
columns.filter(column =>
(column.verbose_name || column.column_name)
.toLowerCase()
.includes(debouncedSearchInput.toLowerCase()),
),
[columns, debouncedSearchInput],
);
const submenuYOffset = useMemo(
() =>
getSubmenuYOffset(
contextMenuY,
filteredColumns.length || 1,
submenuIndex,
SUBMENU_HEIGHT,
showSearch ? SEARCH_INPUT_HEIGHT : 0,
),
[contextMenuY, filteredColumns.length, submenuIndex, showSearch],
);
let tooltip: ReactNode;
if (!handlesDimensionContextMenu) {
tooltip = t('Drill by is not yet supported for this chart type');
} else if (!hasDrillBy) {
tooltip = t('Drill by is not available for this data point');
}
if (!handlesDimensionContextMenu || !hasDrillBy) {
return (
<Menu.Item key="drill-by-disabled" disabled {...rest}>
<div>
{t('Drill by')}
<MenuItemTooltip title={tooltip} />
</div>
</Menu.Item>
);
}
const Row = ({
index,
data,
style,
}: {
index: number;
data: { columns: Column[] };
style: CSSProperties;
}) => {
const { columns, ...rest } = data;
const column = columns[index];
return (
<VirtualizedMenuItem
tooltipText={column.verbose_name || column.column_name}
onClick={e => handleSelection(e, column)}
style={style}
{...rest}
>
{column.verbose_name || column.column_name}
</VirtualizedMenuItem>
);
};
// Don't render drill by menu items when matrixify is enabled
if (formData.matrixify_enabled) {
return null;
}
return (
<>
<Menu.SubMenu
key="drill-by-submenu"
title={t('Drill by')}
popupClassName="chart-context-submenu"
popupOffset={[0, submenuYOffset]}
{...rest}
>
<div data-test="drill-by-submenu">
{showSearch && (
<Input
ref={ref}
prefix={
<Icons.SearchOutlined
iconSize="l"
iconColor={theme.colorIcon}
/>
}
onChange={e => {
e.stopPropagation();
handleInput(e.target.value);
}}
placeholder={t('Search columns')}
onClick={e => {
// prevent closing menu when clicking on input
e.nativeEvent.stopImmediatePropagation();
}}
allowClear
css={css`
width: auto;
max-width: 100%;
margin: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 3}px;
box-shadow: none;
`}
value={searchInput}
/>
)}
{isLoadingDataset ? (
<div
css={css`
padding: ${theme.sizeUnit * 3}px 0;
`}
>
<Loading position="inline-centered" />
</div>
) : filteredColumns.length ? (
<List
width="100%"
height={SUBMENU_HEIGHT}
itemSize={35}
itemCount={filteredColumns.length}
itemData={{ columns: filteredColumns, ...rest }}
overscanCount={20}
>
{Row}
</List>
) : (
<Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
{t('No columns found')}
</Menu.Item>
)}
</div>
</Menu.SubMenu>
</>
);
};

View File

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

View File

@@ -0,0 +1,253 @@
/**
* 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 {
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useMemo,
} from 'react';
import { isEmpty } from 'lodash';
import {
Behavior,
BinaryQueryObjectFilterClause,
css,
extractQueryFields,
getChartMetadataRegistry,
QueryFormData,
removeHTMLTags,
styled,
t,
} from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { Menu } from '@superset-ui/core/components/Menu';
import { RootState } from 'src/dashboard/types';
import { getSubmenuYOffset } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
import { Dataset } from '../types';
const DRILL_TO_DETAIL = t('Drill to detail');
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
const DISABLED_REASONS = {
DATABASE: t(
'Drill to detail is disabled for this database. Change the database settings to enable it.',
),
NO_AGGREGATIONS: t(
'Drill to detail is disabled because this chart does not group data by dimension value.',
),
NO_FILTERS: t(
'Right-click on a dimension value to drill to detail by that value.',
),
NOT_SUPPORTED: t(
'Drill to detail by value is not yet supported for this chart type.',
),
};
const DisabledMenuItem = ({
children,
menuKey,
...rest
}: {
children: ReactNode;
menuKey: string;
}) => (
<Menu.Item disabled key={menuKey} {...rest}>
<div
css={css`
white-space: normal;
max-width: 160px;
`}
>
{children}
</div>
</Menu.Item>
);
const Filter = ({
children,
stripHTML = false,
}: {
children: ReactNode;
stripHTML: boolean;
}) => {
const content =
stripHTML && typeof children === 'string'
? removeHTMLTags(children)
: children;
return <span>{content}</span>;
};
const StyledFilter = styled(Filter)`
${({ theme }) => `
font-weight: ${theme.fontWeightStrong};
color: ${theme.colorPrimary};
`}
`;
export type DrillDetailMenuItemsProps = {
formData: QueryFormData;
filters?: BinaryQueryObjectFilterClause[];
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
isContextMenu?: boolean;
contextMenuY?: number;
onSelection?: () => void;
onClick?: (event: MouseEvent) => void;
submenuIndex?: number;
setShowModal: (show: boolean) => void;
key?: string;
forceSubmenuRender?: boolean;
dataset?: Dataset;
isLoadingDataset?: boolean;
};
const DrillDetailMenuItems = ({
formData,
filters = [],
isContextMenu = false,
contextMenuY = 0,
onSelection = () => null,
onClick = () => null,
submenuIndex = 0,
setFilters,
setShowModal,
key,
...props
}: DrillDetailMenuItemsProps) => {
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
({ datasources }) =>
datasources[formData.datasource]?.database?.disable_drill_to_detail,
);
const openModal = useCallback(
(filters: any, event: any) => {
onClick(event);
onSelection();
setFilters(filters);
setShowModal(true);
},
[onClick, onSelection],
);
// Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu`
// event for dimensions. If it doesn't, tell the user that drill to detail by
// dimension is not supported. If it does, and the `contextmenu` handler didn't
// pass any filters, tell the user that they didn't select a dimension.
const handlesDimensionContextMenu = useMemo(
() =>
getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail),
[formData.viz_type],
);
// Check metrics to see if chart's current configuration lacks
// aggregations, in which case Drill to Detail should be disabled.
const noAggregations = useMemo(() => {
const { metrics } = extractQueryFields(formData);
return isEmpty(metrics);
}, [formData]);
// Ensure submenu doesn't appear offscreen
const submenuYOffset = useMemo(
() =>
getSubmenuYOffset(
contextMenuY,
filters.length > 1 ? filters.length + 1 : filters.length,
submenuIndex,
),
[contextMenuY, filters.length, submenuIndex],
);
let drillDisabled;
let drillByDisabled;
if (drillToDetailDisabled) {
drillDisabled = DISABLED_REASONS.DATABASE;
drillByDisabled = DISABLED_REASONS.DATABASE;
} else if (handlesDimensionContextMenu) {
if (noAggregations) {
drillDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
drillByDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
} else if (!filters?.length) {
drillByDisabled = DISABLED_REASONS.NO_FILTERS;
}
} else {
drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
}
const drillToDetailMenuItem = drillDisabled ? (
<DisabledMenuItem menuKey="drill-to-detail-disabled" {...props}>
{DRILL_TO_DETAIL}
<MenuItemTooltip title={drillDisabled} />
</DisabledMenuItem>
) : (
<Menu.Item key="drill-to-detail" onClick={openModal.bind(null, [])}>
{DRILL_TO_DETAIL}
</Menu.Item>
);
const drillToDetailByMenuItem = drillByDisabled ? (
<DisabledMenuItem menuKey="drill-to-detail-by-disabled" {...props}>
{DRILL_TO_DETAIL_BY}
<MenuItemTooltip title={drillByDisabled} />
</DisabledMenuItem>
) : (
<Menu.SubMenu
popupOffset={[0, submenuYOffset]}
popupClassName="chart-context-submenu"
title={DRILL_TO_DETAIL_BY}
key={key}
{...props}
>
<div data-test="drill-to-detail-by-submenu">
{filters.map((filter, i) => (
<MenuItemWithTruncation
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
menuKey={`drill-detail-filter-${i}`}
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
</MenuItemWithTruncation>
))}
{filters.length > 1 && (
<Menu.Item
key="drill-detail-filter-all"
onClick={openModal.bind(null, filters)}
>
<div>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
</div>
</Menu.Item>
)}
</div>
</Menu.SubMenu>
);
return (
<>
{drillToDetailMenuItem}
{isContextMenu && drillToDetailByMenuItem}
</>
);
};
export default DrillDetailMenuItems;

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,7 +112,7 @@ export const FilterableTable = ({
const keyword = useRef<string | undefined>(filterText); const keyword = useRef<string | undefined>(filterText);
keyword.current = filterText; keyword.current = filterText;
const keywordFilter = useCallback(node => { const keywordFilter = useCallback((node: any) => {
if (keyword.current && node.data) { if (keyword.current && node.data) {
return hasMatch(keyword.current, node.data); return hasMatch(keyword.current, node.data);
} }

View File

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

View File

@@ -98,7 +98,7 @@ export const Header: React.FC<Params> = ({
const [currentSort, setCurrentSort] = useState<string | null>(null); const [currentSort, setCurrentSort] = useState<string | null>(null);
const [sortIndex, setSortIndex] = useState<number | null>(); const [sortIndex, setSortIndex] = useState<number | null>();
const onSort = useCallback( const onSort = useCallback(
event => { (event: any) => {
sortOption.current = (sortOption.current + 1) % SORT_DIRECTION.length; sortOption.current = (sortOption.current + 1) % SORT_DIRECTION.length;
const sort = SORT_DIRECTION[sortOption.current]; const sort = SORT_DIRECTION[sortOption.current];
setSort(sort, event.shiftKey); setSort(sort, event.shiftKey);

View File

@@ -53,32 +53,32 @@ export function GridTable<RecordType extends object>({
[externalFilter], [externalFilter],
); );
const rowIndexLength = `${data.length}}`.length; const rowIndexLength = `${data.length}}`.length;
const onKeyDown: AgGridReactProps<Record<string, any>>['onCellKeyDown'] = const onKeyDown = useCallback((params: any) => {
useCallback(({ event, column, data, value, api }) => { const { event, column, data, value, api } = params;
if ( if (
!document.getSelection?.()?.toString?.() && !document.getSelection?.()?.toString?.() &&
event && event &&
event.key === 'c' && event.key === 'c' &&
(event.ctrlKey || event.metaKey) (event.ctrlKey || event.metaKey)
) { ) {
const columns = const columns =
column.getColId() === PIVOT_COL_ID column.getColId() === PIVOT_COL_ID
? api ? api
.getAllDisplayedColumns() .getAllDisplayedColumns()
.filter((column: Column) => column.getColId() !== PIVOT_COL_ID) .filter((column: Column) => column.getColId() !== PIVOT_COL_ID)
: [column]; : [column];
const record = const record =
column.getColId() === PIVOT_COL_ID column.getColId() === PIVOT_COL_ID
? [ ? [
columns.map((column: Column) => column.getColId()).join('\t'), columns.map((column: Column) => column.getColId()).join('\t'),
columns columns
.map((column: Column) => data?.[column.getColId()]) .map((column: Column) => data?.[column.getColId()])
.join('\t'), .join('\t'),
].join('\n') ].join('\n')
: String(value); : String(value);
copyTextToClipboard(() => Promise.resolve(record)); copyTextToClipboard(() => Promise.resolve(record));
} }
}, []); }, []);
const columnDefs = useMemo( const columnDefs = useMemo(
() => () =>
[ [
@@ -179,7 +179,7 @@ export function GridTable<RecordType extends object>({
isExternalFilterPresent={isExternalFilterPresent} isExternalFilterPresent={isExternalFilterPresent}
doesExternalFilterPass={externalFilter} doesExternalFilterPass={externalFilter}
components={gridComponents} components={gridComponents}
gridOptions={gridOptions} gridOptions={gridOptions as any}
onCellKeyDown={onKeyDown} onCellKeyDown={onKeyDown}
/> />
</div> </div>

View File

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

View File

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

View File

@@ -400,7 +400,7 @@ const DashboardBuilder = () => {
}, [dashboardLayout, dispatch]); }, [dashboardLayout, dispatch]);
const handleDrop = useCallback( const handleDrop = useCallback(
dropResult => dispatch(handleComponentDrop(dropResult)), (dropResult: any) => dispatch(handleComponentDrop(dropResult)),
[dispatch], [dispatch],
); );
@@ -559,7 +559,7 @@ const DashboardBuilder = () => {
: theme.sizeUnit * 8; : theme.sizeUnit * 8;
const renderChild = useCallback( const renderChild = useCallback(
adjustedWidth => { (adjustedWidth: any) => {
const filterBarWidth = dashboardFiltersOpen const filterBarWidth = dashboardFiltersOpen
? adjustedWidth ? adjustedWidth
: CLOSED_FILTER_BAR_WIDTH; : CLOSED_FILTER_BAR_WIDTH;

View File

@@ -279,7 +279,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, [onBeforeUnload]); }, [onBeforeUnload]);
const renderTabBar = useCallback(() => <></>, []); const renderTabBar = useCallback(() => <></>, []);
const handleFocus = useCallback(e => { const handleFocus = useCallback((e: any) => {
if ( if (
// prevent scrolling when tabbing to the tab pane // prevent scrolling when tabbing to the tab pane
e.target.classList.contains('ant-tabs-tabpane') && e.target.classList.contains('ant-tabs-tabpane') &&
@@ -293,7 +293,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, []); }, []);
const renderParentSizeChildren = useCallback( const renderParentSizeChildren = useCallback(
({ width }) => { ({ width }: { width: any }) => {
const tabItems = childIds.map((id, index) => ({ const tabItems = childIds.map((id, index) => ({
key: index === 0 ? DASHBOARD_GRID_ID : index.toString(), key: index === 0 ? DASHBOARD_GRID_ID : index.toString(),
label: null, label: null,

View File

@@ -22,7 +22,7 @@ import { css, styled } from '@apache-superset/core/ui';
import { Constants } from '@superset-ui/core/components'; import { Constants } from '@superset-ui/core/components';
import { RootState } from 'src/dashboard/types'; import { RootState } from 'src/dashboard/types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useDragDropManager } from 'react-dnd'; import { useDndMonitor } from '@dnd-kit/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
@@ -117,34 +117,30 @@ const DashboardWrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
const editMode = useSelector<RootState, boolean>( const editMode = useSelector<RootState, boolean>(
state => state.dashboardState.editMode, state => state.dashboardState.editMode,
); );
const dragDropManager = useDragDropManager(); const [isDragged, setIsDragged] = useState(false);
const [isDragged, setIsDragged] = useState(
dragDropManager.getMonitor().isDragging(), const debouncedSetIsDragged = debounce(
setIsDragged,
Constants.FAST_DEBOUNCE,
); );
useEffect(() => { useDndMonitor({
const monitor = dragDropManager.getMonitor(); onDragStart() {
const debouncedSetIsDragged = debounce( // set a debounced function to prevent drag source
setIsDragged, // from interfering with the drop zone highlighting
Constants.FAST_DEBOUNCE, debouncedSetIsDragged(true);
); },
const unsub = monitor.subscribeToStateChange(() => { onDragEnd() {
const isDragging = monitor.isDragging(); debouncedSetIsDragged.cancel();
if (isDragging) { setIsDragged(false);
// set a debounced function to prevent HTML5 drag source },
// from interfering with the drop zone highlighting });
debouncedSetIsDragged(true);
} else {
debouncedSetIsDragged.cancel();
setIsDragged(false);
}
});
useEffect(() => {
return () => { return () => {
unsub();
debouncedSetIsDragged.cancel(); debouncedSetIsDragged.cancel();
}; };
}, [dragDropManager]); }, []);
return ( return (
<StyledDiv <StyledDiv

View File

@@ -154,7 +154,7 @@ const PropertiesModal = ({
}; };
const handleDashboardData = useCallback( const handleDashboardData = useCallback(
dashboardData => { (dashboardData: any) => {
const { const {
id, id,
dashboard_title, dashboard_title,

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { DragLayer, XYCoord } from 'react-dnd'; import { useDragLayer } from 'react-dnd';
import { Slice } from 'src/dashboard/types'; import { Slice } from 'src/dashboard/types';
import AddSliceCard from '../AddSliceCard'; import AddSliceCard from '../AddSliceCard';
import { import {
@@ -31,10 +31,7 @@ interface DragItem {
} }
interface AddSliceDragPreviewProps { interface AddSliceDragPreviewProps {
dragItem: DragItem | null;
slices: Slice[] | null; slices: Slice[] | null;
isDragging: boolean;
currentOffset: XYCoord | null;
} }
const staticCardStyles: React.CSSProperties = { const staticCardStyles: React.CSSProperties = {
@@ -47,11 +44,15 @@ const staticCardStyles: React.CSSProperties = {
}; };
const AddSliceDragPreview: React.FC<AddSliceDragPreviewProps> = ({ const AddSliceDragPreview: React.FC<AddSliceDragPreviewProps> = ({
dragItem,
slices, slices,
isDragging,
currentOffset,
}) => { }) => {
const { dragItem, isDragging, currentOffset } = useDragLayer(
(monitor: any) => ({
dragItem: monitor.getItem() as DragItem | null,
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}),
);
if (!isDragging || !currentOffset || !dragItem || !slices) return null; if (!isDragging || !currentOffset || !dragItem || !slices) return null;
const slice = slices[dragItem.index]; const slice = slices[dragItem.index];
@@ -77,9 +78,4 @@ const AddSliceDragPreview: React.FC<AddSliceDragPreviewProps> = ({
); );
}; };
// This injects these props into the component export default AddSliceDragPreview;
export default DragLayer(monitor => ({
dragItem: monitor.getItem() as DragItem | null,
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}))(AddSliceDragPreview);

View File

@@ -1,248 +0,0 @@
/**
* 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 { getEmptyImage } from 'react-dnd-html5-backend';
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
import { DragSource, DropTarget } from 'react-dnd';
import cx from 'classnames';
import { css, styled } from '@apache-superset/core/ui';
import { componentShape } from '../../util/propShapes';
import { dragConfig, dropConfig } from './dragDroppableConfig';
import { DROP_FORBIDDEN } from '../../util/getDropPosition';
const propTypes = {
children: PropTypes.func,
className: PropTypes.string,
component: componentShape,
parentComponent: componentShape,
depth: PropTypes.number.isRequired,
disableDragDrop: PropTypes.bool,
dropToChild: PropTypes.bool,
orientation: PropTypes.oneOf(['row', 'column']),
index: PropTypes.number.isRequired,
style: PropTypes.object,
onDrop: PropTypes.func,
onHover: PropTypes.func,
onDropIndicatorChange: PropTypes.func,
onDragTab: PropTypes.func,
editMode: PropTypes.bool,
useEmptyDragPreview: PropTypes.bool,
// from react-dnd
isDragging: PropTypes.bool,
isDraggingOver: PropTypes.bool,
isDraggingOverShallow: PropTypes.bool,
dragComponentType: PropTypes.string,
dragComponentId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
droppableRef: PropTypes.func,
dragSourceRef: PropTypes.func,
dragPreviewRef: PropTypes.func,
};
const defaultProps = {
className: null,
style: null,
parentComponent: null,
disableDragDrop: false,
dropToChild: false,
children() {},
onDrop() {},
onHover() {},
onDropIndicatorChange() {},
onDragTab() {},
orientation: 'row',
useEmptyDragPreview: false,
isDragging: false,
isDraggingOver: false,
isDraggingOverShallow: false,
droppableRef() {},
dragSourceRef() {},
dragPreviewRef() {},
};
const DragDroppableStyles = styled.div`
${({ theme }) => css`
position: relative;
&.dragdroppable--dragging {
opacity: 0.2;
}
&.dragdroppable-row {
width: 100%;
}
&.dragdroppable-column .resizable-container span div {
z-index: 10;
}
& {
.drop-indicator {
display: block;
background-color: ${theme.colorPrimary};
position: absolute;
z-index: 10;
opacity: 0.3;
width: 100%;
height: 100%;
&.drop-indicator--forbidden {
background-color: ${theme.colorErrorBg};
}
}
}
`};
`;
// export unwrapped component for testing
export class UnwrappedDragDroppable extends PureComponent {
constructor(props) {
super(props);
this.state = {
dropIndicator: null, // this gets set/modified by the react-dnd HOCs
};
this.setRef = this.setRef.bind(this);
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
componentDidUpdate(prevProps, prevState) {
const {
onDropIndicatorChange,
isDraggingOver,
component,
index,
dragComponentId,
onDragTab,
} = this.props;
const { dropIndicator } = this.state;
const isTabsType = component.type === TAB_TYPE;
const validStateChange =
dropIndicator !== prevState.dropIndicator ||
isDraggingOver !== prevProps.isDraggingOver ||
index !== prevProps.index;
if (onDropIndicatorChange && isTabsType && validStateChange) {
onDropIndicatorChange({ dropIndicator, isDraggingOver, index });
}
if (dragComponentId !== prevProps.dragComponentId) {
setTimeout(() => {
/**
* This timeout ensures the dargSourceRef and dragPreviewRef are set
* before the component is removed in Tabs.jsx. Otherwise react-dnd
* will not render the drag preview.
*/
onDragTab(dragComponentId);
});
}
}
setRef(ref) {
this.ref = ref;
// this is needed for a custom drag preview
if (this.props.useEmptyDragPreview) {
this.props.dragPreviewRef(getEmptyImage(), {
// IE fallback: specify that we'd rather screenshot the node
// when it already knows it's being dragged so we can hide it with CSS.
captureDraggingState: true,
});
} else {
this.props.dragPreviewRef(ref);
}
this.props.droppableRef?.(ref);
}
render() {
const {
children,
className,
orientation,
dragSourceRef,
disableDragDrop,
isDragging,
isDraggingOver,
style,
editMode,
component,
dragComponentType,
} = this.props;
const { dropIndicator } = this.state;
const dropIndicatorProps =
isDraggingOver && dropIndicator && !disableDragDrop
? {
className: cx(
'drop-indicator',
dropIndicator === DROP_FORBIDDEN && 'drop-indicator--forbidden',
),
}
: null;
const draggingTabOnTab =
component.type === TAB_TYPE && dragComponentType === TAB_TYPE;
const childProps = editMode
? {
dragSourceRef,
dropIndicatorProps,
draggingTabOnTab,
'data-test': 'dragdroppable-content',
}
: {
'data-test': 'dragdroppable-content',
};
return (
<DragDroppableStyles
style={style}
ref={this.setRef}
data-test="dragdroppable-object"
className={cx(
'dragdroppable',
editMode && 'dragdroppable--edit-mode',
orientation === 'row' && 'dragdroppable-row',
orientation === 'column' && 'dragdroppable-column',
isDragging && 'dragdroppable--dragging',
className,
)}
>
{children(childProps)}
</DragDroppableStyles>
);
}
}
UnwrappedDragDroppable.propTypes = propTypes;
UnwrappedDragDroppable.defaultProps = defaultProps;
export const Draggable = DragSource(...dragConfig)(UnwrappedDragDroppable);
export const Droppable = DropTarget(...dropConfig)(UnwrappedDragDroppable);
// note that the composition order here determines using
// component.method() vs decoratedComponentInstance.method() in the drag/drop config
export const DragDroppable = DragSource(...dragConfig)(
DropTarget(...dropConfig)(UnwrappedDragDroppable),
);

View File

@@ -0,0 +1,295 @@
/**
* 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 {
ReactNode,
useState,
useEffect,
useRef,
CSSProperties,
forwardRef,
useImperativeHandle,
} from 'react';
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
import {
useDraggable,
useDroppable,
DndContext,
DragOverlay,
Active,
Over,
} from '@dnd-kit/core';
import {
CSS,
} from '@dnd-kit/utilities';
import cx from 'classnames';
import { css, styled } from '@apache-superset/core/ui';
import { DROP_FORBIDDEN } from '../../util/getDropPosition';
type Orientation = 'row' | 'column';
interface ComponentType {
id: string;
type: string;
parents?: string[];
children?: string[];
meta?: {
width?: number;
height?: number;
headerSize?: string;
background?: string;
chartId?: number;
};
}
interface ChildProps {
dragSourceRef?: (element: HTMLElement | null) => void;
dropIndicatorProps?: {
className: string;
} | null;
draggingTabOnTab?: boolean;
'data-test': string;
}
interface DragDroppableProps {
children: (props: ChildProps) => ReactNode;
className?: string | null;
component: ComponentType;
parentComponent?: ComponentType | null;
depth: number;
disableDragDrop?: boolean;
dropToChild?: boolean;
orientation?: Orientation;
index: number;
style?: CSSProperties | null;
onDrop?: () => void;
onHover?: () => void;
onDropIndicatorChange?: (params: {
dropIndicator: string | null;
isDraggingOver: boolean;
index: number;
}) => void;
onDragTab?: (dragComponentId: string | number | undefined) => void;
editMode?: boolean;
useEmptyDragPreview?: boolean;
}
const DragDroppableStyles = styled.div`
${({ theme }) => css`
position: relative;
&.dragdroppable--dragging {
opacity: 0.2;
}
&.dragdroppable-row {
width: 100%;
}
&.dragdroppable-column .resizable-container span div {
width: 100%;
}
.drop-indicator {
display: block;
background-color: ${theme.colors.primary.base};
position: absolute;
z-index: 10;
}
&.dragdroppable-row .drop-indicator {
left: 0;
top: -1px;
height: 2px;
width: 100%;
min-width: ${theme.gridUnit * 4}px;
}
&.dragdroppable-column .drop-indicator {
top: 0;
left: -1px;
width: 2px;
height: 100%;
min-height: ${theme.gridUnit * 4}px;
}
.drop-indicator--forbidden {
background-color: ${theme.colors.warning.light1};
}
`}
`;
export const DragDroppable = forwardRef<HTMLDivElement, DragDroppableProps>(
(props, ref) => {
const {
children,
className,
component,
parentComponent,
depth,
disableDragDrop,
dropToChild,
orientation,
index,
style,
onDrop,
onHover,
onDropIndicatorChange,
onDragTab,
editMode,
useEmptyDragPreview,
} = props;
const [dropIndicator, setDropIndicator] = useState<string | null>(null);
const elementRef = useRef<HTMLDivElement>(null);
// Setup draggable functionality
const {
attributes: dragAttributes,
listeners: dragListeners,
setNodeRef: setDragNodeRef,
isDragging,
transform,
} = useDraggable({
id: `drag-${component.id}`,
disabled: disableDragDrop || !editMode,
data: {
type: component.type,
id: component.id,
meta: component.meta,
index,
parentId: parentComponent?.id,
parentType: parentComponent?.type,
},
});
// Setup droppable functionality
const {
setNodeRef: setDropNodeRef,
isOver: isDraggingOver,
active,
over,
} = useDroppable({
id: `drop-${component.id}`,
disabled: disableDragDrop,
data: {
type: component.type,
id: component.id,
index,
},
});
// Combine refs
const setRefs = (element: HTMLDivElement | null) => {
elementRef.current = element;
setDragNodeRef(element);
setDropNodeRef(element);
if (ref) {
if (typeof ref === 'function') {
ref(element);
} else {
ref.current = element;
}
}
};
// Handle drop indicator changes
useEffect(() => {
if (onDropIndicatorChange && component.type === TAB_TYPE) {
onDropIndicatorChange({
dropIndicator,
isDraggingOver,
index,
});
}
}, [dropIndicator, isDraggingOver, index, onDropIndicatorChange, component.type]);
// Handle drag tab
useEffect(() => {
if (onDragTab && active?.data.current?.id) {
onDragTab(active.data.current.id);
}
}, [active?.data.current?.id, onDragTab]);
const dragComponentType = active?.data.current?.type;
const draggingTabOnTab =
component.type === TAB_TYPE && dragComponentType === TAB_TYPE;
const dropIndicatorProps =
isDraggingOver && dropIndicator && !disableDragDrop
? {
className: cx(
'drop-indicator',
dropIndicator === DROP_FORBIDDEN && 'drop-indicator--forbidden',
),
}
: null;
const childProps: ChildProps = editMode
? {
dragSourceRef: (element) => {
if (element && !disableDragDrop) {
// Apply drag attributes and listeners to the drag handle
Object.entries(dragAttributes).forEach(([key, value]) => {
element.setAttribute(key, value as string);
});
Object.entries(dragListeners || {}).forEach(([event, handler]) => {
element.addEventListener(event, handler as EventListener);
});
}
},
dropIndicatorProps,
draggingTabOnTab,
'data-test': 'dragdroppable-content',
}
: {
'data-test': 'dragdroppable-content',
};
const transformStyle = transform
? {
transform: CSS.Transform.toString(transform),
}
: undefined;
return (
<DragDroppableStyles
style={{ ...style, ...transformStyle }}
ref={setRefs}
data-test="dragdroppable-object"
className={cx(
'dragdroppable',
editMode && 'dragdroppable--edit-mode',
orientation === 'row' && 'dragdroppable-row',
orientation === 'column' && 'dragdroppable-column',
isDragging && 'dragdroppable--dragging',
className,
)}
>
{children(childProps)}
</DragDroppableStyles>
);
},
);
DragDroppable.displayName = 'DragDroppable';
// Export compatibility aliases for gradual migration
export const Draggable = DragDroppable;
export const Droppable = DragDroppable;

View File

@@ -16,16 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import {
DragSourceMonitor,
DropTargetMonitor,
ConnectDragSource,
ConnectDragPreview,
ConnectDropTarget,
} from 'react-dnd';
import { LayoutItem, ComponentType } from 'src/dashboard/types'; import { LayoutItem, ComponentType } from 'src/dashboard/types';
import handleHover from './handleHover';
import handleDrop from './handleDrop';
// note: the 'type' hook is not useful for us as dropping is contingent on other properties // note: the 'type' hook is not useful for us as dropping is contingent on other properties
const TYPE = 'DRAG_DROPPABLE'; const TYPE = 'DRAG_DROPPABLE';
@@ -68,121 +60,7 @@ export interface DropResult {
position?: string; position?: string;
} }
export interface DragStateProps { // For @dnd-kit, we'll handle drag/drop logic directly in the component
dragSourceRef: ConnectDragSource; // These exports maintain backward compatibility for components that import from this file
dragPreviewRef: ConnectDragPreview; export { TYPE };
isDragging: boolean; export default { TYPE };
dragComponentType?: ComponentType;
dragComponentId?: string;
}
export interface DropStateProps {
droppableRef: ConnectDropTarget;
isDraggingOver: boolean;
isDraggingOverShallow: boolean;
}
export interface DragDroppableComponent {
mounted: boolean;
props: DragDroppableProps;
setState: (stateUpdate: () => { dropIndicator: string | null }) => void;
}
export const dragConfig: [
string,
{
canDrag: (props: DragDroppableProps) => boolean;
beginDrag: (props: DragDroppableProps) => DragItem;
},
(connect: any, monitor: DragSourceMonitor) => DragStateProps,
] = [
TYPE,
{
canDrag(props: DragDroppableProps): boolean {
return !props.disableDragDrop;
},
// this defines the dragging item object returned by monitor.getItem()
beginDrag(props: DragDroppableProps): DragItem {
const { component, index, parentComponent } = props;
return {
type: component.type,
id: component.id,
meta: component.meta,
index,
parentId: parentComponent?.id,
parentType: parentComponent?.type,
};
},
},
function dragStateToProps(
connect: any,
monitor: DragSourceMonitor,
): DragStateProps {
return {
dragSourceRef: connect.dragSource(),
dragPreviewRef: connect.dragPreview(),
isDragging: monitor.isDragging(),
dragComponentType: monitor.getItem()?.type as ComponentType,
dragComponentId: monitor.getItem()?.id as string,
};
},
];
export const dropConfig: [
string,
{
canDrop: (props: DragDroppableProps) => boolean;
hover: (
props: DragDroppableProps,
monitor: DropTargetMonitor,
component: DragDroppableComponent,
) => void;
drop: (
props: DragDroppableProps,
monitor: DropTargetMonitor,
component: DragDroppableComponent,
) => DropResult | undefined;
},
(connect: any, monitor: DropTargetMonitor) => DropStateProps,
] = [
TYPE,
{
canDrop(props: DragDroppableProps): boolean {
return !props.disableDragDrop;
},
hover(
props: DragDroppableProps,
monitor: DropTargetMonitor,
component: DragDroppableComponent,
): void {
if (component && component.mounted) {
handleHover(props, monitor, component);
}
},
// note:
// the react-dnd api requires that the drop() method return a result or undefined
// monitor.didDrop() cannot be used because it returns true only for the most-nested target
drop(
props: DragDroppableProps,
monitor: DropTargetMonitor,
component: DragDroppableComponent,
): DropResult | undefined {
const dropResult = monitor.getDropResult() as DropResult | null;
if ((!dropResult || !dropResult.destination) && component.mounted) {
return handleDrop(props, monitor, component);
}
return undefined;
},
},
function dropStateToProps(
connect: any,
monitor: DropTargetMonitor,
): DropStateProps {
return {
droppableRef: connect.dropTarget(),
isDraggingOver: monitor.isOver(),
isDraggingOverShallow: monitor.isOver({ shallow: true }),
};
},
];

View File

@@ -16,13 +16,69 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { DropTargetMonitor } from 'react-dnd';
import getDropPosition, { import getDropPosition, {
clearDropCache, clearDropCache,
DROP_FORBIDDEN, DROP_FORBIDDEN,
} from '../../util/getDropPosition'; } from '../../util/getDropPosition';
export default function handleDrop(props, monitor, Component) { interface DragItem {
// this may happen due to throttling id: string;
type: string;
meta?: Record<string, any>;
parentId: string;
parentType: string;
index: number;
}
interface ComponentType {
id: string;
type: string;
children: string[];
}
interface DragDroppableProps {
parentComponent?: ComponentType;
component: ComponentType;
index: number;
onDrop: (dropResult: DropResult) => void;
dropToChild?: boolean | ((item: DragItem) => boolean);
}
interface DragDroppableComponent {
mounted: boolean;
setState: (updater: () => { dropIndicator: string | null }) => void;
props: DragDroppableProps;
}
interface DropDestination {
id: string;
type: string;
index: number;
}
interface DropSource {
id: string;
type: string;
index: number;
}
interface DropResult {
source: DropSource;
dragging: {
id: string;
type: string;
meta?: Record<string, any>;
};
destination: DropDestination;
position: string;
}
export default function handleDrop(
props: DragDroppableProps,
monitor: DropTargetMonitor,
Component: DragDroppableComponent,
): DropResult | undefined {
if (!Component.mounted) return undefined; if (!Component.mounted) return undefined;
Component.setState(() => ({ dropIndicator: null })); Component.setState(() => ({ dropIndicator: null }));
@@ -40,9 +96,9 @@ export default function handleDrop(props, monitor, Component) {
dropToChild, dropToChild,
} = Component.props; } = Component.props;
const draggingItem = monitor.getItem(); const draggingItem = monitor.getItem() as DragItem;
const dropResult = { const dropResult: DropResult = {
source: { source: {
id: draggingItem.parentId, id: draggingItem.parentId,
type: draggingItem.parentType, type: draggingItem.parentType,
@@ -54,12 +110,16 @@ export default function handleDrop(props, monitor, Component) {
meta: draggingItem.meta, meta: draggingItem.meta,
}, },
position: dropPosition, position: dropPosition,
destination: {
id: '',
type: '',
index: 0,
},
}; };
const shouldAppendToChildren = const shouldAppendToChildren =
typeof dropToChild === 'function' ? dropToChild(draggingItem) : dropToChild; typeof dropToChild === 'function' ? dropToChild(draggingItem) : dropToChild;
// simplest case, append as child
if (shouldAppendToChildren) { if (shouldAppendToChildren) {
dropResult.destination = { dropResult.destination = {
id: component.id, id: component.id,
@@ -73,8 +133,6 @@ export default function handleDrop(props, monitor, Component) {
index: componentIndex, index: componentIndex,
}; };
} else { } else {
// if the item is in the same list with a smaller index, you must account for the
// "missing" index upon movement within the list
const sameParent = const sameParent =
parentComponent && draggingItem.parentId === parentComponent.id; parentComponent && draggingItem.parentId === parentComponent.id;
const sameParentLowerIndex = const sameParentLowerIndex =

View File

@@ -17,14 +17,33 @@
* under the License. * under the License.
*/ */
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { DropTargetMonitor } from 'react-dnd';
import { DASHBOARD_ROOT_TYPE } from 'src/dashboard/util/componentTypes'; import { DASHBOARD_ROOT_TYPE } from 'src/dashboard/util/componentTypes';
import getDropPosition from 'src/dashboard/util/getDropPosition'; import getDropPosition from 'src/dashboard/util/getDropPosition';
import handleScroll from './handleScroll'; import handleScroll from './handleScroll';
const HOVER_THROTTLE_MS = 100; const HOVER_THROTTLE_MS = 100;
function handleHover(props, monitor, Component) { interface ComponentType {
// this may happen due to throttling type: string;
}
interface DragDroppableProps {
component: ComponentType;
onHover?: () => void;
}
interface DragDroppableComponent {
mounted: boolean;
setState: (updater: () => { dropIndicator: string | null }) => void;
props: DragDroppableProps;
}
function handleHover(
props: DragDroppableProps,
monitor: DropTargetMonitor,
Component: DragDroppableComponent,
): void {
if (!Component.mounted) return; if (!Component.mounted) return;
const dropPosition = getDropPosition(monitor, Component); const dropPosition = getDropPosition(monitor, Component);
@@ -40,12 +59,11 @@ function handleHover(props, monitor, Component) {
return; return;
} }
Component?.props?.onHover(); Component?.props?.onHover?.();
Component.setState(() => ({ Component.setState(() => ({
dropIndicator: dropPosition, dropIndicator: dropPosition,
})); }));
} }
// this is called very frequently by react-dnd
export default throttle(handleHover, HOVER_THROTTLE_MS); export default throttle(handleHover, HOVER_THROTTLE_MS);

View File

@@ -239,7 +239,7 @@ const ChartHolder = ({
}, []); }, []);
const renderChild = useCallback( const renderChild = useCallback(
({ dragSourceRef }) => ( ({ dragSourceRef }: { dragSourceRef: any }) => (
<ResizableContainer <ResizableContainer
id={component.id} id={component.id}
adjustableWidth={parentComponent.type === ROW_TYPE} adjustableWidth={parentComponent.type === ROW_TYPE}

View File

@@ -89,7 +89,7 @@ const DynamicComponent: FC<DynamicComponentProps> = ({
const updateMeta = (metaKey: string, nextValue: string | number) => { const updateMeta = (metaKey: string, nextValue: string | number) => {
updateComponents({ updateComponents({
[component.id]: { [String(component.id)]: {
...component, ...component,
meta: { meta: {
...component.meta, ...component.meta,
@@ -126,7 +126,9 @@ const DynamicComponent: FC<DynamicComponentProps> = ({
<BackgroundStyleDropdown <BackgroundStyleDropdown
id={`${component.id}-background`} id={`${component.id}-background`}
value={component.meta.background} value={component.meta.background}
onChange={value => updateMeta('background', value)} onChange={value =>
updateMeta('background', value as string | number)
}
/>, />,
]} ]}
editMode={editMode} editMode={editMode}

View File

@@ -18,7 +18,7 @@
*/ */
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react';
import { createWrapper, render } from 'spec/helpers/testing-library'; import { createWrapper, render } from 'spec/helpers/testing-library';
import { useCrossFiltersScopingModal } from './useCrossFiltersScopingModal'; import { useCrossFiltersScopingModal } from './useCrossFiltersScopingModal';

View File

@@ -100,7 +100,7 @@ const FilterBarSettings = () => {
}); });
const updateCrossFiltersSetting = useCallback( const updateCrossFiltersSetting = useCallback(
async isEnabled => { async (isEnabled: any) => {
if (!isEnabled) { if (!isEnabled) {
dispatch(clearDataMaskState()); dispatch(clearDataMaskState());
} }

View File

@@ -341,7 +341,7 @@ const FilterControls: FC<FilterControlsProps> = ({
}, [overflowedCrossFilters, overflowedFiltersInScope]); }, [overflowedCrossFilters, overflowedFiltersInScope]);
const rendererCrossFilter = useCallback( const rendererCrossFilter = useCallback(
(crossFilter, orientation, last) => ( (crossFilter: any, orientation: any, last: any) => (
<CrossFilter <CrossFilter
filter={crossFilter} filter={crossFilter}
orientation={orientation} orientation={orientation}

View File

@@ -16,8 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useRef, FC } from 'react';
import { styled } from '@apache-superset/core/ui'; import { styled } from '@apache-superset/core/ui';
import { useRef, FC } from 'react';
import { import {
DragSourceMonitor, DragSourceMonitor,
DropTargetMonitor, DropTargetMonitor,
@@ -57,6 +57,7 @@ interface FilterTabTitleProps {
index: number; index: number;
filterIds: string[]; filterIds: string[];
onRearrange: (dragItemIndex: number, targetIndex: number) => void; onRearrange: (dragItemIndex: number, targetIndex: number) => void;
children?: React.ReactNode;
} }
interface DragItem { interface DragItem {
@@ -73,6 +74,7 @@ export const DraggableFilter: FC<FilterTabTitleProps> = ({
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({ const [{ isDragging }, drag] = useDrag({
type: FILTER_TYPE,
item: { filterIds, type: FILTER_TYPE, index }, item: { filterIds, type: FILTER_TYPE, index },
collect: (monitor: DragSourceMonitor) => ({ collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),

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