@adaptabletools/adaptable
Version:
Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
340 lines (339 loc) • 22 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import * as React from 'react';
import { ColorPicker } from '../../components/ColorPicker';
import { clamp } from '../../Utilities/Extensions/NumberExtensions';
import { CheckBox } from '../../components/CheckBox';
import FormLayout, { FormRow } from '../../components/FormLayout';
import { NumberInput } from '../../components/Input/NumberInput';
import SimpleButton from '../../components/SimpleButton';
import { ButtonNew } from './Buttons/ButtonNew';
import { getGraySwatchColor } from '../UIHelper';
import { ColumnSelector } from './Selectors/ColumnSelector';
import { Tabs } from '../../components/Tabs';
import Radio from '../../components/Radio';
import { Box, Flex } from '../../components/Flex';
import { SingleSelect } from '../../components/NewSelect';
import { InputGroup } from '../../components/InputGroup';
import { Icon } from '../../components/icons';
import HelpBlock from '../../components/HelpBlock';
const DEFAULT_ZERO_CENTRED_COLORS = {
NegativeColor: 'rgba(220, 53, 69, 0.55)',
PositiveColor: 'rgba(40, 167, 69, 0.55)',
};
const hasConfiguredRanges = (ranges) => (ranges?.length ?? 0) > 0;
function deriveInitialRangesType(props) {
if (props.columnComparison) {
return 'ColumnComparison';
}
if (props.showZeroCentredTab && props.zeroCentred) {
return 'ZeroCentred';
}
if (props.allowEmptyRanges && !hasConfiguredRanges(props.ranges)) {
return 'None';
}
return props.rangeValueType === 'Percentage' ? 'PercentageRange' : 'NumberRange';
}
const ColumnComparisonBoundInput = ({ value, disabled, onChange }) => {
const [type, setType] = React.useState(value != null && value !== '' && isNaN(Number(value)) ? 'column' : 'value');
const typeOptions = [
{
value: 'value',
label: (_jsxs(Flex, { alignItems: "center", children: [_jsx(Icon, { name: "edit" }), _jsx(Box, { className: "twa:ml-2", children: "Value" })] })),
},
{
value: 'column',
label: (_jsxs(Flex, { alignItems: "center", children: [_jsx(Icon, { name: "columns" }), _jsx(Box, { className: "twa:ml-2", children: "Column" })] })),
},
];
const editor = type === 'value' ? (_jsx(NumberInput, { className: "twa:w-full", disabled: disabled, type: "number", value: value != null && !isNaN(Number(value)) ? Number(value) : '', onChange: (v) => onChange(v) })) : (_jsx(ColumnSelector, { disabled: disabled, type: "number", value: value != null && isNaN(Number(value)) ? String(value) : undefined, onChange: (columnId) => onChange(columnId || undefined) }));
const editorWrapperClassName = [
'twa:flex-1 twa:basis-0 twa:min-w-0 twa:-ml-px',
'twa:[&_.ab-Input]:w-full twa:[&_.ab-Input]:rounded-l-none!',
'twa:[&_[data-slot=input-group]]:w-full! twa:[&_[data-slot=input-group]]:rounded-l-none!',
].join(' ');
return (_jsxs(InputGroup, { Component: Flex, className: "twa:items-stretch twa:max-w-[20rem]", children: [_jsx(SingleSelect, { className: "twa:w-[120px] twa:max-w-none twa:shrink-0 twa:box-border twa:rounded-r-none!", disabled: disabled, value: type, items: typeOptions, onValueChange: (next) => {
onChange(undefined);
setType(next);
} }), _jsx(Box, { className: editorWrapperClassName, children: editor })] }));
};
export class RangesComponent extends React.Component {
constructor(props) {
super(props);
this.state = { rangesType: deriveInitialRangesType(props) };
}
render() {
let comparisonColor = this.props.columnComparison != null && this.props.columnComparison.Color != null
? this.props.columnComparison.Color
: undefined;
const isRangeValueTypeNumber = this.state.rangesType === 'NumberRange';
const renderNoneContent = () => (_jsx(Box, { className: "twa:text-2 twa:opacity-70 twa:py-2 twa:max-w-[520px]", children: "No coloured bands configured" }));
const rangeGridTemplateColumns = [
'max-content',
'max-content',
'max-content',
...(this.props.showRangeDirection ? ['max-content'] : []),
'max-content',
].join(' ');
const renderRangesContent = () => (_jsxs(_Fragment, { children: [isRangeValueTypeNumber ? (_jsxs(HelpBlock, { className: "twa:my-2 twa:text-3", children: [_jsx(Box, { children: "Define value bands from low to high, each with its own colour." }), _jsxs(Box, { children: ["Tick ", _jsx("strong", { children: "Use column min" }), " or ", _jsx("strong", { children: "Use column max" }), " to anchor the first and last band to the column's actual lowest and highest values."] })] })) : (_jsxs(HelpBlock, { className: "twa:my-2 twa:text-3", children: [_jsx(Box, { children: "Define bands as percentages (0\u2013100) of the column's value range." }), _jsxs(Box, { children: [_jsx("strong", { children: "0%" }), " is the column's lowest value and ", _jsx("strong", { children: "100%" }), " its highest; each band gets its own colour."] })] })), _jsx(Box, { className: "twa:grid twa:items-end twa:gap-x-2 twa:gap-y-3 twa:mb-3 twa:w-fit", style: { gridTemplateColumns: rangeGridTemplateColumns }, children: this.props.ranges?.map((range, index, list) => (_jsxs(Box, { className: "twa:contents", "data-name": "percent-bar-range", children: [_jsxs(Flex, { flexDirection: "column", children: [isRangeValueTypeNumber && index === 0 && (_jsx(CheckBox, { onClick: () => this.setRangeColMin(range), checked: range.Min === 'Col-Min', className: "twa:mt-0 twa:mb-1 twa:text-3", disabled: this.props.disabled, children: "Use column min" })), _jsx(NumberInput, { disabled: this.props.disabled ||
range.Min === 'Col-Min' ||
(index === 0 && !isRangeValueTypeNumber), value: range.Min === 'Col-Min' ? this.props.minMaxRangeValues?.min ?? '' : range.Min, onChange: (value) => {
this.changeRangeMin(index, value);
} })] }), _jsxs(Flex, { flexDirection: "column", children: [isRangeValueTypeNumber && index === list.length - 1 && (_jsx(CheckBox, { className: "twa:mt-0 twa:mb-1 twa:text-3", onClick: () => this.setRangeColMax(range), checked: range.Max === 'Col-Max', disabled: this.props.disabled, children: "Use column max" })), _jsx(NumberInput, { value: range.Max === 'Col-Max' ? this.props.minMaxRangeValues?.max ?? '' : range.Max, disabled: this.props.disabled ||
range.Max === 'Col-Max' ||
(index === list.length - 1 && !isRangeValueTypeNumber), onChange: (value) => {
this.changeRangeMax(index, value);
} })] }), _jsx(ColorPicker, { api: this.props.api, value: range.Color, onChange: (color) => {
this.changeRangeColor(index, color);
}, className: "twa:h-[33px] twa:pl-2" }), this.props.showRangeDirection && (_jsx(CheckBox, { disabled: this.props.disabled, checked: !!range.ReverseGradient, onChange: (checked) => this.changeRangeDirectionUp(index, checked), children: "Reverse Gradient" })), _jsx(SimpleButton, { icon: "delete", disabled: this.props.disabled ||
(!this.props.allowEmptyRanges &&
!!this.props.ranges &&
this.props.ranges.length === 1), onClick: () => this.removeRange(index) })] }, index))) }), _jsx("div", { children: _jsx(ButtonNew, { disabled: this.props.disabled, dataName: "add-range", onClick: () => this.addRange(), children: "Add Range" }) })] }));
const renderZeroCentredContent = () => {
const zc = this.props.zeroCentred ?? DEFAULT_ZERO_CENTRED_COLORS;
const apply = (next) => this.props.onApplyZeroCentred?.(next);
const minLabel = this.props.minMaxRangeValues != null ? this.props.minMaxRangeValues.min : 'column min';
const maxLabel = this.props.minMaxRangeValues != null ? this.props.minMaxRangeValues.max : 'column max';
return (_jsxs(Flex, { flexDirection: "column", "data-name": "zero-centred-ranges", children: [_jsxs(HelpBlock, { className: "twa:my-2 twa:text-3", children: [_jsx(Box, { children: "Colour cells on a diverging scale centred at zero." }), _jsx(Box, { children: "Negative values shade toward the column minimum, positive values toward the maximum \u2014 deeper as they move away from 0." })] }), _jsxs(FormLayout, { columns: ['label', 'colour'], sizes: ['200px', '1fr'], className: "twa:ml-2", children: [_jsx(FormRow, { label: `Negative (${minLabel} → 0):`, children: _jsx(Flex, { alignItems: "center", className: "twa:ml-2", children: _jsx(ColorPicker, { disabled: this.props.disabled, api: this.props.api, value: zc.NegativeColor, onChange: (color) => apply({ ...zc, NegativeColor: color }) }) }) }), _jsx(FormRow, { label: `Positive (0 → ${maxLabel}):`, children: _jsx(Flex, { alignItems: "center", className: "twa:ml-2", children: _jsx(ColorPicker, { disabled: this.props.disabled, api: this.props.api, value: zc.PositiveColor, onChange: (color) => apply({ ...zc, PositiveColor: color }) }) }) })] })] }));
};
const describeBound = (bound) => {
if (bound == null || bound === '') {
return _jsx("em", { children: "not set" });
}
if (!isNaN(Number(bound))) {
return (_jsxs(_Fragment, { children: [_jsx("strong", { children: Number(bound) }), " ", _jsx("span", { className: "twa:opacity-60", children: "(fixed)" })] }));
}
const friendlyName = this.props.api.columnApi.getFriendlyNameForColumnId(String(bound));
return (_jsxs(_Fragment, { children: [_jsx("strong", { children: friendlyName }), " ", _jsx("span", { className: "twa:opacity-60", children: "(per row)" })] }));
};
const renderColumnComparisonContent = () => (_jsxs(Flex, { flexDirection: "column", "data-name": "percent-bar-column-comparison", children: [_jsxs(HelpBlock, { className: "twa:my-2 twa:text-3", children: [_jsxs(Box, { children: ["Scale each cell's bar between a ", _jsx("strong", { children: "Minimum" }), " and a", ' ', _jsx("strong", { children: "Maximum" }), "."] }), _jsxs(Box, { children: ["Set each to a fixed ", _jsx("strong", { children: "Value" }), " (same for all rows) or a", ' ', _jsx("strong", { children: "Column" }), " (read from that column on each row)."] })] }), _jsxs(FormLayout, { columns: ['label', 'first'], sizes: ['auto', '1fr'], children: [_jsx(FormRow, { label: "Minimum:", first: _jsx(ColumnComparisonBoundInput, { disabled: this.props.disabled, value: this.props.columnComparison?.MinValue, onChange: (value) => this.setColumnComparisonMin(value) }) }), _jsx(FormRow, { label: "Maximum:", first: _jsx(ColumnComparisonBoundInput, { disabled: this.props.disabled, value: this.props.columnComparison?.MaxValue, onChange: (value) => this.setColumnComparisonMax(value) }) }), _jsx(FormRow, { label: `Bar ${this.props.api.internalApi.getCorrectEnglishVariant('Colour')}`, first: _jsx(ColorPicker, { disabled: this.props.disabled, api: this.props.api, value: comparisonColor, onChange: (color) => {
this.changeColumnComparisonColor(color);
} }) })] }), _jsxs(Box, { className: "twa:mt-2 twa:p-2 twa:text-3 twa:opacity-80", "data-name": "cc-summary", children: ["Each row's bar runs from ", describeBound(this.props.columnComparison?.MinValue), " to", ' ', describeBound(this.props.columnComparison?.MaxValue), "."] })] }, 'cc'));
const tabFlex = { flex: 1 };
const radioCls = 'twa:m-0 twa:align-baseline';
const renderTabRadio = (label, value) => (_jsx(Radio, { tabIndex: -1, className: radioCls, checked: this.state.rangesType === value, children: label }));
return (_jsxs(Tabs, { autoFocus: false, className: "twa:mt-2", value: this.state.rangesType, onValueChange: (v) => this.handleRangesModeTab(v), children: [this.props.allowEmptyRanges && (_jsx(Tabs.Tab, { value: "None", style: tabFlex, children: renderTabRadio('No Ranges', 'None') })), _jsx(Tabs.Tab, { value: "NumberRange", style: tabFlex, children: renderTabRadio('Number Range', 'NumberRange') }), _jsx(Tabs.Tab, { value: "PercentageRange", style: tabFlex, children: renderTabRadio('Percentage Range', 'PercentageRange') }), this.props.showZeroCentredTab && (_jsx(Tabs.Tab, { value: "ZeroCentred", style: tabFlex, children: renderTabRadio('Zero Centred Range', 'ZeroCentred') })), !this.props.hideColumnComparison && (_jsx(Tabs.Tab, { value: "ColumnComparison", style: tabFlex, children: renderTabRadio('Column Comparison', 'ColumnComparison') })), this.props.allowEmptyRanges && (_jsx(Tabs.Content, { value: "None", children: renderNoneContent() })), _jsx(Tabs.Content, { value: "NumberRange", children: renderRangesContent() }), _jsx(Tabs.Content, { value: "PercentageRange", children: renderRangesContent() }), this.props.showZeroCentredTab && (_jsx(Tabs.Content, { value: "ZeroCentred", children: renderZeroCentredContent() })), !this.props.hideColumnComparison && (_jsx(Tabs.Content, { value: "ColumnComparison", children: renderColumnComparisonContent() }))] }));
}
componentDidUpdate(prevProps) {
const wasComparison = !!prevProps.columnComparison;
const isComparison = !!this.props.columnComparison;
const wasZc = !!prevProps.zeroCentred;
const isZc = !!this.props.zeroCentred;
if (wasComparison !== isComparison) {
this.setState({ rangesType: deriveInitialRangesType(this.props) });
return;
}
if (!isComparison && wasZc !== isZc) {
this.setState({ rangesType: deriveInitialRangesType(this.props) });
return;
}
if ((this.state.rangesType === 'NumberRange' || this.state.rangesType === 'PercentageRange') &&
prevProps.rangeValueType !== this.props.rangeValueType) {
this.setState({
rangesType: this.props.rangeValueType === 'Percentage' ? 'PercentageRange' : 'NumberRange',
});
}
if (this.props.allowEmptyRanges) {
const hadRanges = hasConfiguredRanges(prevProps.ranges);
const hasRanges = hasConfiguredRanges(this.props.ranges);
if (!hadRanges && hasRanges && this.state.rangesType === 'None') {
this.setState({
rangesType: this.props.rangeValueType === 'Percentage' ? 'PercentageRange' : 'NumberRange',
});
}
else if (hadRanges &&
!hasRanges &&
this.state.rangesType !== 'ColumnComparison' &&
this.state.rangesType !== 'ZeroCentred') {
this.setState({ rangesType: 'None' });
}
}
}
handleRangesModeTab(value) {
if (value === this.state.rangesType) {
return;
}
this.setState({ rangesType: value });
if (value === 'None') {
this.props.updateRanges([]);
return;
}
if (value === 'ColumnComparison') {
const columnComparison = {
Color: getGraySwatchColor(),
MinValue: undefined,
MaxValue: undefined,
};
this.props.updateColumnComparison(columnComparison);
return;
}
if (value === 'ZeroCentred') {
this.props.onApplyZeroCentred?.(this.props.zeroCentred ?? DEFAULT_ZERO_CENTRED_COLORS);
return;
}
this.props.onRangeValueTypeChange(value === 'PercentageRange' ? 'Percentage' : 'Number');
}
changeRangeMin(index, value) {
const { ranges } = this.props;
let newMin = Number(value);
if (this.props.rangeValueType === 'Percentage') {
newMin = clamp(newMin, 0, 100);
}
ranges[index].Min = newMin;
if (ranges[index - 1]) {
ranges[index - 1].Max = newMin;
}
this.props.updateRanges(ranges);
}
changeRangeMax(index, value) {
const { ranges } = this.props;
let newMax = Number(value);
if (this.props.rangeValueType === 'Percentage') {
newMax = clamp(newMax, 0, 100);
}
ranges[index].Max = newMax;
if (ranges[index + 1]) {
ranges[index + 1].Min = newMax;
}
this.props.updateRanges(ranges);
}
changeRangeColor(index, value) {
const { ranges } = this.props;
ranges[index].Color = value;
this.props.updateRanges(ranges);
}
changeRangeDirectionUp(index, checked) {
const ranges = [...this.props.ranges];
ranges[index] = { ...ranges[index], ReverseGradient: checked };
this.props.updateRanges(ranges);
}
removeRange(index) {
let ranges = [...this.props.ranges];
ranges.splice(index, 1);
if (this.props.rangeValueType === 'Percentage' && ranges.length === 1) {
ranges[0] = {
...ranges[0],
Max: 100,
};
}
this.props.updateRanges(ranges);
}
setRangeColMin(range) {
const newRanges = this.props.ranges.map((rangeItem) => {
if (rangeItem === range) {
let newMin = rangeItem.Min === 'Col-Min' ? this.props.minMaxRangeValues?.min ?? 0 : 'Col-Min';
return {
...rangeItem,
Min: newMin,
};
}
return rangeItem;
});
this.props.updateRanges(newRanges);
}
setRangeColMax(range) {
const newRanges = this.props.ranges.map((rangeItem) => {
if (rangeItem === range) {
let newMax = rangeItem.Max === 'Col-Max' ? this.props.minMaxRangeValues.max : 'Col-Max';
return {
...rangeItem,
Max: newMax,
};
}
return rangeItem;
});
this.props.updateRanges(newRanges);
}
addRange() {
const existingRanges = this.props.ranges ?? [];
if (existingRanges.length === 0) {
const isPercentage = this.props.rangeValueType === 'Percentage';
const seedRanges = this.props.allowEmptyRanges
? [
{
Min: isPercentage ? 0 : 'Col-Min',
Max: isPercentage ? 100 : 'Col-Max',
Color: getGraySwatchColor(),
},
]
: (() => {
const min = isPercentage ? 0 : this.props.minMaxRangeValues?.min ?? 0;
const max = isPercentage ? 100 : this.props.minMaxRangeValues?.max ?? 100;
const mid = Math.round((min + max) / 2);
return [
{
Min: isPercentage ? 0 : 'Col-Min',
Max: mid,
Color: 'rgba(128, 128, 128, 0.25)',
},
{
Min: mid,
Max: isPercentage ? 100 : 'Col-Max',
Color: 'rgba(128, 128, 128, 0.55)',
},
];
})();
this.props.updateRanges(seedRanges);
return;
}
if (existingRanges.length === 1) {
const splitAtMidpoint = this.splitFullSpanRangeAtMidpoint(existingRanges[0]);
if (splitAtMidpoint) {
this.props.updateRanges(splitAtMidpoint);
return;
}
}
const lastRange = existingRanges[existingRanges.length - 1];
let previousRange = lastRange;
let Min = null;
if (lastRange.Max === 'Col-Max') {
Min = this.props.minMaxRangeValues?.max ?? 100;
previousRange = {
...previousRange,
Max: this.props.minMaxRangeValues?.max ?? 100,
};
}
else {
Min = lastRange.Max;
}
const newRange = {
Min,
Max: lastRange.Max,
Color: getGraySwatchColor(),
};
const newRanges = [...existingRanges.slice(0, -1), previousRange, newRange];
this.props.updateRanges(newRanges);
}
splitFullSpanRangeAtMidpoint(onlyRange) {
const isPercentage = this.props.rangeValueType === 'Percentage';
if (isPercentage) {
if (onlyRange.Min !== 0 || onlyRange.Max !== 100) {
return null;
}
return [
{ ...onlyRange, Max: 50 },
{ Min: 50, Max: 100, Color: getGraySwatchColor() },
];
}
if (onlyRange.Min !== 'Col-Min' || onlyRange.Max !== 'Col-Max') {
return null;
}
const min = this.props.minMaxRangeValues?.min ?? 0;
const max = this.props.minMaxRangeValues?.max ?? 100;
const mid = Math.round((min + max) / 2);
return [
{ ...onlyRange, Max: mid },
{ Min: mid, Max: 'Col-Max', Color: getGraySwatchColor() },
];
}
setColumnComparisonMin(value) {
this.props.updateColumnComparison({ ...this.props.columnComparison, MinValue: value });
}
setColumnComparisonMax(value) {
this.props.updateColumnComparison({ ...this.props.columnComparison, MaxValue: value });
}
changeColumnComparisonColor(value) {
const { columnComparison } = this.props;
columnComparison.Color = value;
this.props.updateColumnComparison(columnComparison);
}
}