Browse Source

Merge branch 'main' into documentation

Falko Schindler 1 year ago
parent
commit
576f4f0559

+ 3 - 2
.github/workflows/test.yml

@@ -6,7 +6,7 @@ jobs:
   test:
     strategy:
       matrix:
-        python: ["3.8", "3.9", "3.10", "3.11"]
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
       fail-fast: false
     runs-on: ubuntu-latest
     timeout-minutes: 40
@@ -19,9 +19,10 @@ jobs:
       - name: set up Poetry
         uses: abatilo/actions-poetry@v2.0.0
         with:
-          poetry-version: "1.3.1"
+          poetry-version: "1.6.1"
       - name: install dependencies
         run: |
+          set -x
           poetry config virtualenvs.create false
           poetry install --all-extras
           # install packages to run the examples

+ 0 - 3
nicegui/elements/chat_message.js

@@ -1,3 +0,0 @@
-export default {
-  template: `<q-chat-message v-bind="$attrs" />`,
-};

+ 10 - 5
nicegui/elements/chat_message.py

@@ -2,12 +2,13 @@ import html
 from typing import List, Optional, Union
 
 from ..element import Element
+from .html import Html
 
 
-class ChatMessage(Element, component='chat_message.js'):
+class ChatMessage(Element):
 
     def __init__(self,
-                 text: Union[str, List[str]], *,
+                 text: Union[str, List[str]] = ..., *,
                  name: Optional[str] = None,
                  label: Optional[str] = None,
                  stamp: Optional[str] = None,
@@ -27,15 +28,15 @@ class ChatMessage(Element, component='chat_message.js'):
         :param sent: render as a sent message (so from current user) (default: False)
         :param text_html: render text as HTML (default: False)
         """
-        super().__init__()
+        super().__init__('q-chat-message')
 
+        if text is ...:
+            text = []
         if isinstance(text, str):
             text = [text]
         if not text_html:
             text = [html.escape(part) for part in text]
             text = [part.replace('\n', '<br />') for part in text]
-        self._props['text'] = text
-        self._props['text-html'] = True
 
         if name is not None:
             self._props['name'] = name
@@ -46,3 +47,7 @@ class ChatMessage(Element, component='chat_message.js'):
         if avatar is not None:
             self._props['avatar'] = avatar
         self._props['sent'] = sent
+
+        with self:
+            for line in text:
+                Html(line)

+ 15 - 0
nicegui/elements/number.py

@@ -14,6 +14,7 @@ class Number(ValidationElement, DisableableElement):
                  value: Optional[float] = None,
                  min: Optional[float] = None,  # pylint: disable=redefined-builtin
                  max: Optional[float] = None,  # pylint: disable=redefined-builtin
+                 precision: Optional[int] = None,
                  step: Optional[float] = None,
                  prefix: Optional[str] = None,
                  suffix: Optional[str] = None,
@@ -33,6 +34,7 @@ class Number(ValidationElement, DisableableElement):
         :param value: the initial value of the field
         :param min: the minimum value allowed
         :param max: the maximum value allowed
+        :param precision: the number of decimal places allowed (default: no limit, negative: decimal places before the dot)
         :param step: the step size for the stepper buttons
         :param prefix: a prefix to prepend to the displayed value
         :param suffix: a suffix to append to the displayed value
@@ -51,6 +53,7 @@ class Number(ValidationElement, DisableableElement):
             self._props['min'] = min
         if max is not None:
             self._props['max'] = max
+        self._precision = precision
         if step is not None:
             self._props['step'] = step
         if prefix is not None:
@@ -79,6 +82,16 @@ class Number(ValidationElement, DisableableElement):
         self._props['max'] = value
         self.sanitize()
 
+    @property
+    def precision(self) -> Optional[int]:
+        """The number of decimal places allowed (default: no limit, negative: decimal places before the dot)."""
+        return self._precision
+
+    @precision.setter
+    def precision(self, value: Optional[int]) -> None:
+        self._precision = value
+        self.sanitize()
+
     @property
     def out_of_limits(self) -> bool:
         """Whether the current value is out of the allowed limits."""
@@ -91,6 +104,8 @@ class Number(ValidationElement, DisableableElement):
         value = float(self.value)
         value = max(value, self.min)
         value = min(value, self.max)
+        if self.precision is not None:
+            value = float(round(value, self.precision))
         self.set_value(float(self.format % value) if self.format else value)
 
     def _event_args_to_value(self, e: GenericEventArguments) -> Any:

+ 13 - 6
nicegui/elements/select.js

@@ -20,13 +20,20 @@ export default {
   },
   methods: {
     filterFn(val, update, abort) {
-      update(() => {
-        const needle = val.toLocaleLowerCase();
-        this.filteredOptions = needle
-          ? this.initialOptions.filter((v) => String(v.label).toLocaleLowerCase().indexOf(needle) > -1)
-          : this.initialOptions;
-      });
+      update(() => (this.filteredOptions = this.findFilteredOptions()));
     },
+    findFilteredOptions() {
+      const needle = this.$el.querySelector("input[type=search]")?.value.toLocaleLowerCase();
+      return needle
+        ? this.initialOptions.filter((v) => String(v.label).toLocaleLowerCase().indexOf(needle) > -1)
+        : this.initialOptions;
+    },
+  },
+  updated() {
+    const newFilteredOptions = this.findFilteredOptions();
+    if (newFilteredOptions.length !== this.filteredOptions.length) {
+      this.filteredOptions = newFilteredOptions;
+    }
   },
   watch: {
     options: {

File diff suppressed because it is too large
+ 282 - 569
poetry.lock


+ 5 - 3
pyproject.toml

@@ -27,9 +27,8 @@ aiofiles = "^23.1.0"
 pywebview = { version = "^4.4.0", optional = true }
 plotly = { version = "^5.13.0", optional = true }
 matplotlib = { version = "^3.5.0", optional = true }
-nicegui-highcharts = { version = "^1.0.1", optional = true }
 httpx = ">=0.24.0,<1.0.0"
-aiohttp = "^3.8.5"
+nicegui-highcharts = { version = "^1.0.1", optional = true }
 ifaddr = "^0.2.0"
 
 [tool.poetry.extras]
@@ -51,7 +50,10 @@ docutils = "^0.19"
 pandas = "^2.0.0"
 secure = "^0.3.0"
 webdriver-manager = "^3.8.6"
-numpy = ">=1.24.0"
+numpy = [
+    {version = "^1.24.0", python = "~3.8"},
+    {version = "^1.26.0", python = ">=3.9,<3.13"}
+]
 selenium = "^4.11.2"
 beautifulsoup4 = "^4.12.2"
 urllib3 = ">=1.26.18,^1.26 || >=2.0.7" # https://github.com/zauberzeug/nicegui/security/dependabot/23

+ 6 - 1
test_startup.sh

@@ -36,7 +36,12 @@ do
     if test $(python3 --version | cut -d' ' -f2 | cut -d'.' -f1,2) = "3.11" && test $path = "examples/sqlite_database"; then
         continue # until https://github.com/omnilib/aiosqlite/issues/241 is fixed
     fi
-    
+
+    # skip if python is 3.12 and if path is examples/sqlite_database
+    if test $(python3 --version | cut -d' ' -f2 | cut -d'.' -f1,2) = "3.12" && test $path = "examples/sqlite_database"; then
+        continue # until https://github.com/omnilib/aiosqlite/issues/241 is fixed
+    fi
+
     # install all requirements except nicegui
     if test -f $path/requirements.txt; then
         sed '/^nicegui/d' $path/requirements.txt > $path/requirements.tmp.txt || error=1 # remove nicegui from requirements.txt

+ 8 - 0
tests/test_chat.py

@@ -25,3 +25,11 @@ def test_newline(screen: Screen):
 
     screen.open('/')
     assert screen.find('Hello').find_element(By.TAG_NAME, 'br')
+
+
+def test_slot(screen: Screen):
+    with ui.chat_message():
+        ui.label('slot')
+
+    screen.open('/')
+    screen.should_contain('slot')

+ 20 - 0
tests/test_number.py

@@ -1,3 +1,4 @@
+import pytest
 from selenium.webdriver.common.by import By
 
 from nicegui import ui
@@ -56,3 +57,22 @@ def test_out_of_limits(screen: Screen):
 
     number.max = 15
     screen.should_contain('out_of_limits: False')
+
+
+@pytest.mark.parametrize('precision', [None, 1, -1])
+def test_rounding(precision: int, screen: Screen):
+    number = ui.number('Number', value=12, precision=precision)
+    ui.label().bind_text_from(number, 'value', lambda value: f'number=_{value}_')
+
+    screen.open('/')
+    screen.should_contain('number=_12_')
+
+    element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Number"]')
+    element.send_keys('.345')
+    screen.click('number=')  # blur the number input
+    if precision is None:
+        screen.should_contain('number=_12.345_')
+    elif precision == 1:
+        screen.should_contain('number=_12.3_')
+    elif precision == -1:
+        screen.should_contain('number=_10.0_')

+ 25 - 0
tests/test_select.py

@@ -140,3 +140,28 @@ def test_add_new_values(screen:  Screen, option_dict: bool, multiple: bool, new_
             screen.should_contain("value = ['a']" if multiple else 'value = None')
             screen.should_contain("options = {'a': 'A', 'b': 'B', 'c': 'C'}" if option_dict else
                                   "options = ['a', 'b', 'c']")
+
+
+def test_keep_filtered_options(screen: Screen):
+    ui.select(options=['A1', 'A2', 'B1', 'B2'], with_input=True, multiple=True)
+
+    screen.open('/')
+    screen.find_by_tag('input').click()
+    screen.should_contain('A1')
+    screen.should_contain('A2')
+    screen.should_contain('B1')
+    screen.should_contain('B2')
+
+    screen.find_by_tag('input').send_keys('A')
+    screen.wait(0.5)
+    screen.should_contain('A1')
+    screen.should_contain('A2')
+    screen.should_not_contain('B1')
+    screen.should_not_contain('B2')
+
+    screen.click('A1')
+    screen.wait(0.5)
+    screen.should_contain('A1')
+    screen.should_contain('A2')
+    screen.should_not_contain('B1')
+    screen.should_not_contain('B2')

+ 8 - 0
website/documentation/more/chat_message_documentation.py

@@ -29,3 +29,11 @@ def more() -> None:
     ''')
     def multiple_messages():
         ui.chat_message(['Hi! 😀', 'How are you?'])
+
+    @text_demo('Chat message with child elements', '''
+        You can add child elements to a chat message.
+    ''')
+    def child_elements():
+        with ui.chat_message():
+            ui.label('Guess where I am!')
+            ui.image('https://picsum.photos/id/249/640/360').classes('w-64')

+ 11 - 1
website/documentation/more/number_documentation.py

@@ -10,10 +10,20 @@ def main_demo() -> None:
 
 
 def more() -> None:
-
     @text_demo('Clearable', '''
         The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
     ''')
     def clearable():
         i = ui.number(value=42).props('clearable')
         ui.label().bind_text_from(i, 'value')
+
+    @text_demo('Number of decimal places', '''
+        You can specify the number of decimal places using the `precision` parameter.
+        A negative value means decimal places before the dot.
+        The rounding takes place when the input loses focus,
+        when sanitization parameters like min, max or precision change,
+        or when `sanitize()` is called manually.
+    ''')
+    def integer():
+        n = ui.number(value=3.14159265359, precision=5)
+        n.sanitize()

Some files were not shown because too many files changed in this diff