From 2d2199a9a8209611561f2b09bdd7d6622e423af9 Mon Sep 17 00:00:00 2001 From: Hojjat Afsharan Date: Fri, 27 Sep 2024 11:22:14 +0200 Subject: [PATCH] MultipleChoiceFormSpec => DualListChoice/CheckboxListChoice CMK-19100 Change-Id: I11542f980361f656cd1fefea8e5e2584d5734d13 --- cmk/gui/form_specs/private/__init__.py | 3 + cmk/gui/form_specs/private/multiple_choice.py | 21 + cmk/gui/form_specs/vue/form_spec_visitor.py | 6 +- cmk/gui/form_specs/vue/shared_type_defs.py | 29 +- .../vue/visitors/multiple_choice.py | 39 +- .../vue/visitors/recomposers/__init__.py | 2 + .../visitors/recomposers/multiple_choice.py | 32 ++ packages/cmk-frontend-vue/package-lock.json | 162 ++++---- .../cmk-frontend-vue/src/assets/variables.css | 1 + .../src/form/components/FormEdit.vue | 6 +- .../src/form/components/FormReadonly.vue | 16 +- .../forms/FormCheckboxListChoice.vue | 58 +++ .../components/forms/FormDualListChoice.vue | 366 ++++++++++++++++++ .../components/forms/FormMultipleChoice.vue | 150 ------- .../components/vue_formspec_components.ts | 22 +- .../forms/FormCheckboxListChoice.test.ts | 45 +++ .../forms/FormDualListChoice.test.ts | 246 ++++++++++++ .../forms/FormMultipleChoice.test.ts | 44 --- .../images/icon_search_action_light.svg | 3 + .../source/vue_formspec/components.json | 105 ++++- 20 files changed, 1052 insertions(+), 304 deletions(-) create mode 100644 cmk/gui/form_specs/private/multiple_choice.py create mode 100644 cmk/gui/form_specs/vue/visitors/recomposers/multiple_choice.py create mode 100644 packages/cmk-frontend-vue/src/form/components/forms/FormCheckboxListChoice.vue create mode 100644 packages/cmk-frontend-vue/src/form/components/forms/FormDualListChoice.vue delete mode 100644 packages/cmk-frontend-vue/src/form/components/forms/FormMultipleChoice.vue create mode 100644 packages/cmk-frontend-vue/tests/form/components/forms/FormCheckboxListChoice.test.ts create mode 100644 packages/cmk-frontend-vue/tests/form/components/forms/FormDualListChoice.test.ts delete mode 100644 packages/cmk-frontend-vue/tests/form/components/forms/FormMultipleChoice.test.ts create mode 100644 packages/cmk-frontend/src/themes/facelift/images/icon_search_action_light.svg diff --git a/cmk/gui/form_specs/private/__init__.py b/cmk/gui/form_specs/private/__init__.py index 9e2695b3660..ffa7ab4f173 100644 --- a/cmk/gui/form_specs/private/__init__.py +++ b/cmk/gui/form_specs/private/__init__.py @@ -14,6 +14,7 @@ from .dictionary_extended import DictionaryExtended from .list_extended import ListExtended from .list_of_strings import ListOfStrings +from .multiple_choice import AdaptiveMultipleChoice, AdaptiveMultipleChoiceLayout from .optional_choice import OptionalChoice from .string_autocompleter import StringAutocompleter from .validators import not_empty @@ -32,4 +33,6 @@ "OptionalChoice", "UnknownFormSpec", "not_empty", + "AdaptiveMultipleChoice", + "AdaptiveMultipleChoiceLayout", ] diff --git a/cmk/gui/form_specs/private/multiple_choice.py b/cmk/gui/form_specs/private/multiple_choice.py new file mode 100644 index 00000000000..9f887c18c15 --- /dev/null +++ b/cmk/gui/form_specs/private/multiple_choice.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + +from dataclasses import dataclass +from enum import Enum + +from cmk.rulesets.v1.form_specs._composed import MultipleChoice + + +@dataclass(frozen=True, kw_only=True) +class AdaptiveMultipleChoiceLayout(str, Enum): + auto = "auto" + dual_list = "dual_list" + checkbox_list = "checkbox_list" + + +@dataclass(frozen=True, kw_only=True) +class AdaptiveMultipleChoice(MultipleChoice): + layout: AdaptiveMultipleChoiceLayout = AdaptiveMultipleChoiceLayout.auto diff --git a/cmk/gui/form_specs/vue/form_spec_visitor.py b/cmk/gui/form_specs/vue/form_spec_visitor.py index 67e59716be3..4aaae4d548a 100644 --- a/cmk/gui/form_specs/vue/form_spec_visitor.py +++ b/cmk/gui/form_specs/vue/form_spec_visitor.py @@ -16,6 +16,7 @@ from cmk.gui.exceptions import MKUserError from cmk.gui.form_specs.converter import SimplePassword, TransformForLegacyData, Tuple from cmk.gui.form_specs.private import ( + AdaptiveMultipleChoice, Catalog, CommentTextArea, DictionaryExtended, @@ -32,6 +33,7 @@ recompose_dictionary, recompose_host_state, recompose_list, + recompose_multiple_choice, recompose_percentage, recompose_regular_expression, recompose_service_state, @@ -132,7 +134,7 @@ def register_form_specs(): register_visitor_class(DataSize, DataSizeVisitor) register_visitor_class(Catalog, CatalogVisitor) register_visitor_class(ListExtended, ListVisitor) - register_visitor_class(MultipleChoice, MultipleChoiceVisitor) + register_visitor_class(TimeSpan, TimeSpanVisitor) register_visitor_class(TransformForLegacyData, TransformVisitor) register_visitor_class(Tuple, TupleVisitor) @@ -140,6 +142,8 @@ def register_form_specs(): register_visitor_class(SimplePassword, SimplePasswordVisitor) register_visitor_class(StringAutocompleter, StringVisitor) register_visitor_class(ListOfStrings, ListOfStringsVisitor) + register_visitor_class(AdaptiveMultipleChoice, MultipleChoiceVisitor) + register_visitor_class(MultipleChoice, MultipleChoiceVisitor, recompose_multiple_choice) # Recomposed register_visitor_class(String, StringVisitor, recompose_string) diff --git a/cmk/gui/form_specs/vue/shared_type_defs.py b/cmk/gui/form_specs/vue/shared_type_defs.py index b4c3ffc8f07..affdd49f615 100644 --- a/cmk/gui/form_specs/vue/shared_type_defs.py +++ b/cmk/gui/form_specs/vue/shared_type_defs.py @@ -89,6 +89,17 @@ class MultipleChoiceElement: title: str +@dataclass(kw_only=True) +class DualListChoiceI18n: + add: str + remove: str + add_all: str + remove_all: str + available_options: str + selected_options: str + selected: str + + class CascadingChoiceLayout(str, Enum): vertical = "vertical" horizontal = "horizontal" @@ -269,10 +280,17 @@ class SingleChoice(FormSpec): @dataclass(kw_only=True) -class MultipleChoice(FormSpec): - type: str = "multiple_choice" - elements: list[MultipleChoiceElement] = field(default_factory=lambda: []) - show_toggle_all: bool = False +class DualListChoice(FormSpec): + i18n: DualListChoiceI18n + elements: Optional[list[MultipleChoiceElement]] = field(default_factory=lambda: []) + show_toggle_all: Optional[bool] = False + type: str = "dual_list_choice" + + +@dataclass(kw_only=True) +class CheckboxListChoice(FormSpec): + type: str = "checkbox_list_choice" + elements: Optional[list[MultipleChoiceElement]] = field(default_factory=lambda: []) @dataclass(kw_only=True) @@ -398,7 +416,8 @@ class ListOfStrings(FormSpec): Password, DataSize, Catalog, - MultipleChoice, + DualListChoice, + CheckboxListChoice, TimeSpan, Tuple, OptionalChoice, diff --git a/cmk/gui/form_specs/vue/visitors/multiple_choice.py b/cmk/gui/form_specs/vue/visitors/multiple_choice.py index 2978b45fe40..a0305079a8b 100644 --- a/cmk/gui/form_specs/vue/visitors/multiple_choice.py +++ b/cmk/gui/form_specs/vue/visitors/multiple_choice.py @@ -5,6 +5,10 @@ from typing import Sequence, TypeVar +from cmk.gui.form_specs.private.multiple_choice import ( + AdaptiveMultipleChoice, + AdaptiveMultipleChoiceLayout, +) from cmk.gui.form_specs.vue import shared_type_defs from cmk.gui.form_specs.vue.validators import build_vue_validators from cmk.gui.form_specs.vue.visitors._base import FormSpecVisitor @@ -16,15 +20,14 @@ get_prefill_default, get_title_and_help, ) -from cmk.gui.i18n import translate_to_current_language +from cmk.gui.i18n import _, translate_to_current_language from cmk.rulesets.v1 import Title -from cmk.rulesets.v1.form_specs import MultipleChoice T = TypeVar("T") -class MultipleChoiceVisitor(FormSpecVisitor[MultipleChoice, Sequence[str]]): +class MultipleChoiceVisitor(FormSpecVisitor[AdaptiveMultipleChoice, Sequence[str]]): def _is_valid_choice(self, value: str) -> bool: return value in [x.name for x in self.form_spec.elements] @@ -48,7 +51,9 @@ def _parse_value(self, raw_value: object) -> Sequence[str] | EmptyValue: def _to_vue( self, raw_value: object, parsed_value: Sequence[str] | EmptyValue - ) -> tuple[shared_type_defs.MultipleChoice, Sequence[str]]: + ) -> tuple[ + shared_type_defs.DualListChoice | shared_type_defs.CheckboxListChoice, Sequence[str] + ]: title, help_text = get_title_and_help(self.form_spec) elements = [ @@ -59,13 +64,35 @@ def _to_vue( for element in self.form_spec.elements ] + if self.form_spec.layout.value == AdaptiveMultipleChoiceLayout.dual_list or ( + self.form_spec.layout.value == AdaptiveMultipleChoiceLayout.auto and len(elements) > 15 + ): + return ( + shared_type_defs.DualListChoice( + title=title, + help=help_text, + elements=elements, + validators=build_vue_validators(compute_validators(self.form_spec)), + i18n=shared_type_defs.DualListChoiceI18n( + add_all=_("Add all >>"), + remove_all=_("<< Remove all"), + add=_("Add >"), + remove=_("< Remove"), + available_options=_("Available options"), + selected_options=_("Selected options"), + selected=_("Selected"), + ), + show_toggle_all=self.form_spec.show_toggle_all, + ), + [] if isinstance(parsed_value, EmptyValue) else parsed_value, + ) + # checkbox list or auto with <= 15 elements return ( - shared_type_defs.MultipleChoice( + shared_type_defs.CheckboxListChoice( title=title, help=help_text, elements=elements, validators=build_vue_validators(compute_validators(self.form_spec)), - show_toggle_all=self.form_spec.show_toggle_all, ), [] if isinstance(parsed_value, EmptyValue) else parsed_value, ) diff --git a/cmk/gui/form_specs/vue/visitors/recomposers/__init__.py b/cmk/gui/form_specs/vue/visitors/recomposers/__init__.py index 0ca3ff26d86..bda1ea3a5b8 100644 --- a/cmk/gui/form_specs/vue/visitors/recomposers/__init__.py +++ b/cmk/gui/form_specs/vue/visitors/recomposers/__init__.py @@ -6,6 +6,7 @@ from .dictionary import recompose as recompose_dictionary from .host_state import recompose as recompose_host_state from .list import recompose as recompose_list +from .multiple_choice import recompose as recompose_multiple_choice from .percentage import recompose as recompose_percentage from .regular_expression import recompose as recompose_regular_expression from .service_state import recompose as recompose_service_state @@ -23,4 +24,5 @@ "recompose_host_state", "recompose_service_state", "recompose_string", + "recompose_multiple_choice", ] diff --git a/cmk/gui/form_specs/vue/visitors/recomposers/multiple_choice.py b/cmk/gui/form_specs/vue/visitors/recomposers/multiple_choice.py new file mode 100644 index 00000000000..6da7d25b482 --- /dev/null +++ b/cmk/gui/form_specs/vue/visitors/recomposers/multiple_choice.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. +from typing import Any + +from cmk.ccc.exceptions import MKGeneralException + +from cmk.gui.form_specs.private import AdaptiveMultipleChoice, AdaptiveMultipleChoiceLayout + +from cmk.rulesets.v1.form_specs import FormSpec, MultipleChoice + + +def recompose(form_spec: FormSpec[Any]) -> AdaptiveMultipleChoice: + if not isinstance(form_spec, MultipleChoice): + raise MKGeneralException( + f"Cannot recompose form spec. Expected a MultipleChoice form spec, got {type(form_spec)}" + ) + + return AdaptiveMultipleChoice( + # FormSpec + title=form_spec.title, + help_text=form_spec.help_text, + custom_validate=form_spec.custom_validate, + migrate=form_spec.migrate, + # MultipleChoice + elements=form_spec.elements, + show_toggle_all=form_spec.show_toggle_all, + prefill=form_spec.prefill, + # AdaptiveMultipleChoice + layout=AdaptiveMultipleChoiceLayout.auto, + ) diff --git a/packages/cmk-frontend-vue/package-lock.json b/packages/cmk-frontend-vue/package-lock.json index 7204d200d83..b7bc20bee11 100644 --- a/packages/cmk-frontend-vue/package-lock.json +++ b/packages/cmk-frontend-vue/package-lock.json @@ -958,208 +958,224 @@ "integrity": "sha512-FM3y6kfJaj5MCoAjdv24EDCTDbuFz+4+pgAunbjYfugwIE4O/xx8mPNji1n/ouG8pHCntSnBr1xwTOensF23Gg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6058,10 +6074,11 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -6073,22 +6090,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -6770,14 +6787,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" diff --git a/packages/cmk-frontend-vue/src/assets/variables.css b/packages/cmk-frontend-vue/src/assets/variables.css index dc0429e87dc..2452410ad97 100644 --- a/packages/cmk-frontend-vue/src/assets/variables.css +++ b/packages/cmk-frontend-vue/src/assets/variables.css @@ -37,6 +37,7 @@ --icon-check: url('~cmk-frontend/src/themes/facelift/images/icon_check.svg'); --icon-save: url('~cmk-frontend/src/themes/facelift/images/icon_save.svg'); --icon-cancel: url('~cmk-frontend/src/themes/facelift/images/icon_cancel.svg'); + --icon-search: url('~cmk-frontend/src/themes/facelift/images/icon_search_action_light.svg'); } @layer theme { diff --git a/packages/cmk-frontend-vue/src/form/components/FormEdit.vue b/packages/cmk-frontend-vue/src/form/components/FormEdit.vue index 8b63797f70b..5630a76e0ac 100644 --- a/packages/cmk-frontend-vue/src/form/components/FormEdit.vue +++ b/packages/cmk-frontend-vue/src/form/components/FormEdit.vue @@ -22,13 +22,14 @@ import FormDataSize from '@/form/components/forms/FormDataSize.vue' import FormCatalog from '@/form/components/forms/FormCatalog.vue' import FormTimeSpan from '@/form/components/forms/FormTimeSpan.vue' import type { ValidationMessages } from '@/form/components/utils/validation' -import FormMultipleChoice from '@/form/components/forms/FormMultipleChoice.vue' +import FormDualListChoice from '@/form/components/forms/FormDualListChoice.vue' import FormPassword from './forms/FormPassword.vue' import FormTuple from '@/form/components/forms/FormTuple.vue' import FormOptionalChoice from '@/form/components/forms/FormOptionalChoice.vue' import FormSimplePassword from '@/form/components/forms/FormSimplePassword.vue' import FormCommentTextArea from './forms/FormCommentTextArea.vue' import FormListOfStrings from '@/form/components/forms/FormListOfStrings.vue' +import FormCheckboxListChoice from './forms/FormCheckboxListChoice.vue' const props = defineProps<{ spec: FormSpec @@ -52,7 +53,8 @@ const components: Record = { boolean_choice: FormBooleanChoice, multiline_text: FormMultilineText, comment_text_area: FormCommentTextArea, - multiple_choice: FormMultipleChoice, + dual_list_choice: FormDualListChoice, + checkbox_list_choice: FormCheckboxListChoice, password: FormPassword, data_size: FormDataSize, catalog: FormCatalog, diff --git a/packages/cmk-frontend-vue/src/form/components/FormReadonly.vue b/packages/cmk-frontend-vue/src/form/components/FormReadonly.vue index 900e43cf6e7..918c89f2617 100644 --- a/packages/cmk-frontend-vue/src/form/components/FormReadonly.vue +++ b/packages/cmk-frontend-vue/src/form/components/FormReadonly.vue @@ -17,11 +17,12 @@ import type { FixedValue, BooleanChoice, MultilineText, - MultipleChoice, Password, Tuple, OptionalChoice, - ListOfStrings + ListOfStrings, + DualListChoice, + CheckboxListChoice } from '@/form/components/vue_formspec_components' import { groupDictionaryValidations, @@ -71,8 +72,10 @@ function renderForm( return renderDataSize(value as [string, string]) case 'catalog': return h('div', 'Catalog does not support readonly') - case 'multiple_choice': - return renderMultipleChoice(formSpec as MultipleChoice, value as string[]) + case 'dual_list_choice': + return renderMultipleChoice(formSpec as DualListChoice, value as string[]) + case 'checkbox_list_choice': + return renderMultipleChoice(formSpec as CheckboxListChoice, value as string[]) case 'password': return renderPassword(formSpec as Password, value as (string | boolean)[]) case 'tuple': @@ -148,7 +151,10 @@ function renderTuple( return h('span', tupleResults) } -function renderMultipleChoice(formSpec: MultipleChoice, value: string[]): VNode { +function renderMultipleChoice( + formSpec: DualListChoice | CheckboxListChoice, + value: string[] +): VNode { let nameToTitle: Record = {} for (const element of formSpec.elements) { nameToTitle[element.name] = element.title diff --git a/packages/cmk-frontend-vue/src/form/components/forms/FormCheckboxListChoice.vue b/packages/cmk-frontend-vue/src/form/components/forms/FormCheckboxListChoice.vue new file mode 100644 index 00000000000..542614b01de --- /dev/null +++ b/packages/cmk-frontend-vue/src/form/components/forms/FormCheckboxListChoice.vue @@ -0,0 +1,58 @@ + + + + + + diff --git a/packages/cmk-frontend-vue/src/form/components/forms/FormDualListChoice.vue b/packages/cmk-frontend-vue/src/form/components/forms/FormDualListChoice.vue new file mode 100644 index 00000000000..bca4080cc6e --- /dev/null +++ b/packages/cmk-frontend-vue/src/form/components/forms/FormDualListChoice.vue @@ -0,0 +1,366 @@ + + + + + + diff --git a/packages/cmk-frontend-vue/src/form/components/forms/FormMultipleChoice.vue b/packages/cmk-frontend-vue/src/form/components/forms/FormMultipleChoice.vue deleted file mode 100644 index 60db9d636ea..00000000000 --- a/packages/cmk-frontend-vue/src/form/components/forms/FormMultipleChoice.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - diff --git a/packages/cmk-frontend-vue/src/form/components/vue_formspec_components.ts b/packages/cmk-frontend-vue/src/form/components/vue_formspec_components.ts index 1158f6ab020..b48f45a2269 100644 --- a/packages/cmk-frontend-vue/src/form/components/vue_formspec_components.ts +++ b/packages/cmk-frontend-vue/src/form/components/vue_formspec_components.ts @@ -21,7 +21,8 @@ export type Components = | Password | DataSize | Catalog - | MultipleChoice + | DualListChoice + | CheckboxListChoice | TimeSpan | Tuple | OptionalChoice @@ -137,10 +138,16 @@ export type Catalog = FormSpec & { type: "catalog"; topics: Topic[]; }; -export type MultipleChoice = FormSpec & { - type: "multiple_choice"; +export type DualListChoice = (FormSpec & { elements: MultipleChoiceElement[]; show_toggle_all: boolean; + i18n: DualListChoiceI18N; +}) & { + type: "dual_list_choice"; +}; +export type CheckboxListChoice = FormSpec & { + type: "checkbox_list_choice"; + elements: MultipleChoiceElement[]; }; export type TimeSpan = FormSpec & { type?: "time_span"; @@ -249,6 +256,15 @@ export interface MultipleChoiceElement { name: string; title: string; } +export interface DualListChoiceI18N { + add: string; + remove: string; + add_all: string; + remove_all: string; + available_options: string; + selected_options: string; + selected: string; +} export interface TimeSpanI18N { millisecond: string; second: string; diff --git a/packages/cmk-frontend-vue/tests/form/components/forms/FormCheckboxListChoice.test.ts b/packages/cmk-frontend-vue/tests/form/components/forms/FormCheckboxListChoice.test.ts new file mode 100644 index 00000000000..0b3ae2d4ff8 --- /dev/null +++ b/packages/cmk-frontend-vue/tests/form/components/forms/FormCheckboxListChoice.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 + * This file is part of Checkmk (https://checkmk.com). It is subject to the terms and + * conditions defined in the file COPYING, which is part of this source code package. + */ +import { fireEvent, render, screen } from '@testing-library/vue' +import type * as FormSpec from '@/form/components/vue_formspec_components' +import CheckboxListChoice from '@/form/components/forms/FormCheckboxListChoice.vue' + +const spec: FormSpec.CheckboxListChoice = { + type: 'checkbox_list_choice', + title: 'fooTitle', + help: 'fooHelp', + elements: [ + { name: 'choice1', title: 'Choice 1' }, + { name: 'choice2', title: 'Choice 2' }, + { name: 'choice3', title: 'Choice 3' }, + { name: 'choice4', title: 'Choice 4' } + ], + validators: [] +} + +test('CmkFormCheckboxListChoice renders value', async () => { + render(CheckboxListChoice, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const choice1Checkbox = await screen.findByRole('checkbox', { name: 'Choice 1' }) + expect(choice1Checkbox).toBeChecked() + const choice2Checkbox = await screen.findByRole('checkbox', { name: 'Choice 2' }) + expect(choice2Checkbox).not.toBeChecked() + + const choice3Checkbox = await screen.findByRole('checkbox', { name: 'Choice 3' }) + expect(choice3Checkbox).toBeChecked() + + const choice4Checkbox = await screen.findByRole('checkbox', { name: 'Choice 4' }) + expect(choice4Checkbox).toBeChecked() + + await fireEvent.click(choice2Checkbox) + expect(choice2Checkbox).toBeChecked() +}) diff --git a/packages/cmk-frontend-vue/tests/form/components/forms/FormDualListChoice.test.ts b/packages/cmk-frontend-vue/tests/form/components/forms/FormDualListChoice.test.ts new file mode 100644 index 00000000000..0372a303c57 --- /dev/null +++ b/packages/cmk-frontend-vue/tests/form/components/forms/FormDualListChoice.test.ts @@ -0,0 +1,246 @@ +/** + * Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 + * This file is part of Checkmk (https://checkmk.com). It is subject to the terms and + * conditions defined in the file COPYING, which is part of this source code package. + */ +import { fireEvent, render, screen } from '@testing-library/vue' +import type * as FormSpec from '@/form/components/vue_formspec_components' +import FormDualListChoiceComponent from '@/form/components/forms/FormDualListChoice.vue' + +const spec: FormSpec.DualListChoice = { + type: 'dual_list_choice', + title: 'fooTitle', + help: 'fooHelp', + elements: [ + { name: 'choice1', title: 'Choice 1' }, + { name: 'choice2', title: 'Choice 2' }, + { name: 'choice3', title: 'Choice 3' }, + { name: 'choice4', title: 'Choice 4' } + ], + i18n: { + add_all: 'Add all', + remove_all: 'Remove all', + add: 'Add', + remove: 'Remove', + available_options: 'Available options', + selected_options: 'Selected options', + selected: 'Selected' + }, + validators: [], + show_toggle_all: false +} + +describe('FormDualListChoice', () => { + test('renders value', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + // check active elements + const activeElement = screen.getByRole('listbox', { name: 'active' }) + expect(activeElement.options.length).equal(3) + + // check inactive elements + const inactiveElement = screen.getByRole('listbox', { name: 'available' }) + expect(inactiveElement.options.length).equal(1) + + const choice3 = screen.getByRole('option', { name: 'Choice 3' }) + await fireEvent.dblClick(choice3) + expect(inactiveElement.options.length).equal(2) + }) + + test('filter choices', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const filterActiveElements = screen.getByTestId('search-active') + await fireEvent.update(filterActiveElements, 'Choice 1') + expect(screen.getByRole('listbox', { name: 'active' }).options.length).equal( + 1 + ) + + const filterInactiveElements = screen.getByTestId('search-inactive') + await fireEvent.update(filterInactiveElements, 'Choice 1') + expect( + screen.getByRole('listbox', { name: 'available' }).options.length + ).equal(0) + }) + + test('add all while filter on available options', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice3', 'choice4'], + backendValidation: [] + } + }) + + const filterInactiveElements = screen.getByTestId('search-inactive') + await fireEvent.update(filterInactiveElements, 'Choice 1') + const addAll = screen.getByRole('button', { name: 'Add all' }) + await fireEvent.click(addAll) + expect(screen.getByRole('listbox', { name: 'active' }).options.length).equal( + 3 + ) + }) + + test('remove all while filter on selected options', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const filterActiveElements = screen.getByTestId('search-active') + await fireEvent.update(filterActiveElements, 'Choice 1') + const removeAll = screen.getByRole('button', { name: 'Remove all' }) + await fireEvent.click(removeAll) + expect( + screen.getByRole('listbox', { name: 'available' }).options.length + ).equal(2) + }) + + test('add by double click on available item', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const choice2 = screen.getByRole('option', { name: 'Choice 2' }) + await fireEvent.dblClick(choice2) + expect(screen.getByRole('listbox', { name: 'active' }).options.length).equal( + 4 + ) + }) + + test('remove by double click on selected item', async () => { + render(FormDualListChoiceComponent, { + props: { + spec, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const choice3 = screen.getByRole('option', { name: 'Choice 3' }) + await fireEvent.dblClick(choice3) + expect(screen.getByRole('listbox', { name: 'active' }).options.length).equal( + 2 + ) + }) + + describe('style', () => { + test('list height must be 200px when there is less than 10 elements', async () => { + render(FormDualListChoiceComponent, { + props: { + spec: { + ...spec, + elements: [ + { name: 'choice1', title: 'Choice 1' }, + { name: 'choice2', title: 'Choice 2' }, + { name: 'choice3', title: 'Choice 3' }, + { name: 'choice4', title: 'Choice 4' }, + { name: 'choice5', title: 'Choice 5' }, + { name: 'choice6', title: 'Choice 6' }, + { name: 'choice7', title: 'Choice 7' }, + { name: 'choice8', title: 'Choice 8' }, + { name: 'choice9', title: 'Choice 9' } + ] + }, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const list = screen.getByRole('listbox', { name: 'active' }) + expect(list.style.height).toBe('200px') + }) + + test('list height must be (11 * 15)px when there is 11 elements', async () => { + render(FormDualListChoiceComponent, { + props: { + spec: { + ...spec, + elements: [ + { name: 'choice1', title: 'Choice 1' }, + { name: 'choice2', title: 'Choice 2' }, + { name: 'choice3', title: 'Choice 3' }, + { name: 'choice4', title: 'Choice 4' }, + { name: 'choice5', title: 'Choice 5' }, + { name: 'choice6', title: 'Choice 6' }, + { name: 'choice7', title: 'Choice 7' }, + { name: 'choice8', title: 'Choice 8' }, + { name: 'choice9', title: 'Choice 9' }, + { name: 'choice10', title: 'Choice 10' }, + { name: 'choice11', title: 'Choice 11' } + ] + }, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + + const list = screen.getByRole('listbox', { name: 'active' }) + expect(list.style.height).toBe('165px') + }) + test('list max height must be 400px when there is lots of items', async () => { + render(FormDualListChoiceComponent, { + props: { + spec: { + ...spec, + elements: [ + { name: 'choice1', title: 'Choice 1' }, + { name: 'choice2', title: 'Choice 2' }, + { name: 'choice3', title: 'Choice 3' }, + { name: 'choice4', title: 'Choice 4' }, + { name: 'choice5', title: 'Choice 5' }, + { name: 'choice6', title: 'Choice 6' }, + { name: 'choice7', title: 'Choice 7' }, + { name: 'choice8', title: 'Choice 8' }, + { name: 'choice9', title: 'Choice 9' }, + { name: 'choice10', title: 'Choice 10' }, + { name: 'choice11', title: 'Choice 11' }, + { name: 'choice12', title: 'Choice 12' }, + { name: 'choice13', title: 'Choice 13' }, + { name: 'choice14', title: 'Choice 14' }, + { name: 'choice15', title: 'Choice 15' }, + { name: 'choice16', title: 'Choice 16' }, + { name: 'choice17', title: 'Choice 17' }, + { name: 'choice18', title: 'Choice 18' }, + { name: 'choice19', title: 'Choice 19' }, + { name: 'choice20', title: 'Choice 20' }, + { name: 'choice21', title: 'Choice 21' }, + { name: 'choice22', title: 'Choice 22' }, + { name: 'choice23', title: 'Choice 23' }, + { name: 'choice24', title: 'Choice 24' }, + { name: 'choice25', title: 'Choice 25' }, + { name: 'choice26', title: 'Choice 26' }, + { name: 'choice27', title: 'Choice 27' }, + { name: 'choice28', title: 'Choice 28' }, + { name: 'choice29', title: 'Choice 29' }, + { name: 'choice30', title: 'Choice 30' } + ] + }, + data: ['choice1', 'choice3', 'choice4'], + backendValidation: [] + } + }) + const list = screen.getByRole('listbox', { name: 'active' }) + expect(list.style.height).toBe('400px') + }) + }) +}) diff --git a/packages/cmk-frontend-vue/tests/form/components/forms/FormMultipleChoice.test.ts b/packages/cmk-frontend-vue/tests/form/components/forms/FormMultipleChoice.test.ts deleted file mode 100644 index 77f451b35fd..00000000000 --- a/packages/cmk-frontend-vue/tests/form/components/forms/FormMultipleChoice.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 - * This file is part of Checkmk (https://checkmk.com). It is subject to the terms and - * conditions defined in the file COPYING, which is part of this source code package. - */ -import { fireEvent, render, screen } from '@testing-library/vue' -import type * as FormSpec from '@/form/components/vue_formspec_components' -import CmkFormMultipleChoice from '@/form/components/forms/FormMultipleChoice.vue' - -const spec: FormSpec.MultipleChoice = { - type: 'multiple_choice', - title: 'fooTitle', - help: 'fooHelp', - elements: [ - { name: 'choice1', title: 'Choice 1' }, - { name: 'choice2', title: 'Choice 2' }, - { name: 'choice3', title: 'Choice 3' }, - { name: 'choice4', title: 'Choice 4' } - ], - validators: [], - show_toggle_all: false -} - -test('CmkFormMultipleChoice renders value', async () => { - render(CmkFormMultipleChoice, { - props: { - spec, - data: ['choice1', 'choice3', 'choice4'], - backendValidation: [] - } - }) - - // check active elements - const activeElement = screen.getByRole('listbox', { name: 'active' }) - expect(activeElement.options.length).equal(3) - - // check inactive elements - const inactiveElement = screen.getByRole('listbox', { name: 'available' }) - expect(inactiveElement.options.length).equal(1) - - const choice3 = screen.getByRole('option', { name: 'Choice 3' }) - await fireEvent.dblClick(choice3) - expect(inactiveElement.options.length).equal(2) -}) diff --git a/packages/cmk-frontend/src/themes/facelift/images/icon_search_action_light.svg b/packages/cmk-frontend/src/themes/facelift/images/icon_search_action_light.svg new file mode 100644 index 00000000000..2ffdc70a680 --- /dev/null +++ b/packages/cmk-frontend/src/themes/facelift/images/icon_search_action_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/cmk-shared-typing/source/vue_formspec/components.json b/packages/cmk-shared-typing/source/vue_formspec/components.json index b3cf12439e1..8ba102bd313 100644 --- a/packages/cmk-shared-typing/source/vue_formspec/components.json +++ b/packages/cmk-shared-typing/source/vue_formspec/components.json @@ -355,31 +355,101 @@ }, "required": ["name", "title"] }, - "multiple_choice": { + "dual_list_choice": { + "id": "dual_list_choice", "type": "object", "properties": { "type": { - "const": "multiple_choice", - "default": "multiple_choice" + "const": "dual_list_choice", + "default": "dual_list_choice" + } + }, + "allOf": [ + { + "$ref": "#/$defs/form_spec" }, - "elements": { - "type": "array", - "items": { - "$ref": "#/$defs/multiple_choice_element" + { + "type": "object", + "properties": { + "elements": { + "type": "array", + "items": { + "$ref": "#/$defs/multiple_choice_element" + }, + "default": [] + }, + "show_toggle_all": { + "type": "boolean", + "default": false + }, + "i18n": { + "type": "object", + "title": "dual_list_choice_i18n", + "properties": { + "add": { + "type": "string" + }, + "remove": { + "type": "string" + }, + "add_all": { + "type": "string" + }, + "remove_all": { + "type": "string" + }, + "available_options": { + "type": "string" + }, + "selected_options": { + "type": "string" + }, + "selected": { + "type": "string" + } + }, + "required": [ + "add", + "remove", + "add_all", + "remove_all", + "available_options", + "selected_options", + "selected" + ] + } }, - "default": [] - }, - "show_toggle_all": { - "type": "boolean", - "default": false + "required": ["elements", "show_toggle_all", "i18n"] } - }, - "required": ["type", "elements", "show_toggle_all"], + ], + "required": ["type"] + }, + "checkbox_list_choice": { + "id": "checkbox_list_choice", + "type": "object", "allOf": [ { "$ref": "#/$defs/form_spec" + }, + { + "type": "object", + "properties": { + "type": { + "const": "checkbox_list_choice", + "default": "checkbox_list_choice" + }, + "elements": { + "type": "array", + "items": { + "$ref": "#/$defs/multiple_choice_element" + }, + "default": [] + } + }, + "required": ["type", "elements", "show_toggle_all"] } - ] + ], + "required": ["type"] }, "cascading_single_choice_element": { "type": "object", @@ -841,7 +911,10 @@ "$ref": "#/$defs/catalog" }, { - "$ref": "#/$defs/multiple_choice" + "$ref": "#/$defs/dual_list_choice" + }, + { + "$ref": "#/$defs/checkbox_list_choice" }, { "$ref": "#/$defs/time_span"