diff --git a/src/components/ColorPickerInput.tsx b/src/components/ColorPickerInput.tsx new file mode 100644 index 000000000..89ebdb212 --- /dev/null +++ b/src/components/ColorPickerInput.tsx @@ -0,0 +1,76 @@ +import { useState, useRef, MutableRefObject, useLayoutEffect } from "react"; +import { Box, useTheme } from "@mui/material"; + +import { Color, ColorPicker } from "react-color-palette"; + +const useResponsiveWidth = (): [ + width: number, + setRef: MutableRefObject +] => { + const ref = useRef(null); + const [width, setWidth] = useState(0); + + useLayoutEffect(() => { + if (!ref || !ref.current) { + return; + } + const resizeObserver = new ResizeObserver((targets) => { + const { width: currentWidth } = targets[0].contentRect; + setWidth(currentWidth); + }); + + resizeObserver.observe(ref.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return [width, ref]; +}; + +export interface IColorPickerProps { + value: Color; + onChangeComplete: (color: Color) => void; + disabled?: boolean; +} + +export default function ColorPickerInput({ + value, + onChangeComplete, + disabled = false, +}: IColorPickerProps) { + const [localValue, setLocalValue] = useState(value); + const [width, setRef] = useResponsiveWidth(); + const theme = useTheme(); + + return ( + + setLocalValue(color)} + onChangeComplete={onChangeComplete} + /> + + ); +} diff --git a/src/components/fields/Percentage/BasicCell.tsx b/src/components/fields/Percentage/BasicCell.tsx index 05fcfac69..c2dbef58e 100644 --- a/src/components/fields/Percentage/BasicCell.tsx +++ b/src/components/fields/Percentage/BasicCell.tsx @@ -1,41 +1,21 @@ import { IBasicCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; -import { resultColorsScale } from "@src/utils/color"; export default function Percentage({ value }: IBasicCellProps) { const theme = useTheme(); - if (typeof value === "number") - return ( - <> -
-
- {Math.round(value * 100)}% -
- - ); - - return null; + const percentage = typeof value === "number" ? value : 0; + return ( +
+ {Math.round(percentage * 100)}% +
+ ); } diff --git a/src/components/fields/Percentage/Settings.tsx b/src/components/fields/Percentage/Settings.tsx new file mode 100644 index 000000000..e28dde2cc --- /dev/null +++ b/src/components/fields/Percentage/Settings.tsx @@ -0,0 +1,171 @@ +import { useState } from "react"; + +import { + Box, + Checkbox, + Grid, + InputLabel, + MenuItem, + TextField, + Typography, + useTheme, +} from "@mui/material"; +import ColorPickerInput from "@src/components/ColorPickerInput"; +import { ISettingsProps } from "@src/components/fields/types"; + +import { Color, toColor } from "react-color-palette"; +import { fieldSx } from "@src/components/SideDrawer/utils"; +import { resultColorsScale, defaultColors } from "@src/utils/color"; + +const colorLabels: { [key: string]: string } = { + 0: "Start", + 1: "Middle", + 2: "End", +}; + +export default function Settings({ onChange, config }: ISettingsProps) { + const colors: string[] = config.colors ?? defaultColors; + + const [checkStates, setCheckStates] = useState( + colors.map(Boolean) + ); + + const onCheckboxChange = (index: number, checked: boolean) => { + onChange("colors")( + colors.map((value: any, idx: number) => + index === idx ? (checked ? value || defaultColors[idx] : null) : value + ) + ); + setCheckStates( + checkStates.map((value, idx) => (index === idx ? checked : value)) + ); + }; + + const handleColorChange = (index: number, color: Color): void => { + onChange("colors")( + colors.map((value, idx) => (index === idx ? color.hex : value)) + ); + }; + + return ( + <> + + {checkStates.map((checked: boolean, index: number) => { + const colorHex = colors[index]; + return ( + + onCheckboxChange(index, !checked)} + /> + + + {checked && ( + + + `0 0 0 1px ${theme.palette.divider} inset`, + borderRadius: 0.5, + opacity: 0.5, + }} + /> + {colorHex} + + )} + + {colorHex && ( +
+ + handleColorChange(index, color) + } + disabled={!checkStates[index]} + /> +
+ )} +
+
+ ); + })} +
+ + + ); +} + +const Preview = ({ colors }: { colors: any }) => { + const theme = useTheme(); + return ( + + Preview: + + {[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1].map((value) => { + return ( + + + + {Math.floor(value * 100)}% + + + ); + })} + + + ); +}; diff --git a/src/components/fields/Percentage/SideDrawerField.tsx b/src/components/fields/Percentage/SideDrawerField.tsx index ea4e0681b..a6cc43662 100644 --- a/src/components/fields/Percentage/SideDrawerField.tsx +++ b/src/components/fields/Percentage/SideDrawerField.tsx @@ -1,7 +1,6 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; -import { TextField, InputAdornment, Box } from "@mui/material"; -import { emphasize } from "@mui/material/styles"; +import { TextField, InputAdornment, Box, useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; import { getFieldId } from "@src/components/SideDrawer/utils"; @@ -12,6 +11,8 @@ export default function Percentage({ onSubmit, disabled, }: ISideDrawerFieldProps) { + const { colors } = (column as any).config; + const theme = useTheme(); return ( - `0 0 0 1px ${theme.palette.divider} inest`, + boxShadow: `0 0 0 1px ${theme.palette.divider} inset`, backgroundColor: typeof value === "number" - ? resultColorsScale(value).toHex() + "!important" + ? resultColorsScale( + value, + colors, + theme.palette.background.paper + ).toHex() + "!important" : undefined, }} /> diff --git a/src/components/fields/Percentage/TableCell.tsx b/src/components/fields/Percentage/TableCell.tsx new file mode 100644 index 000000000..29174a398 --- /dev/null +++ b/src/components/fields/Percentage/TableCell.tsx @@ -0,0 +1,41 @@ +import { IHeavyCellProps } from "@src/components/fields/types"; + +import { useTheme } from "@mui/material"; +import { resultColorsScale } from "@src/utils/color"; + +export default function Percentage({ column, value }: IHeavyCellProps) { + const theme = useTheme(); + const { colors } = (column as any).config; + + const percentage = typeof value === "number" ? value : 0; + return ( + <> +
+
+ {Math.round(percentage * 100)}% +
+ + ); +} diff --git a/src/components/fields/Percentage/index.tsx b/src/components/fields/Percentage/index.tsx index c44690e32..f353774fc 100644 --- a/src/components/fields/Percentage/index.tsx +++ b/src/components/fields/Percentage/index.tsx @@ -1,12 +1,20 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell"; +import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; import { Percentage as PercentageIcon } from "@src/assets/icons"; -import BasicCell from "./BasicCell"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "@src/components/fields/Number/Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; + +const BasicCell = lazy( + () => import("./BasicCell" /* webpackChunkName: "BasicCell-Percentage" */) +); + +const TableCell = lazy( + () => import("./TableCell" /* webpackChunkName: "TableCell-Percentage" */) +); + const SideDrawerField = lazy( () => import( @@ -14,6 +22,10 @@ const SideDrawerField = lazy( ) ); +const Settings = lazy( + () => import("./Settings" /* webpackChunkName: "Settings-Percentage" */) +); + export const config: IFieldConfig = { type: FieldType.percentage, name: "Percentage", @@ -22,11 +34,13 @@ export const config: IFieldConfig = { initialValue: 0, initializable: true, icon: , + requireConfiguration: true, description: "Percentage stored as a number between 0 and 1.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), + TableCell: withHeavyCell(BasicCell, TableCell), TableEditor: TextEditor, SideDrawerField, + settings: Settings, filter: { operators: filterOperators, }, diff --git a/src/utils/color.ts b/src/utils/color.ts index 58dbfa53e..686e27160 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -1,12 +1,18 @@ import { colord } from "colord"; -export const resultColors = { - No: "#ED4747", - Maybe: "#f3c900", - Yes: "#1fad5f", -}; +export const defaultColors = ["#ED4747", "#F3C900", "#1FAD5F"]; -export const resultColorsScale = (value: number) => +export const resultColorsScale = ( + value: number, + colors: any = defaultColors, + defaultColor: string = "#fff" +) => value <= 0.5 - ? colord(resultColors.No).mix(resultColors.Maybe, value * 2) - : colord(resultColors.Maybe).mix(resultColors.Yes, (value - 0.5) * 2); + ? colord(colors[0] || defaultColor).mix( + colors[1] || defaultColor, + value * 2 + ) + : colord(colors[1] || defaultColor).mix( + colors[2] || defaultColor, + (value - 0.5) * 2 + );