feat: Implement drag and drop columns for filters (#13340)

* Implement DnD feature for filters

* minor refactor

* Fix types

* Fix undefined error

* Refactor

* Fix ts errors

* Fix conflicting dnd types

* Bump superset-ui packages

* Change DndItemType case to PascalCase

* Remove redundant null check

* Fix

* Fix csrf mock api call
This commit is contained in:
Kamil Gabryjelski
2021-03-07 10:54:08 +01:00
committed by GitHub
parent 3970d7316b
commit 7b370e6f17
23 changed files with 1069 additions and 540 deletions

View File

@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { ColumnMeta, ColumnOption } from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash';
import { LabelProps } from './types';
import DndSelectLabel from './DndSelectLabel';
import OptionWrapper from './components/OptionWrapper';
import { OptionSelector } from './utils';
import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
import { DndItemType } from '../../DndItemType';
export const DndColumnSelect = (props: LabelProps) => {
const { value, options } = props;
const optionSelector = new OptionSelector(options, value);
const [values, setValues] = useState<ColumnMeta[]>(optionSelector.values);
const onDrop = (item: DatasourcePanelDndItem) => {
const column = item.value as ColumnMeta;
if (!optionSelector.isArray && !isEmpty(optionSelector.values)) {
optionSelector.replace(0, column.column_name);
} else {
optionSelector.add(column.column_name);
}
setValues(optionSelector.values);
props.onChange(optionSelector.getValues());
};
const canDrop = (item: DatasourcePanelDndItem) =>
!optionSelector.has((item.value as ColumnMeta).column_name);
const onClickClose = (index: number) => {
optionSelector.del(index);
setValues(optionSelector.values);
props.onChange(optionSelector.getValues());
};
const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
optionSelector.swap(dragIndex, hoverIndex);
setValues(optionSelector.values);
props.onChange(optionSelector.getValues());
};
const valuesRenderer = () =>
values.map((column, idx) => (
<OptionWrapper
key={idx}
index={idx}
clickClose={onClickClose}
onShiftOptions={onShiftOptions}
type={DndItemType.ColumnOption}
>
<ColumnOption column={column} showType />
</OptionWrapper>
));
return (
<DndSelectLabel<string | string[], ColumnMeta[]>
values={values}
onDrop={onDrop}
canDrop={canDrop}
valuesRenderer={valuesRenderer}
accept={DndItemType.Column}
{...props}
/>
);
};

View File

@@ -1,119 +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 React, { useState } from 'react';
import { useDrop } from 'react-dnd';
import { isEmpty } from 'lodash';
import { t, useTheme } from '@superset-ui/core';
import { BaseControlConfig, ColumnMeta } from '@superset-ui/chart-controls';
import ControlHeader from 'src/explore/components/ControlHeader';
import {
AddControlLabel,
DndLabelsContainer,
HeaderContainer,
} from 'src/explore/components/OptionControls';
import {
DatasourcePanelDndItem,
DatasourcePanelDndType,
} from 'src/explore/components/DatasourcePanel/types';
import Icon from 'src/components/Icon';
import OptionWrapper from './components/OptionWrapper';
import { OptionSelector } from './utils';
interface LabelProps extends BaseControlConfig {
name: string;
value: string[] | string | null;
onChange: (value: string[] | string | null) => void;
options: { string: ColumnMeta };
}
export default function DndColumnSelectLabel(props: LabelProps) {
const theme = useTheme();
const { value, options } = props;
const optionSelector = new OptionSelector(options, value);
const [groupByOptions, setGroupByOptions] = useState<ColumnMeta[]>(
optionSelector.groupByOptions,
);
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
accept: DatasourcePanelDndType.COLUMN,
drop: (item: DatasourcePanelDndItem) => {
if (!optionSelector.isArray && !isEmpty(optionSelector.groupByOptions)) {
optionSelector.replace(0, item.metricOrColumnName);
} else {
optionSelector.add(item.metricOrColumnName);
}
setGroupByOptions(optionSelector.groupByOptions);
props.onChange(optionSelector.getValues());
},
canDrop: (item: DatasourcePanelDndItem) =>
!optionSelector.has(item.metricOrColumnName),
collect: monitor => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
type: monitor.getItemType(),
}),
});
function onClickClose(index: number) {
optionSelector.del(index);
setGroupByOptions(optionSelector.groupByOptions);
props.onChange(optionSelector.getValues());
}
function onShiftOptions(dragIndex: number, hoverIndex: number) {
optionSelector.swap(dragIndex, hoverIndex);
setGroupByOptions(optionSelector.groupByOptions);
props.onChange(optionSelector.getValues());
}
function renderPlaceHolder() {
return (
<AddControlLabel cancelHover>
<Icon name="plus-small" color={theme.colors.grayscale.light1} />
{t('Drop Columns')}
</AddControlLabel>
);
}
function renderOptions() {
return groupByOptions.map((column, idx) => (
<OptionWrapper
key={idx}
index={idx}
column={column}
clickClose={onClickClose}
onShiftOptions={onShiftOptions}
/>
));
}
return (
<div ref={datasourcePanelDrop}>
<HeaderContainer>
<ControlHeader {...props} />
</HeaderContainer>
<DndLabelsContainer canDrop={canDrop} isOver={isOver}>
{isEmpty(groupByOptions) ? renderPlaceHolder() : renderOptions()}
</DndLabelsContainer>
</div>
);
}

View File

@@ -0,0 +1,334 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with work for additional information
* regarding copyright ownership. The ASF licenses file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use 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 React, { useEffect, useMemo, useState } from 'react';
import { logging, SupersetClient } from '@superset-ui/core';
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
import { Tooltip } from 'src/common/components/Tooltip';
import { OPERATORS } from 'src/explore/constants';
import { OptionSortType } from 'src/explore/types';
import { DndFilterSelectProps, FilterOptionValueType } from './types';
import AdhocFilterPopoverTrigger from '../FilterControl/AdhocFilterPopoverTrigger';
import OptionWrapper from './components/OptionWrapper';
import DndSelectLabel from './DndSelectLabel';
import AdhocFilter, {
CLAUSES,
EXPRESSION_TYPES,
} from '../FilterControl/AdhocFilter';
import AdhocMetric from '../MetricControl/AdhocMetric';
import {
DatasourcePanelDndItem,
DndItemValue,
} from '../../DatasourcePanel/types';
import { DndItemType } from '../../DndItemType';
const isDictionaryForAdhocFilter = (value: FilterOptionValueType) =>
!(value instanceof AdhocFilter) && value?.expressionType;
export const DndFilterSelect = (props: DndFilterSelectProps) => {
const propsValues = Array.from(props.value ?? []);
const [values, setValues] = useState(
propsValues.map((filter: FilterOptionValueType) =>
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
),
);
const [partitionColumn, setPartitionColumn] = useState(undefined);
const [newFilterPopoverVisible, setNewFilterPopoverVisible] = useState(false);
const [droppedItem, setDroppedItem] = useState<DndItemValue | null>(null);
const optionsForSelect = (
columns: ColumnMeta[],
formData: Record<string, any>,
) => {
const options: OptionSortType[] = [
...columns,
...[...(formData?.metrics || []), formData?.metric].map(
metric =>
metric &&
(typeof metric === 'string'
? { saved_metric_name: metric }
: new AdhocMetric(metric)),
),
].filter(option => option);
return options
.reduce(
(
results: (OptionSortType & { filterOptionName: string })[],
option,
) => {
if ('saved_metric_name' in option && option.saved_metric_name) {
results.push({
...option,
filterOptionName: option.saved_metric_name,
});
} else if ('column_name' in option && option.column_name) {
results.push({
...option,
filterOptionName: `_col_${option.column_name}`,
});
} else if (option instanceof AdhocMetric) {
results.push({
...option,
filterOptionName: `_adhocmetric_${option.label}`,
});
}
return results;
},
[],
)
.sort(
(a: OptionSortType, b: OptionSortType) =>
(a.saved_metric_name || a.column_name || a.label)?.localeCompare(
b.saved_metric_name || b.column_name || b.label || '',
) ?? 0,
);
};
const [options, setOptions] = useState(
optionsForSelect(props.columns, props.formData),
);
useEffect(() => {
const { datasource } = props;
if (datasource && datasource.type === 'table') {
const dbId = datasource.database?.id;
const {
datasource_name: name,
schema,
is_sqllab_view: isSqllabView,
} = datasource;
if (!isSqllabView && dbId && name && schema) {
SupersetClient.get({
endpoint: `/superset/extra_table_metadata/${dbId}/${name}/${schema}/`,
})
.then(({ json }: { json: Record<string, any> }) => {
if (json && json.partitions) {
const { partitions } = json;
// for now only show latest_partition option
// when table datasource has only 1 partition key.
if (
partitions &&
partitions.cols &&
Object.keys(partitions.cols).length === 1
) {
setPartitionColumn(partitions.cols[0]);
}
}
})
.catch((error: Record<string, any>) => {
logging.error('fetch extra_table_metadata:', error.statusText);
});
}
}
}, []);
useEffect(() => {
setOptions(optionsForSelect(props.columns, props.formData));
}, [props.columns, props.formData]);
useEffect(() => {
setValues(
(props.value || []).map((filter: FilterOptionValueType) =>
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
),
);
}, [props.value]);
const onClickClose = (index: number) => {
const valuesCopy = [...values];
valuesCopy.splice(index, 1);
setValues(valuesCopy);
props.onChange(valuesCopy);
};
const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
const newValues = [...values];
[newValues[hoverIndex], newValues[dragIndex]] = [
newValues[dragIndex],
newValues[hoverIndex],
];
setValues(newValues);
};
const getMetricExpression = (savedMetricName: string) =>
props.savedMetrics.find(
(savedMetric: Metric) => savedMetric.metric_name === savedMetricName,
)?.expression;
const mapOption = (option: FilterOptionValueType) => {
// already a AdhocFilter, skip
if (option instanceof AdhocFilter) {
return option;
}
const filterOptions = option as Record<string, any>;
// via datasource saved metric
if (filterOptions.saved_metric_name) {
return new AdhocFilter({
expressionType:
props.datasource.type === 'druid'
? EXPRESSION_TYPES.SIMPLE
: EXPRESSION_TYPES.SQL,
subject:
props.datasource.type === 'druid'
? filterOptions.saved_metric_name
: getMetricExpression(filterOptions.saved_metric_name),
operator: OPERATORS['>'],
comparator: 0,
clause: CLAUSES.HAVING,
});
}
// has a custom label, meaning it's custom column
if (filterOptions.label) {
return new AdhocFilter({
expressionType:
props.datasource.type === 'druid'
? EXPRESSION_TYPES.SIMPLE
: EXPRESSION_TYPES.SQL,
subject:
props.datasource.type === 'druid'
? filterOptions.label
: new AdhocMetric(option).translateToSql(),
operator: OPERATORS['>'],
comparator: 0,
clause: CLAUSES.HAVING,
});
}
// add a new filter item
if (filterOptions.column_name) {
return new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: filterOptions.column_name,
operator: OPERATORS['=='],
comparator: '',
clause: CLAUSES.WHERE,
isNew: true,
});
}
return null;
};
const onFilterEdit = (changedFilter: AdhocFilter) => {
props.onChange(
values.map((value: AdhocFilter) => {
if (value.filterOptionName === changedFilter.filterOptionName) {
return changedFilter;
}
return value;
}),
);
};
const onNewFilter = (newFilter: AdhocFilter) => {
const mappedOption = mapOption(newFilter);
if (mappedOption) {
const newValues = [...values, mappedOption];
setValues(newValues);
props.onChange(newValues);
}
};
const togglePopover = (visible: boolean) => {
setNewFilterPopoverVisible(visible);
};
const closePopover = () => {
togglePopover(false);
};
const valuesRenderer = () =>
values.map((adhocFilter: AdhocFilter, index: number) => {
const label = adhocFilter.getDefaultLabel();
return (
<AdhocFilterPopoverTrigger
key={index}
adhocFilter={adhocFilter}
options={options}
datasource={props.datasource}
onFilterEdit={onFilterEdit}
partitionColumn={partitionColumn}
>
<OptionWrapper
key={index}
index={index}
clickClose={onClickClose}
onShiftOptions={onShiftOptions}
type={DndItemType.FilterOption}
withCaret
>
<Tooltip title={label}>{label}</Tooltip>
</OptionWrapper>
</AdhocFilterPopoverTrigger>
);
});
const adhocFilter = useMemo(() => {
if (droppedItem?.metric_name) {
return new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
clause: CLAUSES.HAVING,
sqlExpression: droppedItem?.expression,
});
}
if (droppedItem instanceof AdhocMetric) {
return new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
clause: CLAUSES.HAVING,
sqlExpression: (droppedItem as AdhocMetric)?.translateToSql(),
});
}
return new AdhocFilter({
subject: (droppedItem as ColumnMeta)?.column_name,
});
}, [droppedItem]);
return (
<>
<DndSelectLabel<FilterOptionValueType, FilterOptionValueType[]>
values={values}
onDrop={(item: DatasourcePanelDndItem) => {
setDroppedItem(item.value);
togglePopover(true);
}}
canDrop={() => true}
valuesRenderer={valuesRenderer}
accept={[
DndItemType.Column,
DndItemType.Metric,
DndItemType.MetricOption,
DndItemType.AdhocMetricOption,
]}
{...props}
/>
<AdhocFilterPopoverTrigger
adhocFilter={adhocFilter}
options={options}
datasource={props.datasource}
onFilterEdit={onNewFilter}
partitionColumn={partitionColumn}
isControlledComponent
visible={newFilterPopoverVisible}
togglePopover={togglePopover}
closePopover={closePopover}
createNew
>
<div />
</AdhocFilterPopoverTrigger>
</>
);
};

View File

@@ -0,0 +1,73 @@
/**
* 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 React from 'react';
import { useDrop } from 'react-dnd';
import { isEmpty } from 'lodash';
import { t, useTheme } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import {
AddControlLabel,
DndLabelsContainer,
HeaderContainer,
} from 'src/explore/components/OptionControls';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import Icon from 'src/components/Icon';
import { DndColumnSelectProps } from './types';
export default function DndSelectLabel<T, O>(
props: DndColumnSelectProps<T, O>,
) {
const theme = useTheme();
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
accept: props.accept,
drop: (item: DatasourcePanelDndItem) => {
props.onDrop(item);
},
canDrop: (item: DatasourcePanelDndItem) => props.canDrop(item),
collect: monitor => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
type: monitor.getItemType(),
}),
});
function renderPlaceHolder() {
return (
<AddControlLabel cancelHover>
<Icon name="plus-small" color={theme.colors.grayscale.light1} />
{t('Drop Columns')}
</AddControlLabel>
);
}
return (
<div ref={datasourcePanelDrop}>
<HeaderContainer>
<ControlHeader {...props} />
</HeaderContainer>
<DndLabelsContainer canDrop={canDrop} isOver={isOver}>
{isEmpty(props.values) ? renderPlaceHolder() : props.valuesRenderer()}
</DndLabelsContainer>
</div>
);
}

View File

@@ -18,7 +18,6 @@
*/
import React from 'react';
import { useTheme } from '@superset-ui/core';
import { ColumnOption } from '@superset-ui/chart-controls';
import Icon from 'src/components/Icon';
import {
CaretContainer,
@@ -32,7 +31,10 @@ export default function Option(props: OptionProps) {
const theme = useTheme();
return (
<OptionControlContainer data-test="option-label">
<OptionControlContainer
data-test="option-label"
withCaret={props.withCaret}
>
<CloseContainer
role="button"
data-test="remove-control-button"
@@ -40,9 +42,7 @@ export default function Option(props: OptionProps) {
>
<Icon name="x-small" color={theme.colors.grayscale.light1} />
</CloseContainer>
<Label data-test="control-label">
<ColumnOption column={props.column} showType />
</Label>
<Label data-test="control-label">{props.children}</Label>
{props.withCaret && (
<CaretContainer>
<Icon name="caret-right" color={theme.colors.grayscale.light1} />

View File

@@ -25,15 +25,25 @@ import {
} from 'react-dnd';
import { DragContainer } from 'src/explore/components/OptionControls';
import Option from './Option';
import { OptionProps, GroupByItemInterface, GroupByItemType } from '../types';
import { OptionProps, OptionItemInterface } from '../types';
import { DndItemType } from '../../../DndItemType';
export default function OptionWrapper(props: OptionProps) {
const { index, onShiftOptions } = props;
export default function OptionWrapper(
props: OptionProps & { type: DndItemType },
) {
const {
index,
type,
onShiftOptions,
clickClose,
withCaret,
children,
} = props;
const ref = useRef<HTMLDivElement>(null);
const item: GroupByItemInterface = {
const item: OptionItemInterface = {
dragIndex: index,
type: GroupByItemType,
type,
};
const [, drag] = useDrag({
item,
@@ -43,9 +53,9 @@ export default function OptionWrapper(props: OptionProps) {
});
const [, drop] = useDrop({
accept: GroupByItemType,
accept: type,
hover: (item: GroupByItemInterface, monitor: DropTargetMonitor) => {
hover: (item: OptionItemInterface, monitor: DropTargetMonitor) => {
if (!ref.current) {
return;
}
@@ -89,8 +99,15 @@ export default function OptionWrapper(props: OptionProps) {
drag(drop(ref));
return (
<DragContainer ref={ref}>
<Option {...props} />
<DragContainer ref={ref} {...props}>
<Option
index={index}
clickClose={clickClose}
onShiftOptions={onShiftOptions}
withCaret={withCaret}
>
{children}
</Option>
</DragContainer>
);
}

View File

@@ -16,4 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
export { default } from './DndColumnSelectLabel';
export { default } from './DndSelectLabel';
export * from './DndColumnSelect';
export * from './DndFilterSelect';

View File

@@ -16,19 +16,51 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ColumnMeta } from '@superset-ui/chart-controls';
import { ReactNode } from 'react';
import { AdhocFilter } from '@superset-ui/core';
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
import { DndItemType } from '../../DndItemType';
export interface OptionProps {
column: ColumnMeta;
children: ReactNode;
index: number;
clickClose: (index: number) => void;
onShiftOptions: (dragIndex: number, hoverIndex: number) => void;
withCaret?: boolean;
}
export const GroupByItemType = 'groupByItem';
export interface GroupByItemInterface {
type: typeof GroupByItemType;
export interface OptionItemInterface {
type: string;
dragIndex: number;
}
export interface LabelProps<T = string[] | string> {
name: string;
value?: T;
onChange: (value?: T) => void;
options: { string: ColumnMeta };
}
export interface DndColumnSelectProps<
T = string[] | string,
O = string[] | string
> extends LabelProps<T> {
values?: O;
onDrop: (item: DatasourcePanelDndItem) => void;
canDrop: (item: DatasourcePanelDndItem) => boolean;
valuesRenderer: () => ReactNode;
accept: DndItemType | DndItemType[];
}
export type FilterOptionValueType = Record<string, any> | AdhocFilter;
export interface DndFilterSelectProps {
name: string;
value: FilterOptionValueType[];
columns: ColumnMeta[];
datasource: Record<string, any>;
formData: Record<string, any>;
savedMetrics: Metric[];
onChange: (filters: FilterOptionValueType[]) => void;
options: { string: ColumnMeta };
}

View File

@@ -19,7 +19,7 @@
import { ColumnMeta } from '@superset-ui/chart-controls';
export class OptionSelector {
groupByOptions: ColumnMeta[];
values: ColumnMeta[];
options: { string: ColumnMeta };
@@ -27,18 +27,18 @@ export class OptionSelector {
constructor(
options: { string: ColumnMeta },
values: string[] | string | null,
initialValues?: string[] | string,
) {
this.options = options;
let groupByValues: string[];
if (Array.isArray(values)) {
groupByValues = values;
let values: string[];
if (Array.isArray(initialValues)) {
values = initialValues;
this.isArray = true;
} else {
groupByValues = values ? [values] : [];
values = initialValues ? [initialValues] : [];
this.isArray = false;
}
this.groupByOptions = groupByValues
this.values = values
.map(value => {
if (value in options) {
return options[value];
@@ -50,37 +50,32 @@ export class OptionSelector {
add(value: string) {
if (value in this.options) {
this.groupByOptions.push(this.options[value]);
this.values.push(this.options[value]);
}
}
del(idx: number) {
this.groupByOptions.splice(idx, 1);
this.values.splice(idx, 1);
}
replace(idx: number, value: string) {
if (this.groupByOptions[idx]) {
this.groupByOptions[idx] = this.options[value];
if (this.values[idx]) {
this.values[idx] = this.options[value];
}
}
swap(a: number, b: number) {
[this.groupByOptions[a], this.groupByOptions[b]] = [
this.groupByOptions[b],
this.groupByOptions[a],
];
[this.values[a], this.values[b]] = [this.values[b], this.values[a]];
}
has(groupBy: string): boolean {
return !!this.getValues()?.includes(groupBy);
}
getValues(): string[] | string | null {
getValues(): string[] | string | undefined {
if (!this.isArray) {
return this.groupByOptions.length > 0
? this.groupByOptions[0].column_name
: null;
return this.values.length > 0 ? this.values[0].column_name : undefined;
}
return this.groupByOptions.map(option => option.column_name);
return this.values.map(option => option.column_name);
}
}