Skip to content
This repository has been archived by the owner on Feb 15, 2024. It is now read-only.

Commit

Permalink
Add the range selection capability to the slider element (#905)
Browse files Browse the repository at this point in the history
* Add support for array of numbers in slider
* Support for range of lov values
* Add minimal tests on range
* Picked from PR#906 to fix tests
* Make linters happy
  • Loading branch information
FabienLelaquais authored Sep 7, 2023
1 parent ea0552e commit d9f1bcf
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 89 deletions.
12 changes: 6 additions & 6 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ name = "pypi"
[packages]
"backports.zoneinfo" = {version="==0.2.1", markers="python_version < '3.9'", extras=["tzdata"]}
flask = "==2.2.5"
flask-cors = "==3.0.10"
flask-socketio = "==5.3.0"
gevent = "==22.10.2"
flask-cors = "==4.0.0"
flask-socketio = "==5.3.6"
gevent = "==23.7.0"
gevent-websocket = "==0.10.1"
kthread = "==0.2.3"
markdown = "==3.4.1"
markdown = "==3.4.4"
pandas = "==2.0.0"
pyarrow = "==10.0.1"
pyngrok = "==5.1"
python-dotenv = "==0.19"
python-magic = {version = "==0.4.24", markers="sys_platform != 'win32'"}
python-magic-bin = {version = "==0.4.14", markers="sys_platform == 'win32'"}
pytz = "==2021.3"
simple-websocket = "==0.9"
simple-websocket = "==0.10.1"
taipy-config = {ref = "develop", git = "https://github.com/avaiga/taipy-config.git"}
tzlocal = "==3.0"
gitignore-parser = "==0.1.1"
twisted = "==22.10.0"
twisted = "==23.8.0"

[dev-packages]
black = "*"
Expand Down
6 changes: 3 additions & 3 deletions gui/dom/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gui/packaging/taipy-gui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export interface TaipyTableProps extends TaipyActiveProps, TaipyMultiSelectProps
nanValue?: string;
filter?: boolean;
size?: "small" | "medium";
userData?: string;
userData?: unknown;
}
export interface TaipyPaginatedTableProps extends TaipyTableProps {
pageSizeOptions?: string;
Expand Down
14 changes: 14 additions & 0 deletions gui/src/components/Taipy/Slider.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ describe("Slider Component", () => {
type: "SEND_UPDATE_ACTION",
});
});
it("holds a numeric range", async () => {
const { getByDisplayValue } = render(<Slider defaultValue={"[10,90]"} value={undefined as unknown as number[]} />);
const elt1 = getByDisplayValue("10");
expect(elt1.tagName).toBe("INPUT");
const elt2 = getByDisplayValue("90");
expect(elt2.tagName).toBe("INPUT");
});
it("shows discrete value when lov", async () => {
const { getAllByText } = render(
<Slider
Expand All @@ -107,6 +114,13 @@ describe("Slider Component", () => {
expect(elts).toHaveLength(1);
expect(elts[0].tagName).toBe("P");
});
it("holds a lov range", async () => {
const { getByDisplayValue } = render(<Slider value={["B", "C"]} defaultLov={'[["A", "A"], ["B", "B"], ["C", "C"], ["D", "D"]]'} />);
const elt1 = getByDisplayValue("1");
expect(elt1.tagName).toBe("INPUT");
const elt2 = getByDisplayValue("2");
expect(elt2.tagName).toBe("INPUT");
});
it("shows marks", async () => {
const { getAllByText } = render(
<Slider
Expand Down
184 changes: 122 additions & 62 deletions gui/src/components/Taipy/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { getCssSize, getUpdateVar } from "./utils";
import { Icon } from "../../utils/icon";
import { SyntheticEvent } from "react";

interface SliderProps extends LovProps<number | string, number | string> {
interface SliderProps extends LovProps<number | string | number[] | string[], number | string | number[] | string[]> {
width?: string;
height?: string;
min?: number;
Expand All @@ -37,6 +37,8 @@ interface SliderProps extends LovProps<number | string, number | string> {
changeDelay?: number;
}

const emptyString = () => ""

const Slider = (props: SliderProps) => {
const {
id,
Expand All @@ -50,10 +52,10 @@ const Slider = (props: SliderProps) => {
updateVars = "",
valueById,
} = props;
const [value, setValue] = useState(0);
const [value, setValue] = useState<number|number[]>(0);
const dispatch = useDispatch();
const delayCall = useRef(-1);
const lastVal = useRef<string|number>(0);
const lastVal = useRef<number|string|number[]|string[]>(0);
const module = useModule();

const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
Expand All @@ -68,11 +70,24 @@ const Slider = (props: SliderProps) => {
const max = lovList.length ? lovList.length - 1 : props.max;
const horizontalOrientation = props.orientation ? props.orientation.charAt(0).toLowerCase() !== "v" : true;

const handleRange = useCallback(
// Converts the slider value (number or array of numbers) to a proper backend value
// depending on whether we're dealing with a lov or not
const convertValue = useCallback(
(v: number | number[]): number | string | number[] | string[] =>
Array.isArray(v)
? (lovList.length
? v.map((i) => lovList.length > (i as number) ? lovList[i as number].id : lovList[0].id)
: v)
: (lovList.length && lovList.length > (v as number) ? lovList[v as number].id : v),
[lovList])

const handleChange = useCallback(
(e: Event, val: number | number[]) => {
setValue(val as number);
setValue(val);
if (update) {
lastVal.current = lovList.length && lovList.length > (val as number) ? lovList[val as number].id : val as number;
lastVal.current = convertValue(val)
// Similar invocations of createSendUpdateAction(), but they happen at different
// points in time.
if (changeDelay) {
if (delayCall.current < 0) {
delayCall.current = window.setTimeout(() => {
Expand All @@ -86,31 +101,23 @@ const Slider = (props: SliderProps) => {
delayCall.current = 0;
}
},
[lovList, update, updateVarName, dispatch, propagate, updateVars, valueById, props.onChange, changeDelay, module]
[update, updateVarName, dispatch, propagate, updateVars, valueById, props.onChange, changeDelay, module, convertValue]
);

const handleRangeCommitted = useCallback(
const handleChangeCommitted = useCallback(
(e: Event | SyntheticEvent, val: number | number[]) => {
setValue(val as number);
setValue(val);
if (!update) {
const value = lovList.length && lovList.length > (val as number) ? lovList[val as number].id : val;
dispatch(
createSendUpdateAction(
updateVarName,
value,
module,
props.onChange,
propagate,
valueById ? undefined : getUpdateVar(updateVars, "lov")
)
);
const converted_value = convertValue(val)
dispatch(createSendUpdateAction(updateVarName, converted_value, module, props.onChange, propagate, valueById ? undefined : getUpdateVar(updateVars, "lov")))
}
},
[lovList, update, updateVarName, dispatch, propagate, updateVars, valueById, props.onChange, module]
[update, updateVarName, dispatch, propagate, updateVars, valueById, props.onChange, module, convertValue]
);

const getLabel = useCallback(
(value: number) =>
(value: number | number[] | null) =>
value === null || Array.isArray(value) ? null :
lovList.length && lovList.length > value ? (
typeof lovList[value].item === "string" ? (
<Typography>{lovList[value].item as string}</Typography>
Expand All @@ -124,7 +131,7 @@ const Slider = (props: SliderProps) => {
);

const getText = useCallback(
(value: number, before: boolean) => {
(value: number|number[], before: boolean) => {
if (lovList.length) {
if (before && (textAnchor === "top" || textAnchor === "left")) {
return getLabel(value);
Expand All @@ -138,6 +145,7 @@ const Slider = (props: SliderProps) => {
[lovList, textAnchor, getLabel]
);


const marks = useMemo(() => {
if (props.labels) {
if (typeof props.labels === "boolean") {
Expand Down Expand Up @@ -193,59 +201,111 @@ const Slider = (props: SliderProps) => {
return { ...sx, display: "inline-block" };
}, [lovList, horizontalOrientation, textAnchor, width, props.height]);

useEffect(() => {
if (props.value === undefined) {
let val = 0;
if (defaultValue !== undefined) {
if (typeof defaultValue === "string") {
if (lovList.length) {
try {
const arrVal = JSON.parse(defaultValue) as string[];
val = lovList.findIndex((item) => item.id === arrVal[0]);
val = val === -1 ? 0 : val;
} catch (e) {
// Too bad also
}
} else {
try {
val = parseInt(defaultValue, 10);
} catch (e) {
// too bad
// Parse the default value once and for all
const parsedDefaultValue = useMemo(() => {
if (defaultValue === undefined) {
return 0
}
if (typeof defaultValue === "string") {
if (lovList.length) {
try {
const arr = JSON.parse(defaultValue) as string[];
if (arr.length > 1) {
return arr.map((i) => lovList.findIndex((j) => j.id === i))
// Force unknown values to index 0
.map((v) => v === -1 ? 0 : v)
}
else {
const val = lovList.findIndex((item) => item.id === arr[0])
return val === -1 ? 0 : val
}
}
catch (e) {
throw new Error("Slider lov value couldn't be parsed");
}
}
else {
const val = Number(defaultValue)
if (isNaN(val)) {
try {
const arr = JSON.parse(defaultValue) as number[]
if (arr.some(isNaN)) {
throw new Error("Slider values should all be numbers")
}
return arr
} catch (e) {
// Invalid values
return 0
}
} else {
val = defaultValue as number;
}
else {
return val
}
}
setValue(val);
}
return defaultValue as number
}, [defaultValue, lovList])

useEffect(() => {
if (props.value === undefined) {
if (parsedDefaultValue !== undefined) {
setValue(parsedDefaultValue)
}
} else {
if (lovList.length) {
const val = lovList.findIndex((item) => item.id === props.value);
setValue(val === -1 ? 0 : val);
if (Array.isArray(props.value)) {
setValue(props.value.map((i) => lovList.findIndex((j) => j.id === i))
// Force unknown values to index 0
.map((v) => v === -1 ? 0 : v))
}
else {
const val = lovList.findIndex((item) => item.id === props.value);
setValue(val === -1 ? 0 : val);
}
} else {
setValue(props.value as number);
setValue(Array.isArray(props.value) ? props.value as number[] : props.value as number);
}
}
}, [props.value, lovList, defaultValue]);
}, [props.value, lovList, parsedDefaultValue, convertValue]);

return (
<Box sx={textAnchorSx} className={className}>
{getText(value, true)}
<Tooltip title={hover || ""}>
<MuiSlider
id={id}
value={value as number}
onChange={handleRange}
onChangeCommitted={handleRangeCommitted}
disabled={!active}
valueLabelDisplay="auto"
min={min}
max={max}
step={1}
marks={marks}
valueLabelFormat={getLabel}
orientation={horizontalOrientation ? undefined : "vertical"}
/>
{Array.isArray(parsedDefaultValue)
?
<MuiSlider
id={id}
value={value}
onChange={handleChange}
onChangeCommitted={handleChangeCommitted}
disabled={!active}
valueLabelDisplay="auto"
min={min}
max={max}
step={1}
marks={marks}
valueLabelFormat={getLabel}
orientation={horizontalOrientation ? undefined : "vertical"}
getAriaLabel={emptyString}
aria-label={undefined}
/>
:
<MuiSlider
id={id}
value={value ? value : 0}
onChange={handleChange}
onChangeCommitted={handleChangeCommitted}
disabled={!active}
valueLabelDisplay="auto"
min={min}
max={max}
step={1}
marks={marks}
valueLabelFormat={getLabel}
orientation={horizontalOrientation ? undefined : "vertical"}
/>
}
</Tooltip>
{getText(value, false)}
</Box>
Expand Down
2 changes: 1 addition & 1 deletion gui/src/components/Taipy/tableUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export interface TaipyTableProps extends TaipyActiveProps, TaipyMultiSelectProps
filter?: boolean;
size?: "small" | "medium";
defaultKey?: string; // for testing purposes only
userData?: string;
userData?: unknown;
}

export type PageSizeOptionsType = (
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ filterwarnings =
ignore::DeprecationWarning:twisted
ignore::DeprecationWarning:pandas
ignore::DeprecationWarning:numpy
ignore::FutureWarning:pyarrow
markers =
teste2e:End-to-end tests
Loading

0 comments on commit d9f1bcf

Please sign in to comment.