From 1a696b8e64778a22b5402d440621d5b66407936f Mon Sep 17 00:00:00 2001 From: Alexandre Rousseau Date: Sat, 28 Sep 2024 21:16:17 +0200 Subject: [PATCH 1/2] feat(ui): Implement range selector. WF-4 --- .../public/components/rangeinput.png | Bin 0 -> 9021 bytes .../core/base/BaseInputSlider.utils.ts | 14 +++ .../components/core/base/BaseInputSlider.vue | 64 ++++++++++ .../core/base/BaseInputSliderLayout.vue | 60 +++++++++ .../core/base/BaseInputSliderRange.vue | 102 +++++++++++++++ ...nputRange.vue => BaseInputSliderThumb.vue} | 113 +++++------------ .../components/core/input/CoreSliderInput.vue | 4 +- .../core/input/CoreSliderRangeInput.vue | 117 ++++++++++++++++++ .../src/composables/useBoundingClientRect.ts | 18 +++ src/ui/src/core/templateMap.ts | 2 + src/writer/core.py | 21 ++++ tests/backend/test_core.py | 17 +++ tests/e2e/tests/components.spec.ts | 1 + 13 files changed, 449 insertions(+), 84 deletions(-) create mode 100644 docs/framework/public/components/rangeinput.png create mode 100644 src/ui/src/components/core/base/BaseInputSlider.utils.ts create mode 100644 src/ui/src/components/core/base/BaseInputSlider.vue create mode 100644 src/ui/src/components/core/base/BaseInputSliderLayout.vue create mode 100644 src/ui/src/components/core/base/BaseInputSliderRange.vue rename src/ui/src/components/core/base/{BaseInputRange.vue => BaseInputSliderThumb.vue} (63%) create mode 100644 src/ui/src/components/core/input/CoreSliderRangeInput.vue create mode 100644 src/ui/src/composables/useBoundingClientRect.ts diff --git a/docs/framework/public/components/rangeinput.png b/docs/framework/public/components/rangeinput.png new file mode 100644 index 0000000000000000000000000000000000000000..22d790efb9fdb6cd3ede87e77769c199ca459664 GIT binary patch literal 9021 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, + step: Ref, +) { + const precision = computed( + () => String(step.value).split(".")[1]?.length ?? 0, + ); + return computed(() => Number(value.value).toFixed(precision.value)); +} diff --git a/src/ui/src/components/core/base/BaseInputSlider.vue b/src/ui/src/components/core/base/BaseInputSlider.vue new file mode 100644 index 000000000..2cc029a99 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSlider.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/ui/src/components/core/base/BaseInputSliderLayout.vue b/src/ui/src/components/core/base/BaseInputSliderLayout.vue new file mode 100644 index 000000000..3069a7979 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSliderLayout.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/ui/src/components/core/base/BaseInputSliderRange.vue b/src/ui/src/components/core/base/BaseInputSliderRange.vue new file mode 100644 index 000000000..73c68fad7 --- /dev/null +++ b/src/ui/src/components/core/base/BaseInputSliderRange.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/ui/src/components/core/base/BaseInputRange.vue b/src/ui/src/components/core/base/BaseInputSliderThumb.vue similarity index 63% rename from src/ui/src/components/core/base/BaseInputRange.vue rename to src/ui/src/components/core/base/BaseInputSliderThumb.vue index c23db79e6..f63ee1e59 100644 --- a/src/ui/src/components/core/base/BaseInputRange.vue +++ b/src/ui/src/components/core/base/BaseInputSliderThumb.vue @@ -1,75 +1,52 @@ - - diff --git a/src/ui/src/composables/useBoundingClientRect.ts b/src/ui/src/composables/useBoundingClientRect.ts new file mode 100644 index 000000000..f688f7aa1 --- /dev/null +++ b/src/ui/src/composables/useBoundingClientRect.ts @@ -0,0 +1,18 @@ +import { Ref, ref, onUnmounted, computed } from "vue"; + +/** + * Watch the bounding client rect of an element using `setInterval` + */ +export function useBoundingClientRect(htmlRef: Ref, ms = 500) { + const rect = ref(); + + const id = setInterval(() => { + rect.value = htmlRef.value?.getBoundingClientRect(); + }, ms); + + onUnmounted(() => { + clearInterval(id); + }); + + return computed(() => rect.value); +} diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts index 22c3d281f..b05dd014a 100644 --- a/src/ui/src/core/templateMap.ts +++ b/src/ui/src/core/templateMap.ts @@ -28,6 +28,7 @@ import CoreNumberInput from "../components/core/input/CoreNumberInput.vue"; import CoreRadioInput from "../components/core/input/CoreRadioInput.vue"; import CoreSelectInput from "../components/core/input/CoreSelectInput.vue"; import CoreSliderInput from "../components/core/input/CoreSliderInput.vue"; +import CoreSliderRangeInput from "../components/core/input/CoreSliderRangeInput.vue"; import CoreTextInput from "../components/core/input/CoreTextInput.vue"; import CoreTextareaInput from "../components/core/input/CoreTextareaInput.vue"; import CoreTimeInput from "../components/core/input/CoreTimeInput.vue"; @@ -102,6 +103,7 @@ const templateMap = { textareainput: CoreTextareaInput, numberinput: CoreNumberInput, sliderinput: CoreSliderInput, + rangeinput: CoreSliderRangeInput, colorinput: CoreColorInput, dateinput: CoreDateInput, timeinput: CoreTimeInput, diff --git a/src/writer/core.py b/src/writer/core.py index 0fa7d5668..1387771f2 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -11,6 +11,7 @@ import logging import math import multiprocessing +import numbers import re import secrets import time @@ -1388,6 +1389,26 @@ def _transform_time_change(self, ev) -> str: return payload + def _transform_range_change(self, ev) -> list[int]: + payload = ev.payload + + if not isinstance(payload, list): + raise ValueError("Range must be an array.") + + if len(payload) != 2: + raise ValueError("Range must contains exactly two values.") + + if not isinstance(payload[0], numbers.Real): + raise ValueError("First item is not a number.") + + if not isinstance(payload[1], numbers.Real): + raise ValueError("Second item is not a number.") + + if payload[0] > payload[1]: + raise ValueError("First item is higher than second.") + + return payload + def _transform_change_page_size(self, ev) -> Optional[int]: try: return int(ev.payload) diff --git a/tests/backend/test_core.py b/tests/backend/test_core.py index bbbf262eb..da5221d13 100644 --- a/tests/backend/test_core.py +++ b/tests/backend/test_core.py @@ -875,6 +875,23 @@ def test_date_change(self) -> None: self.ed.transform(ev_valid) assert ev_valid.payload == "2019-11-23" + def test_range_change(self) -> None: + ev_invalid = WriterEvent( + type="wf-range-change", + instancePath=self.root_instance_path, + payload="virus" + ) + with pytest.raises(RuntimeError): + self.ed.transform(ev_invalid) + + ev_valid = WriterEvent( + type="wf-range-change", + instancePath=self.root_instance_path, + payload=[10,42] + ) + self.ed.transform(ev_valid) + assert ev_valid.payload == [10, 42] + def test_time_change(self) -> None: ev_invalid = WriterEvent( type="wf-time-change", diff --git a/tests/e2e/tests/components.spec.ts b/tests/e2e/tests/components.spec.ts index 8abfea5cd..f4f315e5d 100644 --- a/tests/e2e/tests/components.spec.ts +++ b/tests/e2e/tests/components.spec.ts @@ -29,6 +29,7 @@ const mapComponents = { dateinput: {locator: '.component.wf-type-dateinput label'}, timeinput: {locator: '.component.wf-type-timeinput label'}, sliderinput: {locator: '.component.wf-type-sliderinput label'}, + rangeinput: {locator: '.component.wf-type-rangeinput label'}, numberinput: {locator: '.component.wf-type-numberinput label'}, textinput: {locator: '.component.wf-type-textinput label'}, } From f864eedf38118c5a4623f377ed168b13fea044aa Mon Sep 17 00:00:00 2001 From: Alexandre Rousseau Date: Thu, 17 Oct 2024 14:46:19 +0200 Subject: [PATCH 2/2] fix(ui): update slider value if `min/max` change. WF-4 --- .../components/core/base/BaseInputSlider.vue | 28 ++++++++++++++++++- .../core/base/BaseInputSliderRange.vue | 20 ++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ui/src/components/core/base/BaseInputSlider.vue b/src/ui/src/components/core/base/BaseInputSlider.vue index 2cc029a99..bd6d16d29 100644 --- a/src/ui/src/components/core/base/BaseInputSlider.vue +++ b/src/ui/src/components/core/base/BaseInputSlider.vue @@ -30,7 +30,15 @@ diff --git a/src/ui/src/components/core/base/BaseInputSliderRange.vue b/src/ui/src/components/core/base/BaseInputSliderRange.vue index 73c68fad7..738787c95 100644 --- a/src/ui/src/components/core/base/BaseInputSliderRange.vue +++ b/src/ui/src/components/core/base/BaseInputSliderRange.vue @@ -35,7 +35,7 @@