mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
chore: Select component refactoring - FilterControl - Iteration 5 (#15777)
* Refactor Select for AdhocFilterEditPopoverSqlTabContent * Refactor Selects * Fix numeric options * Clean up * Fix Select value * Add showSearch * Display null label * Implement StyledSelect * Update superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/index.jsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Fix aria-label * Revert MetricControls changes * Update ariaLabel * Reconcile with latest Select changes Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
0f16177bde
commit
8ad03c4f25
@ -52,7 +52,7 @@ describe('AdhocFilters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
xit('Set simple adhoc filter', () => {
|
xit('Set simple adhoc filter', () => {
|
||||||
cy.get('[data-test=adhoc-filter-simple-value] .Select__control').click();
|
cy.get('[aria-label="Comparator option"] .Select__control').click();
|
||||||
cy.get('[data-test=adhoc-filter-simple-value] input[type=text]')
|
cy.get('[data-test=adhoc-filter-simple-value] input[type=text]')
|
||||||
.focus()
|
.focus()
|
||||||
.type('Jack{enter}', { delay: 20 });
|
.type('Jack{enter}', { delay: 20 });
|
||||||
|
@ -68,11 +68,6 @@ const FilterPopoverContentContainer = styled.div`
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-edit-clause-dropdown {
|
|
||||||
width: ${({ theme }) => theme.gridUnit * 30}px;
|
|
||||||
margin-right: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-edit-clause-info {
|
.filter-edit-clause-info {
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.xs}px;
|
font-size: ${({ theme }) => theme.typography.sizes.xs}px;
|
||||||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
padding-left: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { NativeSelect as Select } from 'src/components/Select';
|
import { Select } from 'src/components';
|
||||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
Operators,
|
Operators,
|
||||||
@ -36,25 +36,7 @@ import AdhocFilter, {
|
|||||||
EXPRESSION_TYPES,
|
EXPRESSION_TYPES,
|
||||||
CLAUSES,
|
CLAUSES,
|
||||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||||
import { Input, SelectProps } from 'src/common/components';
|
import { Input } from 'src/common/components';
|
||||||
|
|
||||||
const SelectWithLabel = styled(Select)`
|
|
||||||
.ant-select-selector {
|
|
||||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selector::after {
|
|
||||||
content: '${(
|
|
||||||
pr: SelectProps<any> & {
|
|
||||||
labelText: string | boolean;
|
|
||||||
},
|
|
||||||
) => pr.labelText || '\\A0'}';
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export interface SimpleColumnType {
|
export interface SimpleColumnType {
|
||||||
id: number;
|
id: number;
|
||||||
@ -132,10 +114,10 @@ export const useSimpleTabFilterProps = (props: Props) => {
|
|||||||
HAVING_OPERATORS.indexOf(operator) === -1)
|
HAVING_OPERATORS.indexOf(operator) === -1)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const onSubjectChange = (id: string | number) => {
|
const onSubjectChange = (id: string) => {
|
||||||
const option = props.options.find(
|
const option = props.options.find(
|
||||||
option =>
|
option =>
|
||||||
('id' in option && option.id === id) ||
|
('column_name' in option && option.column_name === id) ||
|
||||||
('optionName' in option && option.optionName === id),
|
('optionName' in option && option.optionName === id),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -222,10 +204,6 @@ export const useSimpleTabFilterProps = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
||||||
const selectProps = {
|
|
||||||
name: 'select-column',
|
|
||||||
showSearch: true,
|
|
||||||
};
|
|
||||||
const {
|
const {
|
||||||
onSubjectChange,
|
onSubjectChange,
|
||||||
onOperatorChange,
|
onOperatorChange,
|
||||||
@ -233,7 +211,6 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||||||
onComparatorChange,
|
onComparatorChange,
|
||||||
} = useSimpleTabFilterProps(props);
|
} = useSimpleTabFilterProps(props);
|
||||||
const [suggestions, setSuggestions] = useState<Record<string, any>>([]);
|
const [suggestions, setSuggestions] = useState<Record<string, any>>([]);
|
||||||
const [currentSuggestionSearch, setCurrentSuggestionSearch] = useState('');
|
|
||||||
const [
|
const [
|
||||||
loadingComparatorSuggestions,
|
loadingComparatorSuggestions,
|
||||||
setLoadingComparatorSuggestions,
|
setLoadingComparatorSuggestions,
|
||||||
@ -279,10 +256,6 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||||||
<FilterDefinitionOption option={option} />
|
<FilterDefinitionOption option={option} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearSuggestionSearch = () => {
|
|
||||||
setCurrentSuggestionSearch('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOptionsRemaining = () => {
|
const getOptionsRemaining = () => {
|
||||||
const { comparator } = props.adhocFilter;
|
const { comparator } = props.adhocFilter;
|
||||||
// if select is multi/value is array, we show the options not selected
|
// if select is multi/value is array, we show the options not selected
|
||||||
@ -299,7 +272,9 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||||||
|
|
||||||
let columns = props.options;
|
let columns = props.options;
|
||||||
const { subject, operator, comparator, operatorId } = props.adhocFilter;
|
const { subject, operator, comparator, operatorId } = props.adhocFilter;
|
||||||
|
|
||||||
const subjectSelectProps = {
|
const subjectSelectProps = {
|
||||||
|
ariaLabel: t('Select subject'),
|
||||||
value: subject ?? undefined,
|
value: subject ?? undefined,
|
||||||
onChange: onSubjectChange,
|
onChange: onSubjectChange,
|
||||||
notFoundContent: t(
|
notFoundContent: t(
|
||||||
@ -332,33 +307,44 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||||||
'%s operator(s)',
|
'%s operator(s)',
|
||||||
OPERATORS_OPTIONS.filter(op => isOperatorRelevant(op, subject)).length,
|
OPERATORS_OPTIONS.filter(op => isOperatorRelevant(op, subject)).length,
|
||||||
),
|
),
|
||||||
value: OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId]?.display,
|
value: operatorId,
|
||||||
onChange: onOperatorChange,
|
onChange: onOperatorChange,
|
||||||
autoFocus: !!subjectSelectProps.value && !operator,
|
autoFocus: !!subjectSelectProps.value && !operator,
|
||||||
name: 'select-operator',
|
ariaLabel: t('Select operator'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldFocusComparator =
|
const shouldFocusComparator =
|
||||||
!!subjectSelectProps.value && !!operatorSelectProps.value;
|
!!subjectSelectProps.value && !!operatorSelectProps.value;
|
||||||
|
|
||||||
const comparatorSelectProps: SelectProps<any> & {
|
const comparatorSelectProps = {
|
||||||
labelText: string | boolean;
|
|
||||||
} = {
|
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
showSearch: true,
|
allowNewOptions: true,
|
||||||
mode: MULTI_OPERATORS.has(operatorId) ? 'tags' : undefined,
|
ariaLabel: t('Comparator option'),
|
||||||
tokenSeparators: [',', '\n', '\t', ';'],
|
mode: MULTI_OPERATORS.has(operatorId)
|
||||||
|
? ('multiple' as const)
|
||||||
|
: ('single' as const),
|
||||||
loading: loadingComparatorSuggestions,
|
loading: loadingComparatorSuggestions,
|
||||||
value: comparator,
|
value: comparator,
|
||||||
onChange: onComparatorChange,
|
onChange: onComparatorChange,
|
||||||
notFoundContent: t('Type a value here'),
|
notFoundContent: t('Type a value here'),
|
||||||
disabled: DISABLE_INPUT_OPERATORS.includes(operatorId),
|
disabled: DISABLE_INPUT_OPERATORS.includes(operatorId),
|
||||||
placeholder: createSuggestionsPlaceholder(),
|
placeholder: createSuggestionsPlaceholder(),
|
||||||
labelText:
|
|
||||||
comparator && comparator.length > 0 && createSuggestionsPlaceholder(),
|
|
||||||
autoFocus: shouldFocusComparator,
|
autoFocus: shouldFocusComparator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelText =
|
||||||
|
comparator && comparator.length > 0 && createSuggestionsPlaceholder();
|
||||||
|
|
||||||
|
const SelectWithLabel = styled(Select)`
|
||||||
|
.ant-select-selector::after {
|
||||||
|
content: ${() => labelText || '\\A0'};
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
@ -366,81 +352,42 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||||||
marginTop: theme.gridUnit * 4,
|
marginTop: theme.gridUnit * 4,
|
||||||
marginBottom: theme.gridUnit * 4,
|
marginBottom: theme.gridUnit * 4,
|
||||||
})}
|
})}
|
||||||
{...selectProps}
|
options={columns.map(column => ({
|
||||||
|
value:
|
||||||
|
('column_name' in column && column.column_name) ||
|
||||||
|
('optionName' in column && column.optionName) ||
|
||||||
|
'',
|
||||||
|
label:
|
||||||
|
('saved_metric_name' in column && column.saved_metric_name) ||
|
||||||
|
('column_name' in column && column.column_name) ||
|
||||||
|
('label' in column && column.label),
|
||||||
|
key:
|
||||||
|
('id' in column && column.id) ||
|
||||||
|
('optionName' in column && column.optionName) ||
|
||||||
|
undefined,
|
||||||
|
customLabel: renderSubjectOptionLabel(column),
|
||||||
|
}))}
|
||||||
{...subjectSelectProps}
|
{...subjectSelectProps}
|
||||||
filterOption={(input, option) =>
|
/>
|
||||||
option && option.filterBy
|
|
||||||
? option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
|
||||||
>
|
|
||||||
{columns.map(column => (
|
|
||||||
<Select.Option
|
|
||||||
value={
|
|
||||||
('id' in column && column.id) ||
|
|
||||||
('optionName' in column && column.optionName) ||
|
|
||||||
''
|
|
||||||
}
|
|
||||||
filterBy={
|
|
||||||
('saved_metric_name' in column && column.saved_metric_name) ||
|
|
||||||
('column_name' in column && column.column_name) ||
|
|
||||||
('label' in column && column.label)
|
|
||||||
}
|
|
||||||
key={
|
|
||||||
('id' in column && column.id) ||
|
|
||||||
('optionName' in column && column.optionName) ||
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{renderSubjectOptionLabel(column)}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select
|
<Select
|
||||||
css={theme => ({ marginBottom: theme.gridUnit * 4 })}
|
css={theme => ({ marginBottom: theme.gridUnit * 4 })}
|
||||||
{...selectProps}
|
options={OPERATORS_OPTIONS.filter(op =>
|
||||||
|
isOperatorRelevant(op, subject),
|
||||||
|
).map(option => ({
|
||||||
|
value: option,
|
||||||
|
label: OPERATOR_ENUM_TO_OPERATOR_TYPE[option].display,
|
||||||
|
key: option,
|
||||||
|
}))}
|
||||||
{...operatorSelectProps}
|
{...operatorSelectProps}
|
||||||
filterOption={(input, option) =>
|
/>
|
||||||
option && option.children
|
|
||||||
? option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
|
||||||
>
|
|
||||||
{OPERATORS_OPTIONS.filter(op => isOperatorRelevant(op, subject)).map(
|
|
||||||
option => (
|
|
||||||
<Select.Option value={option} key={option}>
|
|
||||||
{OPERATOR_ENUM_TO_OPERATOR_TYPE[option].display}
|
|
||||||
</Select.Option>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
{MULTI_OPERATORS.has(operatorId) || suggestions.length > 0 ? (
|
{MULTI_OPERATORS.has(operatorId) || suggestions.length > 0 ? (
|
||||||
<SelectWithLabel
|
<SelectWithLabel
|
||||||
data-test="adhoc-filter-simple-value"
|
options={suggestions.map((suggestion: string) => ({
|
||||||
|
value: suggestion,
|
||||||
|
label: String(suggestion),
|
||||||
|
}))}
|
||||||
{...comparatorSelectProps}
|
{...comparatorSelectProps}
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
/>
|
||||||
onSearch={val => setCurrentSuggestionSearch(val)}
|
|
||||||
onSelect={clearSuggestionSearch}
|
|
||||||
onBlur={clearSuggestionSearch}
|
|
||||||
>
|
|
||||||
{suggestions.map((suggestion: string) => (
|
|
||||||
<Select.Option value={suggestion} key={suggestion}>
|
|
||||||
{String(suggestion)}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* enable selecting an option not included in suggestions */}
|
|
||||||
{currentSuggestionSearch &&
|
|
||||||
!suggestions.some(
|
|
||||||
(suggestion: string) => suggestion === currentSuggestionSearch,
|
|
||||||
) && (
|
|
||||||
<Select.Option value={currentSuggestionSearch}>
|
|
||||||
{currentSuggestionSearch}
|
|
||||||
</Select.Option>
|
|
||||||
)}
|
|
||||||
</SelectWithLabel>
|
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
data-test="adhoc-filter-simple-value"
|
data-test="adhoc-filter-simple-value"
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NativeSelect as Select } from 'src/components/Select';
|
import { Select } from 'src/components';
|
||||||
import { t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||||
|
|
||||||
@ -44,6 +44,13 @@ const propTypes = {
|
|||||||
activeKey: PropTypes.string.isRequired,
|
activeKey: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledSelect = styled(Select)`
|
||||||
|
${({ theme }) => `
|
||||||
|
width: ${theme.gridUnit * 30}px;
|
||||||
|
marginRight: ${theme.gridUnit}px;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
export default class AdhocFilterEditPopoverSqlTabContent extends React.Component {
|
export default class AdhocFilterEditPopoverSqlTabContent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -54,7 +61,7 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||||||
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
|
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
|
||||||
|
|
||||||
this.selectProps = {
|
this.selectProps = {
|
||||||
name: 'select-column',
|
ariaLabel: t('Select column'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,22 +118,19 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||||||
})
|
})
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
|
const selectOptions = Object.keys(CLAUSES).map(clause => ({
|
||||||
|
label: clause,
|
||||||
|
value: clause,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<div className="filter-edit-clause-section">
|
<div className="filter-edit-clause-section">
|
||||||
<Select
|
<StyledSelect
|
||||||
|
options={selectOptions}
|
||||||
{...this.selectProps}
|
{...this.selectProps}
|
||||||
{...clauseSelectProps}
|
{...clauseSelectProps}
|
||||||
className="filter-edit-clause-dropdown"
|
/>
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
|
||||||
>
|
|
||||||
{Object.keys(CLAUSES).map(clause => (
|
|
||||||
<Select.Option value={clause} key={clause}>
|
|
||||||
{clause}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<span className="filter-edit-clause-info">
|
<span className="filter-edit-clause-info">
|
||||||
<strong>WHERE</strong> {t('Filters by columns')}
|
<strong>WHERE</strong> {t('Filters by columns')}
|
||||||
<br />
|
<br />
|
||||||
|
Loading…
Reference in New Issue
Block a user