mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat: conditional formatting improvements in tables (#34330)
This commit is contained in:
@@ -458,6 +458,10 @@ export enum Comparator {
|
||||
BetweenOrEqual = '≤ x ≤',
|
||||
BetweenOrLeftEqual = '≤ x <',
|
||||
BetweenOrRightEqual = '< x ≤',
|
||||
BeginsWith = 'begins with',
|
||||
EndsWith = 'ends with',
|
||||
Containing = 'containing',
|
||||
NotContaining = 'not containing',
|
||||
}
|
||||
|
||||
export const MultipleValueComparators = [
|
||||
@@ -469,7 +473,7 @@ export const MultipleValueComparators = [
|
||||
|
||||
export type ConditionalFormattingConfig = {
|
||||
operator?: Comparator;
|
||||
targetValue?: number;
|
||||
targetValue?: number | string;
|
||||
targetValueLeft?: number;
|
||||
targetValueRight?: number;
|
||||
column?: string;
|
||||
@@ -478,7 +482,7 @@ export type ConditionalFormattingConfig = {
|
||||
|
||||
export type ColorFormatters = {
|
||||
column: string;
|
||||
getColorFromValue: (value: number) => string | undefined;
|
||||
getColorFromValue: (value: number | string) => string | undefined;
|
||||
}[];
|
||||
|
||||
export default {};
|
||||
|
||||
@@ -32,13 +32,18 @@ const MIN_OPACITY_BOUNDED = 0.05;
|
||||
const MIN_OPACITY_UNBOUNDED = 0;
|
||||
const MAX_OPACITY = 1;
|
||||
export const getOpacity = (
|
||||
value: number,
|
||||
cutoffPoint: number,
|
||||
extremeValue: number,
|
||||
value: number | string,
|
||||
cutoffPoint: number | string,
|
||||
extremeValue: number | string,
|
||||
minOpacity = MIN_OPACITY_BOUNDED,
|
||||
maxOpacity = MAX_OPACITY,
|
||||
) => {
|
||||
if (extremeValue === cutoffPoint) {
|
||||
if (
|
||||
extremeValue === cutoffPoint ||
|
||||
typeof cutoffPoint !== 'number' ||
|
||||
typeof extremeValue !== 'number' ||
|
||||
typeof value !== 'number'
|
||||
) {
|
||||
return maxOpacity;
|
||||
}
|
||||
return Math.min(
|
||||
@@ -61,16 +66,16 @@ export const getColorFunction = (
|
||||
targetValueRight,
|
||||
colorScheme,
|
||||
}: ConditionalFormattingConfig,
|
||||
columnValues: number[],
|
||||
columnValues: number[] | string[],
|
||||
alpha?: boolean,
|
||||
) => {
|
||||
let minOpacity = MIN_OPACITY_BOUNDED;
|
||||
const maxOpacity = MAX_OPACITY;
|
||||
|
||||
let comparatorFunction: (
|
||||
value: number,
|
||||
allValues: number[],
|
||||
) => false | { cutoffValue: number; extremeValue: number };
|
||||
value: number | string,
|
||||
allValues: number[] | string[],
|
||||
) => false | { cutoffValue: number | string; extremeValue: number | string };
|
||||
if (operator === undefined || colorScheme === undefined) {
|
||||
return () => undefined;
|
||||
}
|
||||
@@ -90,7 +95,10 @@ export const getColorFunction = (
|
||||
switch (operator) {
|
||||
case Comparator.None:
|
||||
minOpacity = MIN_OPACITY_UNBOUNDED;
|
||||
comparatorFunction = (value: number, allValues: number[]) => {
|
||||
comparatorFunction = (value: number | string, allValues: number[]) => {
|
||||
if (typeof value !== 'number') {
|
||||
return { cutoffValue: value!, extremeValue: value! };
|
||||
}
|
||||
const cutoffValue = Math.min(...allValues);
|
||||
const extremeValue = Math.max(...allValues);
|
||||
return value >= cutoffValue && value <= extremeValue
|
||||
@@ -100,49 +108,65 @@ export const getColorFunction = (
|
||||
break;
|
||||
case Comparator.GreaterThan:
|
||||
comparatorFunction = (value: number, allValues: number[]) =>
|
||||
value > targetValue!
|
||||
? { cutoffValue: targetValue!, extremeValue: Math.max(...allValues) }
|
||||
typeof targetValue === 'number' && value > targetValue!
|
||||
? {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue: Math.max(...allValues),
|
||||
}
|
||||
: false;
|
||||
break;
|
||||
case Comparator.LessThan:
|
||||
comparatorFunction = (value: number, allValues: number[]) =>
|
||||
value < targetValue!
|
||||
? { cutoffValue: targetValue!, extremeValue: Math.min(...allValues) }
|
||||
typeof targetValue === 'number' && value < targetValue!
|
||||
? {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue: Math.min(...allValues),
|
||||
}
|
||||
: false;
|
||||
break;
|
||||
case Comparator.GreaterOrEqual:
|
||||
comparatorFunction = (value: number, allValues: number[]) =>
|
||||
value >= targetValue!
|
||||
? { cutoffValue: targetValue!, extremeValue: Math.max(...allValues) }
|
||||
typeof targetValue === 'number' && value >= targetValue!
|
||||
? {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue: Math.max(...allValues),
|
||||
}
|
||||
: false;
|
||||
break;
|
||||
case Comparator.LessOrEqual:
|
||||
comparatorFunction = (value: number, allValues: number[]) =>
|
||||
value <= targetValue!
|
||||
? { cutoffValue: targetValue!, extremeValue: Math.min(...allValues) }
|
||||
typeof targetValue === 'number' && value <= targetValue!
|
||||
? {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue: Math.min(...allValues),
|
||||
}
|
||||
: false;
|
||||
break;
|
||||
case Comparator.Equal:
|
||||
comparatorFunction = (value: number) =>
|
||||
comparatorFunction = (value: number | string) =>
|
||||
value === targetValue!
|
||||
? { cutoffValue: targetValue!, extremeValue: targetValue! }
|
||||
: false;
|
||||
break;
|
||||
case Comparator.NotEqual:
|
||||
comparatorFunction = (value: number, allValues: number[]) => {
|
||||
if (value === targetValue!) {
|
||||
return false;
|
||||
if (typeof targetValue === 'number') {
|
||||
if (value === targetValue!) {
|
||||
return false;
|
||||
}
|
||||
const max = Math.max(...allValues);
|
||||
const min = Math.min(...allValues);
|
||||
return {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue:
|
||||
Math.abs(targetValue! - min) > Math.abs(max - targetValue!)
|
||||
? min
|
||||
: max,
|
||||
};
|
||||
}
|
||||
const max = Math.max(...allValues);
|
||||
const min = Math.min(...allValues);
|
||||
return {
|
||||
cutoffValue: targetValue!,
|
||||
extremeValue:
|
||||
Math.abs(targetValue! - min) > Math.abs(max - targetValue!)
|
||||
? min
|
||||
: max,
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
break;
|
||||
case Comparator.Between:
|
||||
comparatorFunction = (value: number) =>
|
||||
@@ -168,12 +192,38 @@ export const getColorFunction = (
|
||||
? { cutoffValue: targetValueLeft!, extremeValue: targetValueRight! }
|
||||
: false;
|
||||
break;
|
||||
case Comparator.BeginsWith:
|
||||
comparatorFunction = (value: string) =>
|
||||
isString(value) && value?.startsWith(targetValue as string)
|
||||
? { cutoffValue: targetValue!, extremeValue: targetValue! }
|
||||
: false;
|
||||
break;
|
||||
case Comparator.EndsWith:
|
||||
comparatorFunction = (value: string) =>
|
||||
isString(value) && value?.endsWith(targetValue as string)
|
||||
? { cutoffValue: targetValue!, extremeValue: targetValue! }
|
||||
: false;
|
||||
break;
|
||||
case Comparator.Containing:
|
||||
comparatorFunction = (value: string) =>
|
||||
isString(value) &&
|
||||
value?.toLowerCase().includes((targetValue as string).toLowerCase())
|
||||
? { cutoffValue: targetValue!, extremeValue: targetValue! }
|
||||
: false;
|
||||
break;
|
||||
case Comparator.NotContaining:
|
||||
comparatorFunction = (value: string) =>
|
||||
isString(value) &&
|
||||
!value?.toLowerCase().includes((targetValue as string).toLowerCase())
|
||||
? { cutoffValue: targetValue!, extremeValue: targetValue! }
|
||||
: false;
|
||||
break;
|
||||
default:
|
||||
comparatorFunction = () => false;
|
||||
break;
|
||||
}
|
||||
|
||||
return (value: number) => {
|
||||
return (value: number | string) => {
|
||||
const compareResult = comparatorFunction(value, columnValues);
|
||||
if (compareResult === false) return undefined;
|
||||
const { cutoffValue, extremeValue } = compareResult;
|
||||
@@ -218,3 +268,7 @@ export const getColorFormatters = memoizeOne(
|
||||
[],
|
||||
) ?? [],
|
||||
);
|
||||
|
||||
function isString(value: unknown) {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ const mockData = [
|
||||
];
|
||||
const countValues = mockData.map(row => row.count);
|
||||
|
||||
const strData = [{ name: 'Brian' }, { name: 'Carlos' }, { name: 'Diana' }];
|
||||
const strValues = strData.map(row => row.name);
|
||||
|
||||
describe('round', () => {
|
||||
it('round', () => {
|
||||
expect(round(1)).toEqual(1);
|
||||
@@ -339,6 +342,90 @@ describe('getColorFunction()', () => {
|
||||
expect(colorFunction(50)).toBeUndefined();
|
||||
expect(colorFunction(100)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getColorFunction BeginsWith', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.BeginsWith,
|
||||
targetValue: 'C',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Brian')).toBeUndefined();
|
||||
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
it('getColorFunction EndsWith', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.EndsWith,
|
||||
targetValue: 'n',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Carlos')).toBeUndefined();
|
||||
expect(colorFunction('Brian')).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
it('getColorFunction Containing', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.Containing,
|
||||
targetValue: 'o',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Diana')).toBeUndefined();
|
||||
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
it('getColorFunction NotContaining', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.NotContaining,
|
||||
targetValue: 'i',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Diana')).toBeUndefined();
|
||||
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
it('getColorFunction Equal', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.Equal,
|
||||
targetValue: 'Diana',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Carlos')).toBeUndefined();
|
||||
expect(colorFunction('Diana')).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
it('getColorFunction None', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
operator: Comparator.None,
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
strValues,
|
||||
);
|
||||
expect(colorFunction('Diana')).toEqual('#FF0000FF');
|
||||
expect(colorFunction('Carlos')).toEqual('#FF0000FF');
|
||||
expect(colorFunction('Brian')).toEqual('#FF0000FF');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColorFormatters()', () => {
|
||||
@@ -388,4 +475,47 @@ describe('getColorFormatters()', () => {
|
||||
const colorFormatters = getColorFormatters(undefined, mockData);
|
||||
expect(colorFormatters.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('correct column string config', () => {
|
||||
const columnConfigString = [
|
||||
{
|
||||
operator: Comparator.BeginsWith,
|
||||
targetValue: 'D',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
{
|
||||
operator: Comparator.EndsWith,
|
||||
targetValue: 'n',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
{
|
||||
operator: Comparator.Containing,
|
||||
targetValue: 'o',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
{
|
||||
operator: Comparator.NotContaining,
|
||||
targetValue: 'i',
|
||||
colorScheme: '#FF0000',
|
||||
column: 'name',
|
||||
},
|
||||
];
|
||||
const colorFormatters = getColorFormatters(columnConfigString, strData);
|
||||
expect(colorFormatters.length).toEqual(4);
|
||||
|
||||
expect(colorFormatters[0].column).toEqual('name');
|
||||
expect(colorFormatters[0].getColorFromValue('Diana')).toEqual('#FF0000FF');
|
||||
|
||||
expect(colorFormatters[1].column).toEqual('name');
|
||||
expect(colorFormatters[1].getColorFromValue('Brian')).toEqual('#FF0000FF');
|
||||
|
||||
expect(colorFormatters[2].column).toEqual('name');
|
||||
expect(colorFormatters[2].getColorFromValue('Carlos')).toEqual('#FF0000FF');
|
||||
|
||||
expect(colorFormatters[3].column).toEqual('name');
|
||||
expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -722,6 +722,8 @@ const config: ControlPanelConfig = {
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap[colname] ?? colname),
|
||||
dataType:
|
||||
colnames && coltypes[colnames?.indexOf(colname)],
|
||||
}))
|
||||
: [];
|
||||
const columnOptions = explore?.controls?.time_compare?.value
|
||||
|
||||
@@ -110,6 +110,8 @@ export default {
|
||||
(Array.isArray(verboseMap)
|
||||
? verboseMap[colname as number]
|
||||
: verboseMap[colname as string]) ?? colname,
|
||||
dataType:
|
||||
colnames && coltypes[colnames?.indexOf(colname)],
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
|
||||
@@ -413,6 +413,8 @@ const config: ControlPanelConfig = {
|
||||
? (explore?.datasource as Dataset)?.verbose_map
|
||||
: (explore?.datasource?.columns ?? {});
|
||||
const chartStatus = chart?.chartStatus;
|
||||
const { colnames, coltypes } =
|
||||
chart?.queriesResponse?.[0] ?? {};
|
||||
const metricColumn = values.map(value => {
|
||||
if (typeof value === 'string') {
|
||||
return {
|
||||
@@ -420,9 +422,15 @@ const config: ControlPanelConfig = {
|
||||
label: Array.isArray(verboseMap)
|
||||
? value
|
||||
: verboseMap[value],
|
||||
dataType: colnames && coltypes[colnames?.indexOf(value)],
|
||||
};
|
||||
}
|
||||
return { value: value.label, label: value.label };
|
||||
return {
|
||||
value: value.label,
|
||||
label: value.label,
|
||||
dataType:
|
||||
colnames && coltypes[colnames?.indexOf(value.label)],
|
||||
};
|
||||
});
|
||||
return {
|
||||
removeIrrelevantConditions: chartStatus === 'success',
|
||||
|
||||
@@ -726,7 +726,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
const {
|
||||
key,
|
||||
label: originalLabel,
|
||||
isNumeric,
|
||||
dataType,
|
||||
isMetric,
|
||||
isPercentMetric,
|
||||
@@ -771,7 +770,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
const { truncateLongCells } = config;
|
||||
|
||||
const hasColumnColorFormatters =
|
||||
isNumeric &&
|
||||
Array.isArray(columnColorFormatters) &&
|
||||
columnColorFormatters.length > 0;
|
||||
|
||||
|
||||
@@ -772,17 +772,22 @@ const config: ControlPanelConfig = {
|
||||
chart?.queriesResponse?.[0] ?? {};
|
||||
const numericColumns =
|
||||
Array.isArray(colnames) && Array.isArray(coltypes)
|
||||
? colnames
|
||||
.filter(
|
||||
(colname: string, index: number) =>
|
||||
coltypes[index] === GenericDataType.Numeric,
|
||||
)
|
||||
.map((colname: string) => ({
|
||||
value: colname,
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap[colname] ?? colname),
|
||||
}))
|
||||
? colnames.reduce((acc, colname, index) => {
|
||||
if (
|
||||
coltypes[index] === GenericDataType.Numeric ||
|
||||
(!explore?.controls?.time_compare?.value &&
|
||||
coltypes[index] === GenericDataType.String)
|
||||
) {
|
||||
acc.push({
|
||||
value: colname,
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap[colname] ?? colname),
|
||||
dataType: coltypes[index],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
: [];
|
||||
const columnOptions = explore?.controls?.time_compare?.value
|
||||
? processComparisonColumns(
|
||||
|
||||
@@ -571,6 +571,191 @@ describe('plugin-chart-table', () => {
|
||||
);
|
||||
cells = document.querySelectorAll('td');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter(operator begins with)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: 'begins with',
|
||||
targetValue: 'J',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator ends with)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: 'ends with',
|
||||
targetValue: 'ia',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Maria')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator containing)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: 'containing',
|
||||
targetValue: 'c',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator not containing)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: 'not containing',
|
||||
targetValue: 'i',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator =)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: '=',
|
||||
targetValue: 'Joe',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('render color with string column color formatter (operator None)', () => {
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...transformProps({
|
||||
...testData.advanced,
|
||||
rawFormData: {
|
||||
...testData.advanced.rawFormData,
|
||||
conditional_formatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'name',
|
||||
operator: 'None',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
expect(getComputedStyle(screen.getByText('Maria')).background).toBe(
|
||||
'rgba(172, 225, 196, 1)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,13 +24,19 @@ import {
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { Comparator } from '@superset-ui/chart-controls';
|
||||
import { ColorSchemeEnum } from '@superset-ui/plugin-chart-table';
|
||||
import { GenericDataType } from '@superset-ui/core';
|
||||
import { FormattingPopoverContent } from './FormattingPopoverContent';
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
const columns = [
|
||||
{ label: 'Column 1', value: 'column1' },
|
||||
{ label: 'Column 2', value: 'column2' },
|
||||
{ label: 'Column 1', value: 'column1', dataType: GenericDataType.Numeric },
|
||||
{ label: 'Column 2', value: 'column2', dataType: GenericDataType.Numeric },
|
||||
];
|
||||
|
||||
const columnsStringType = [
|
||||
{ label: 'Column 1', value: 'column1', dataType: GenericDataType.String },
|
||||
{ label: 'Column 2', value: 'column2', dataType: GenericDataType.String },
|
||||
];
|
||||
|
||||
const extraColorChoices = [
|
||||
@@ -119,3 +125,19 @@ test('renders None for operator when Green for increase is selected', async () =
|
||||
// Assert that the operator is set to 'None'
|
||||
expect(screen.getByText(/none/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays the correct input fields based on the selected string type operator', async () => {
|
||||
render(
|
||||
<FormattingPopoverContent
|
||||
onChange={mockOnChange}
|
||||
columns={columnsStringType}
|
||||
extraColorChoices={extraColorChoices}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getAllByLabelText('Operator')[0], {
|
||||
target: { value: Comparator.BeginsWith },
|
||||
});
|
||||
fireEvent.click(await screen.findByTitle('begins with'));
|
||||
expect(await screen.findByLabelText('Target value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,8 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { styled, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
GenericDataType,
|
||||
styled,
|
||||
SupersetTheme,
|
||||
t,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
Comparator,
|
||||
MultipleValueComparators,
|
||||
@@ -28,6 +34,7 @@ import {
|
||||
Form,
|
||||
FormItem,
|
||||
InputNumber,
|
||||
Input,
|
||||
Col,
|
||||
Row,
|
||||
type FormProps,
|
||||
@@ -45,6 +52,10 @@ const FullWidthInputNumber = styled(InputNumber)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FullWidthInput = styled(Input)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const JustifyEnd = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -70,6 +81,15 @@ const operatorOptions = [
|
||||
{ value: Comparator.BetweenOrRightEqual, label: '< x ≤' },
|
||||
];
|
||||
|
||||
const stringOperatorOptions = [
|
||||
{ value: Comparator.None, label: t('None') },
|
||||
{ value: Comparator.Equal, label: '=' },
|
||||
{ value: Comparator.BeginsWith, label: t('begins with') },
|
||||
{ value: Comparator.EndsWith, label: t('ends with') },
|
||||
{ value: Comparator.Containing, label: t('containing') },
|
||||
{ value: Comparator.NotContaining, label: t('not containing') },
|
||||
];
|
||||
|
||||
const targetValueValidator =
|
||||
(
|
||||
compare: (targetValue: number, compareValue: number) => boolean,
|
||||
@@ -132,24 +152,41 @@ const shouldFormItemUpdate = (
|
||||
isOperatorMultiValue(prevValues.operator) !==
|
||||
isOperatorMultiValue(currentValues.operator);
|
||||
|
||||
const renderOperator = ({ showOnlyNone }: { showOnlyNone?: boolean } = {}) => (
|
||||
<FormItem
|
||||
name="operator"
|
||||
label={t('Operator')}
|
||||
rules={rulesRequired}
|
||||
initialValue={operatorOptions[0].value}
|
||||
>
|
||||
<Select
|
||||
ariaLabel={t('Operator')}
|
||||
options={showOnlyNone ? [operatorOptions[0]] : operatorOptions}
|
||||
/>
|
||||
</FormItem>
|
||||
);
|
||||
const renderOperator = ({
|
||||
showOnlyNone,
|
||||
columnType,
|
||||
}: { showOnlyNone?: boolean; columnType?: GenericDataType } = {}) => {
|
||||
const options =
|
||||
columnType === GenericDataType.String
|
||||
? stringOperatorOptions
|
||||
: operatorOptions;
|
||||
|
||||
const renderOperatorFields = ({ getFieldValue }: GetFieldValue) =>
|
||||
isOperatorNone(getFieldValue('operator')) ? (
|
||||
return (
|
||||
<FormItem
|
||||
name="operator"
|
||||
label={t('Operator')}
|
||||
rules={rulesRequired}
|
||||
initialValue={options[0].value}
|
||||
>
|
||||
<Select
|
||||
ariaLabel={t('Operator')}
|
||||
options={showOnlyNone ? [options[0]] : options}
|
||||
/>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOperatorFields = (
|
||||
{ getFieldValue }: GetFieldValue,
|
||||
columnType?: GenericDataType,
|
||||
) => {
|
||||
const columnTypeString = columnType === GenericDataType.String;
|
||||
const operatorColSpan = columnTypeString ? 8 : 6;
|
||||
const valueColSpan = columnTypeString ? 16 : 18;
|
||||
|
||||
return isOperatorNone(getFieldValue('operator')) ? (
|
||||
<Row gutter={12}>
|
||||
<Col span={6}>{renderOperator()}</Col>
|
||||
<Col span={operatorColSpan}>{renderOperator({ columnType })}</Col>
|
||||
</Row>
|
||||
) : isOperatorMultiValue(getFieldValue('operator')) ? (
|
||||
<Row gutter={12}>
|
||||
@@ -165,7 +202,7 @@ const renderOperatorFields = ({ getFieldValue }: GetFieldValue) =>
|
||||
<FullWidthInputNumber />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span={6}>{renderOperator()}</Col>
|
||||
<Col span={6}>{renderOperator({ columnType })}</Col>
|
||||
<Col span={9}>
|
||||
<FormItem
|
||||
name="targetValueRight"
|
||||
@@ -181,18 +218,19 @@ const renderOperatorFields = ({ getFieldValue }: GetFieldValue) =>
|
||||
</Row>
|
||||
) : (
|
||||
<Row gutter={12}>
|
||||
<Col span={6}>{renderOperator()}</Col>
|
||||
<Col span={18}>
|
||||
<Col span={operatorColSpan}>{renderOperator({ columnType })}</Col>
|
||||
<Col span={valueColSpan}>
|
||||
<FormItem
|
||||
name="targetValue"
|
||||
label={t('Target value')}
|
||||
rules={rulesRequired}
|
||||
>
|
||||
<FullWidthInputNumber />
|
||||
{columnTypeString ? <FullWidthInput /> : <FullWidthInputNumber />}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormattingPopoverContent = ({
|
||||
config,
|
||||
@@ -202,10 +240,11 @@ export const FormattingPopoverContent = ({
|
||||
}: {
|
||||
config?: ConditionalFormattingConfig;
|
||||
onChange: (config: ConditionalFormattingConfig) => void;
|
||||
columns: { label: string; value: string }[];
|
||||
columns: { label: string; value: string; dataType: GenericDataType }[];
|
||||
extraColorChoices?: { label: string; value: string }[];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [form] = Form.useForm();
|
||||
const colorScheme = colorSchemeOptions(theme);
|
||||
const [showOperatorFields, setShowOperatorFields] = useState(
|
||||
config === undefined ||
|
||||
@@ -218,8 +257,45 @@ export const FormattingPopoverContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const [column, setColumn] = useState<string>(
|
||||
config?.column || columns[0]?.value,
|
||||
);
|
||||
const [previousColumnType, setPreviousColumnType] = useState<
|
||||
GenericDataType | undefined
|
||||
>();
|
||||
|
||||
const columnType = useMemo(
|
||||
() => columns.find(item => item.value === column)?.dataType,
|
||||
[columns, column],
|
||||
);
|
||||
|
||||
const handleColumnChange = (value: string) => {
|
||||
const newColumnType = columns.find(item => item.value === value)?.dataType;
|
||||
if (newColumnType !== previousColumnType) {
|
||||
const defaultOperator =
|
||||
newColumnType === GenericDataType.String
|
||||
? stringOperatorOptions[0].value
|
||||
: operatorOptions[0].value;
|
||||
|
||||
form.setFieldsValue({
|
||||
operator: defaultOperator,
|
||||
});
|
||||
}
|
||||
setColumn(value);
|
||||
setPreviousColumnType(newColumnType);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (column && !previousColumnType) {
|
||||
setPreviousColumnType(
|
||||
columns.find(item => item.value === column)?.dataType,
|
||||
);
|
||||
}
|
||||
}, [column, columns, previousColumnType]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onChange}
|
||||
initialValues={config}
|
||||
requiredMark="optional"
|
||||
@@ -233,7 +309,13 @@ export const FormattingPopoverContent = ({
|
||||
rules={rulesRequired}
|
||||
initialValue={columns[0]?.value}
|
||||
>
|
||||
<Select ariaLabel={t('Select column')} options={columns} />
|
||||
<Select
|
||||
ariaLabel={t('Select column')}
|
||||
options={columns}
|
||||
onChange={value => {
|
||||
handleColumnChange(value as string);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -253,10 +335,12 @@ export const FormattingPopoverContent = ({
|
||||
</Row>
|
||||
<FormItem noStyle shouldUpdate={shouldFormItemUpdate}>
|
||||
{showOperatorFields ? (
|
||||
renderOperatorFields
|
||||
(props: GetFieldValue) => renderOperatorFields(props, columnType)
|
||||
) : (
|
||||
<Row gutter={12}>
|
||||
<Col span={6}>{renderOperator({ showOnlyNone: true })}</Col>
|
||||
<Col span={6}>
|
||||
{renderOperator({ showOnlyNone: true, columnType })}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { PopoverProps } from '@superset-ui/core/components/Popover';
|
||||
import { Comparator, ControlComponentProps } from '@superset-ui/chart-controls';
|
||||
import { GenericDataType } from '@superset-ui/core';
|
||||
|
||||
export type ConditionalFormattingConfig = {
|
||||
operator?: Comparator;
|
||||
@@ -33,7 +34,7 @@ export type ConditionalFormattingConfig = {
|
||||
export type ConditionalFormattingControlProps = ControlComponentProps<
|
||||
ConditionalFormattingConfig[]
|
||||
> & {
|
||||
columnOptions: { label: string; value: string }[];
|
||||
columnOptions: { label: string; value: string; dataType: GenericDataType }[];
|
||||
removeIrrelevantConditions: boolean;
|
||||
verboseMap: Record<string, string>;
|
||||
label: string;
|
||||
@@ -42,7 +43,7 @@ export type ConditionalFormattingControlProps = ControlComponentProps<
|
||||
};
|
||||
|
||||
export type FormattingPopoverProps = PopoverProps & {
|
||||
columns: { label: string; value: string }[];
|
||||
columns: { label: string; value: string; dataType: GenericDataType }[];
|
||||
onChange: (value: ConditionalFormattingConfig) => void;
|
||||
config?: ConditionalFormattingConfig;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user