mirror of
https://github.com/apache/superset.git
synced 2026-06-01 13:49:21 +00:00
refactor(explore): improve focus management in adhoc filter editor (#35801)
This commit is contained in:
committed by
GitHub
parent
f6f15f58ee
commit
6e27bee2ca
@@ -38,10 +38,9 @@ import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetr
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { TestDataset } from '@superset-ui/chart-controls';
|
||||
import { TestDataset, Dataset } from '@superset-ui/chart-controls';
|
||||
import AdhocFilterEditPopoverSimpleTabContent, {
|
||||
useSimpleTabFilterProps,
|
||||
Props,
|
||||
} from '.';
|
||||
import { Clauses, ExpressionTypes } from '../types';
|
||||
|
||||
@@ -144,228 +143,11 @@ jest.mock('@superset-ui/core', () => ({
|
||||
|
||||
const mockedIsFeatureEnabled = isFeatureEnabled as jest.Mock;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('AdhocFilterEditPopoverSimpleTabContent', () => {
|
||||
test('can render the simple tab form', () => {
|
||||
expect(() => setup()).not.toThrow();
|
||||
});
|
||||
|
||||
test('shows boolean only operators when subject is boolean', () => {
|
||||
const props = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'BOOL',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IsTrue,
|
||||
Operators.IsFalse,
|
||||
Operators.IsNull,
|
||||
Operators.IsFalse,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
test('shows boolean only operators when subject is number', () => {
|
||||
const props = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'INT',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IsTrue,
|
||||
Operators.IsFalse,
|
||||
Operators.IsNull,
|
||||
Operators.IsNotNull,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
|
||||
test('will convert from individual comparator to array if the operator changes to multi', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.In);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toEqual(['10']);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toEqual(Operators.In);
|
||||
});
|
||||
|
||||
test('will convert from array to individual comparators if the operator changes from multi', () => {
|
||||
const props = setup({
|
||||
adhocFilter: simpleMultiAdhocFilter,
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LessThan);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleMultiAdhocFilter.duplicateWith({
|
||||
operatorId: Operators.LessThan,
|
||||
operator: '<',
|
||||
comparator: '10',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('passes the new adhocFilter to onChange after onComparatorChange', () => {
|
||||
const props = setup();
|
||||
const { onComparatorChange } = useSimpleTabFilterProps(props);
|
||||
onComparatorChange('20');
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('will filter operators for table datasources', () => {
|
||||
const props = setup({ datasource: { type: 'table' } });
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.Like, 'value')).toBe(true);
|
||||
});
|
||||
|
||||
test('will show LATEST PARTITION operator', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: simpleCustomFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.LatestPartition, 'ds')).toBe(true);
|
||||
expect(isOperatorRelevant(Operators.LatestPartition, 'value')).toBe(false);
|
||||
});
|
||||
|
||||
test('will generate custom sqlExpression for LATEST PARTITION operator', () => {
|
||||
const testAdhocFilter = new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'ds',
|
||||
});
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: testAdhocFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LatestPartition);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
operatorId: Operators.LatestPartition,
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: "ds = '{{ presto.latest_partition('schema.table1') }}'",
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('will not display boolean operators when column type is string', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'STRING' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(false);
|
||||
});
|
||||
});
|
||||
test('will display boolean operators when column is an expression', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table',
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [
|
||||
{
|
||||
column_name: 'value',
|
||||
expression: 'case when value is 0 then "NO"',
|
||||
},
|
||||
],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(true);
|
||||
});
|
||||
});
|
||||
test('sets comparator to undefined when operator is IS_TRUE', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IsTrue);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsTrue);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('IS TRUE');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
test('sets comparator to undefined when operator is IS_FALSE', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IsFalse);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsFalse);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('IS FALSE');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
test('sets comparator to undefined when operator is IS_NULL or IS_NOT_NULL', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
[Operators.IsNull, Operators.IsNotNull].forEach(op => {
|
||||
onOperatorChange(op);
|
||||
expect(props.onChange.called).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(op);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe(
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const ADVANCED_DATA_TYPE_ENDPOINT_VALID =
|
||||
'glob:*/api/v1/advanced_data_type/convert?q=(type:type,values:!(v))';
|
||||
const ADVANCED_DATA_TYPE_ENDPOINT_INVALID =
|
||||
'glob:*/api/v1/advanced_data_type/convert?q=(type:type,values:!(e))';
|
||||
|
||||
fetchMock.get(ADVANCED_DATA_TYPE_ENDPOINT_VALID, {
|
||||
result: {
|
||||
display_value: 'VALID',
|
||||
@@ -382,182 +164,436 @@ fetchMock.get(ADVANCED_DATA_TYPE_ENDPOINT_INVALID, {
|
||||
values: [],
|
||||
},
|
||||
});
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('AdhocFilterEditPopoverSimpleTabContent Advanced data Type Test', () => {
|
||||
const setupFilter = async (props: Props) => {
|
||||
await act(async () => {
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
|
||||
store,
|
||||
});
|
||||
});
|
||||
};
|
||||
let isFeatureEnabledMock: jest.SpyInstance;
|
||||
|
||||
let isFeatureEnabledMock: any;
|
||||
beforeEach(async () => {
|
||||
isFeatureEnabledMock = mockedIsFeatureEnabled.mockImplementation(
|
||||
(featureFlag: FeatureFlag) =>
|
||||
featureFlag === FeatureFlag.EnableAdvancedDataTypes,
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchMock.resetHistory();
|
||||
isFeatureEnabledMock = mockedIsFeatureEnabled.mockImplementation(
|
||||
(featureFlag: FeatureFlag) =>
|
||||
featureFlag === FeatureFlag.EnableAdvancedDataTypes,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(() => {
|
||||
if (isFeatureEnabledMock) {
|
||||
isFeatureEnabledMock.mockRestore();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should not call API when column has no advanced data type', async () => {
|
||||
fetchMock.resetHistory();
|
||||
test('can render the simple tab form', () => {
|
||||
expect(() => setup()).not.toThrow();
|
||||
});
|
||||
|
||||
const props = getAdvancedDataTypeTestProps();
|
||||
|
||||
await setupFilter(props);
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
// When the column is not a advanced data type,
|
||||
// the advanced data type endpoint should not be called
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(
|
||||
0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should call API when column has advanced data type', async () => {
|
||||
fetchMock.resetHistory();
|
||||
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
test('shows boolean only operators when subject is boolean', () => {
|
||||
const props = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'BOOL',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await setupFilter(props);
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
// When the column is a advanced data type,
|
||||
// the advanced data type endpoint should be called
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(
|
||||
1,
|
||||
),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(true);
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IsTrue,
|
||||
Operators.IsFalse,
|
||||
Operators.IsNull,
|
||||
Operators.IsFalse,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
|
||||
test('save button should be disabled if error message from API is returned', async () => {
|
||||
fetchMock.resetHistory();
|
||||
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
test('shows boolean only operators when subject is number', () => {
|
||||
const props = setup({
|
||||
adhocFilter: new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'value',
|
||||
operatorId: null,
|
||||
operator: null,
|
||||
comparator: null,
|
||||
clause: null,
|
||||
}),
|
||||
datasource: {
|
||||
columns: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
id: 3,
|
||||
column_name: 'value',
|
||||
type: 'INT',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await setupFilter(props);
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'e');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
// When the column is a advanced data type but an error response is given by the endpoint,
|
||||
// the save button should be disabled
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_INVALID)).toHaveLength(
|
||||
1,
|
||||
),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(false);
|
||||
},
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
[
|
||||
Operators.IsTrue,
|
||||
Operators.IsFalse,
|
||||
Operators.IsNull,
|
||||
Operators.IsNotNull,
|
||||
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
|
||||
});
|
||||
|
||||
test('advanced data type operator list should update after API response', async () => {
|
||||
fetchMock.resetHistory();
|
||||
test('will convert from individual comparator to array if the operator changes to multi', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.In);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toEqual(['10']);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toEqual(Operators.In);
|
||||
});
|
||||
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
},
|
||||
],
|
||||
});
|
||||
test('will convert from array to individual comparators if the operator changes from multi', () => {
|
||||
const props = setup({
|
||||
adhocFilter: simpleMultiAdhocFilter,
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LessThan);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleMultiAdhocFilter.duplicateWith({
|
||||
operatorId: Operators.LessThan,
|
||||
operator: '<',
|
||||
comparator: '10',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await setupFilter(props);
|
||||
test('passes the new adhocFilter to onChange after onComparatorChange', () => {
|
||||
const props = setup();
|
||||
const { onComparatorChange } = useSimpleTabFilterProps(props);
|
||||
onComparatorChange('20');
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
simpleAdhocFilter.duplicateWith({ comparator: '20' }),
|
||||
);
|
||||
});
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
test('will filter operators for table datasources', () => {
|
||||
const props = setup({ datasource: { type: 'table' as const } });
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.Like, 'value')).toBe(true);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
test('will show LATEST PARTITION operator', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table' as const,
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: simpleCustomFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
expect(isOperatorRelevant(Operators.LatestPartition, 'ds')).toBe(true);
|
||||
expect(isOperatorRelevant(Operators.LatestPartition, 'value')).toBe(false);
|
||||
});
|
||||
|
||||
// When the column is a advanced data type,
|
||||
// the advanced data type endpoint should be called
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(
|
||||
1,
|
||||
),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(true);
|
||||
test('will generate custom sqlExpression for LATEST PARTITION operator', () => {
|
||||
const testAdhocFilter = new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'ds',
|
||||
});
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table' as const,
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
},
|
||||
adhocFilter: testAdhocFilter,
|
||||
partitionColumn: 'ds',
|
||||
});
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.LatestPartition);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0]).toEqual(
|
||||
testAdhocFilter.duplicateWith({
|
||||
subject: 'ds',
|
||||
operator: 'LATEST PARTITION',
|
||||
operatorId: Operators.LatestPartition,
|
||||
comparator: null,
|
||||
clause: 'WHERE',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: "ds = '{{ presto.latest_partition('schema.table1') }}'",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const operatorValueField = screen.getByRole('combobox', {
|
||||
name: 'Select operator',
|
||||
});
|
||||
|
||||
userEvent.click(operatorValueField);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(operatorValueField, '{enter}');
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('Equal to (=)', {
|
||||
selector: '.ant-select-selection-item',
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
test('will not display boolean operators when column type is string', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table' as const,
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [{ column_name: 'value', type: 'STRING' }],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('will display boolean operators when column is an expression', () => {
|
||||
const props = setup({
|
||||
datasource: {
|
||||
type: 'table' as const,
|
||||
datasource_name: 'table1',
|
||||
schema: 'schema',
|
||||
columns: [
|
||||
{
|
||||
column_name: 'value',
|
||||
expression: 'case when value is 0 then "NO"',
|
||||
},
|
||||
],
|
||||
},
|
||||
adhocFilter: simpleAdhocFilter,
|
||||
});
|
||||
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
|
||||
const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
|
||||
booleanOnlyOperators.forEach(operator => {
|
||||
expect(isOperatorRelevant(operator, 'value')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('sets comparator to undefined when operator is IS_TRUE', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IsTrue);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsTrue);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('IS TRUE');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
|
||||
test('sets comparator to undefined when operator is IS_FALSE', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
onOperatorChange(Operators.IsFalse);
|
||||
expect(props.onChange.calledOnce).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsFalse);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe('IS FALSE');
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
|
||||
test('sets comparator to undefined when operator is IS_NULL or IS_NOT_NULL', () => {
|
||||
const props = setup();
|
||||
const { onOperatorChange } = useSimpleTabFilterProps(props);
|
||||
[Operators.IsNull, Operators.IsNotNull].forEach(op => {
|
||||
onOperatorChange(op);
|
||||
expect(props.onChange.called).toBe(true);
|
||||
expect(props.onChange.lastCall.args[0].operatorId).toBe(op);
|
||||
expect(props.onChange.lastCall.args[0].operator).toBe(
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
|
||||
);
|
||||
expect(props.onChange.lastCall.args[0].comparator).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call API when column has no advanced data type', async () => {
|
||||
const props = getAdvancedDataTypeTestProps();
|
||||
|
||||
await act(async () => {
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(0),
|
||||
);
|
||||
});
|
||||
|
||||
test('should call API when column has advanced data type', async () => {
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(1),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(true);
|
||||
});
|
||||
|
||||
test('save button should be disabled if error message from API is returned', async () => {
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'e');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_INVALID)).toHaveLength(
|
||||
1,
|
||||
),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(false);
|
||||
});
|
||||
|
||||
test('advanced data type operator list should update after API response', async () => {
|
||||
const props = getAdvancedDataTypeTestProps({
|
||||
options: [
|
||||
{
|
||||
type: 'DOUBLE',
|
||||
column_name: 'advancedDataType',
|
||||
id: 5,
|
||||
advanced_data_type: 'type',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
const filterValueField = screen.getByPlaceholderText(
|
||||
'Filter value (case sensitive)',
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, 'v');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(filterValueField, '{enter}');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(ADVANCED_DATA_TYPE_ENDPOINT_VALID)).toHaveLength(1),
|
||||
);
|
||||
expect(props.validHandler.lastCall.args[0]).toBe(true);
|
||||
|
||||
const operatorValueField = screen.getByRole('combobox', {
|
||||
name: 'Select operator',
|
||||
});
|
||||
|
||||
userEvent.click(operatorValueField);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.type(operatorValueField, '{enter}');
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('Equal to (=)', {
|
||||
selector: '.ant-select-selection-item',
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('dropdown should remain open when clicked after filter is configured', async () => {
|
||||
const onChange = sinon.spy();
|
||||
const validHandler = sinon.spy();
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue({});
|
||||
|
||||
const filterWithSubjectAndOperator = new AdhocFilter({
|
||||
expressionType: ExpressionTypes.Simple,
|
||||
subject: 'value',
|
||||
operatorId: Operators.Equals,
|
||||
operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.Equals].operation,
|
||||
comparator: '10',
|
||||
clause: Clauses.Where,
|
||||
});
|
||||
|
||||
const props = {
|
||||
adhocFilter: filterWithSubjectAndOperator,
|
||||
onChange,
|
||||
options,
|
||||
datasource: {
|
||||
...TestDataset,
|
||||
columns: [{ column_name: 'value', type: 'DOUBLE', id: 3 }],
|
||||
filter_select: false,
|
||||
} as Dataset,
|
||||
partitionColumn: 'test',
|
||||
validHandler,
|
||||
};
|
||||
|
||||
render(<AdhocFilterEditPopoverSimpleTabContent {...props} />);
|
||||
|
||||
const operatorDropdown = screen.getByRole('combobox', {
|
||||
name: 'Select operator',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(operatorDropdown);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(operatorDropdown).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
expect(operatorDropdown).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, ChangeEvent, useEffect, useState } from 'react';
|
||||
import { FC, ChangeEvent, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { Input, Select, Tooltip } from '@superset-ui/core/components';
|
||||
import { Input, InputRef, Select, Tooltip } from '@superset-ui/core/components';
|
||||
import {
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
@@ -263,12 +263,15 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
onComparatorChange,
|
||||
onDatePickerChange,
|
||||
} = useSimpleTabFilterProps(props);
|
||||
const [comparator, setComparator] = useState(props.adhocFilter.comparator);
|
||||
const comparatorInputRef = useRef<InputRef | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<
|
||||
Record<'label' | 'value', any>[]
|
||||
>([]);
|
||||
const [comparator, setComparator] = useState(props.adhocFilter.comparator);
|
||||
const [loadingComparatorSuggestions, setLoadingComparatorSuggestions] =
|
||||
useState(false);
|
||||
useState<boolean>(false);
|
||||
const [hasFocusedComparator, setHasFocusedComparator] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const {
|
||||
advancedDataTypesState,
|
||||
@@ -361,7 +364,6 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
notFoundContent: t('Type a value here'),
|
||||
disabled: DISABLE_INPUT_OPERATORS.includes(operatorId),
|
||||
placeholder: createSuggestionsPlaceholder(),
|
||||
autoFocus: shouldFocusComparator,
|
||||
};
|
||||
|
||||
const labelText =
|
||||
@@ -411,16 +413,33 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!datePicker) {
|
||||
refreshComparatorSuggestions();
|
||||
}
|
||||
}, [props.adhocFilter.subject]);
|
||||
// loadingComparatorSuggestions intentionally omitted - set inside effect, would cause infinite loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
props.adhocFilter.subject,
|
||||
props.adhocFilter.clause,
|
||||
props.datasource,
|
||||
datePicker,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)) {
|
||||
fetchSubjectAdvancedDataType(props);
|
||||
fetchSubjectAdvancedDataType(
|
||||
props.options,
|
||||
props.adhocFilter.subject,
|
||||
props.validHandler,
|
||||
);
|
||||
}
|
||||
}, [props.adhocFilter.subject]);
|
||||
}, [
|
||||
props.adhocFilter.subject,
|
||||
props.options,
|
||||
props.validHandler,
|
||||
fetchSubjectAdvancedDataType,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)) {
|
||||
@@ -430,6 +449,8 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
subjectAdvancedDataType,
|
||||
);
|
||||
}
|
||||
// advancedDataTypesState intentionally omitted - set by the callback, would cause infinite API calls
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [comparator, subjectAdvancedDataType, fetchAdvancedDataTypeValueCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -437,6 +458,22 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
setComparator(props.adhocFilter.comparator);
|
||||
}
|
||||
}, [props.adhocFilter.comparator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
shouldFocusComparator &&
|
||||
!hasFocusedComparator &&
|
||||
comparatorInputRef.current
|
||||
) {
|
||||
comparatorInputRef.current.focus();
|
||||
setHasFocusedComparator(true);
|
||||
}
|
||||
|
||||
if (!shouldFocusComparator) {
|
||||
setHasFocusedComparator(false);
|
||||
}
|
||||
}, [shouldFocusComparator, hasFocusedComparator]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
// another name for columns, just for following previous naming.
|
||||
@@ -506,11 +543,7 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
<Input
|
||||
data-test="adhoc-filter-simple-value"
|
||||
name="filter-value"
|
||||
ref={ref => {
|
||||
if (ref && shouldFocusComparator) {
|
||||
ref.focus();
|
||||
}
|
||||
}}
|
||||
ref={comparatorInputRef}
|
||||
onChange={onInputComparatorChange}
|
||||
value={comparator}
|
||||
placeholder={t('Filter value (case sensitive)')}
|
||||
|
||||
@@ -76,20 +76,25 @@ const useAdvancedDataTypes = (validHandler: (isValid: boolean) => void) => {
|
||||
[validHandler],
|
||||
);
|
||||
|
||||
const fetchSubjectAdvancedDataType = (props: Props) => {
|
||||
const option = props.options.find(
|
||||
option =>
|
||||
('column_name' in option &&
|
||||
option.column_name === props.adhocFilter.subject) ||
|
||||
('optionName' in option &&
|
||||
option.optionName === props.adhocFilter.subject),
|
||||
);
|
||||
if (option && 'advanced_data_type' in option) {
|
||||
setSubjectAdvancedDataType(option.advanced_data_type);
|
||||
} else {
|
||||
props.validHandler(true);
|
||||
}
|
||||
};
|
||||
const fetchSubjectAdvancedDataType = useCallback(
|
||||
(
|
||||
options: Props['options'],
|
||||
subject: Props['adhocFilter']['subject'],
|
||||
validHandler: Props['validHandler'],
|
||||
) => {
|
||||
const option = options.find(
|
||||
opt =>
|
||||
('column_name' in opt && opt.column_name === subject) ||
|
||||
('optionName' in opt && opt.optionName === subject),
|
||||
);
|
||||
if (option && 'advanced_data_type' in option) {
|
||||
setSubjectAdvancedDataType(option.advanced_data_type);
|
||||
} else {
|
||||
validHandler(true);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
advancedDataTypesState,
|
||||
|
||||
Reference in New Issue
Block a user