UNPKG

@adaptabletools/adaptable

Version:

Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

340 lines (339 loc) 22 kB
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); } }