Преглед на файлове

Merge branch 'main' into wptmdoorn/main

# Conflicts:
#	main.py
Falko Schindler преди 2 години
родител
ревизия
db9cba0ee5

+ 1 - 0
examples/sqlite_database/.gitignore

@@ -0,0 +1 @@
+*.db

+ 79 - 0
examples/sqlite_database/main.py

@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+import sqlite3
+from pathlib import Path
+from typing import Any, Dict
+
+from nicegui import ui
+
+DB_FILE = Path(__file__).parent / 'users.db'
+DB_FILE.touch()
+conn = sqlite3.connect(DB_FILE, check_same_thread=False)
+cursor = conn.cursor()
+cursor.execute('CREATE TABLE IF NOT EXISTS users (id integer primary key AUTOINCREMENT, name text, age integer)')
+conn.commit()
+
+
+@ui.refreshable
+def users_ui() -> None:
+    cursor.execute('SELECT * FROM users')
+    for row in cursor.fetchall():
+        user = {'id': row[0], 'name': row[1], 'age': row[2]}
+        with ui.card():
+            with ui.row().classes('justify-between w-full'):
+                ui.label(user['id'])
+                ui.label(user['name'])
+                ui.label(user['age'])
+            with ui.row():
+                ui.button('edit', on_click=lambda _, user=user: open_dialog(user))
+                ui.button('delete', on_click=lambda _, user=user: delete(user), color='red')
+
+
+def create() -> None:
+    cursor.execute('INSERT INTO users (name, age) VALUES (?, ?)', (name.value, age.value))
+    conn.commit()
+    ui.notify(f'Created new user {name.value}')
+    name.value = ''
+    age.value = None
+    users_ui.refresh()
+
+
+def update() -> None:
+    query = 'UPDATE users SET name=?, age=? WHERE id=?'
+    cursor.execute(query, (dialog_name.value, dialog_age.value, dialog_id))
+    conn.commit()
+    ui.notify(f'Updated user {dialog_name.value}')
+    dialog.close()
+    users_ui.refresh()
+
+
+def delete(user: Dict[str, Any]) -> None:
+    cursor.execute('DELETE from users WHERE id=?', (user['id'],))
+    conn.commit()
+    ui.notify(f'Deleted user {user["name"]}')
+    users_ui.refresh()
+
+
+def open_dialog(user: Dict[str, Any]) -> None:
+    global dialog_id
+    dialog_id = user['id']
+    dialog_name.value = user['name']
+    dialog_age.value = user['age']
+    dialog.open()
+
+
+name = ui.input(label='Name')
+age = ui.number(label='Age', format='%.0f')
+ui.button('Add new user', on_click=create)
+
+users_ui()
+
+with ui.dialog() as dialog:
+    with ui.card():
+        dialog_id = None
+        dialog_name = ui.input('Name')
+        dialog_age = ui.number('Age', format='%.0f')
+        with ui.row():
+            ui.button('Save', on_click=update)
+            ui.button('Close', on_click=dialog.close).props('outline')
+
+ui.run()

+ 1 - 0
main.py

@@ -271,6 +271,7 @@ async def index_page(client: Client):
             example_link('Single Page App', 'navigate without reloading the page')
             example_link('Chat App', 'a simple chat app')
             example_link('Chat with AI App', 'a simple chat app with AI')
+            example_link('SQLite Database', 'CRUD operations on a SQLite database')
             example_link('Pandas DataFrame',
                          'shows how to display an editable [pandas](https://pandas.pydata.org) DataFrame')
 

+ 1 - 1
nicegui/client.py

@@ -53,7 +53,7 @@ class Client:
     @property
     def ip(self) -> Optional[str]:
         """Return the IP address of the client, or None if the client is not connected."""
-        return self.environ.get('REMOTE_ADDR') if self.environ else None
+        return self.environ['asgi.scope']['client'][0] if self.environ else None
 
     @property
     def has_socket_connection(self) -> bool:

+ 2 - 2
nicegui/elements/chat_message.js

@@ -1,7 +1,7 @@
 export default {
   template: `
     <q-chat-message
-      :text="[text]"
+      :text="text"
       :name="name"
       :label="label"
       :stamp="stamp"
@@ -10,7 +10,7 @@ export default {
     />
   `,
   props: {
-    text: String,
+    text: Array,
     name: String,
     label: String,
     stamp: String,

+ 14 - 3
nicegui/elements/chat_message.py

@@ -1,4 +1,5 @@
-from typing import Optional
+import html
+from typing import List, Optional, Union
 
 from ..dependencies import register_component
 from ..element import Element
@@ -9,26 +10,36 @@ register_component('chat_message', __file__, 'chat_message.js')
 class ChatMessage(Element):
 
     def __init__(self,
-                 text: str, *,
+                 text: Union[str, List[str]], *,
                  name: Optional[str] = None,
                  label: Optional[str] = None,
                  stamp: Optional[str] = None,
                  avatar: Optional[str] = None,
                  sent: bool = False,
+                 text_html: bool = False,
                  ) -> None:
         """Chat Message
 
         Based on Quasar's `Chat Message <https://quasar.dev/vue-components/chat/>`_ component.
 
-        :param text: the message body
+        :param text: the message body (can be a list of strings for multiple message parts)
         :param name: the name of the message author
         :param label: renders a label header/section only
         :param stamp: timestamp of the message
         :param avatar: URL to an avatar
         :param sent: render as a sent message (so from current user) (default: False)
+        :param text_html: render text as HTML (default: False)
         """
         super().__init__('chat_message')
+
+        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
         if label is not None:

+ 8 - 0
nicegui/static/nicegui.css

@@ -77,6 +77,14 @@
   padding: 1rem 1rem 0.5rem 1rem;
   margin: 1rem 0;
 }
+.nicegui-markdown th {
+  padding: 0.5rem;
+  border: 1px solid #8884;
+}
+.nicegui-markdown td {
+  padding: 0.5rem;
+  border: 1px solid #8884;
+}
 
 #popup {
   position: fixed;

+ 1 - 1
tests/test_auto_context.py

@@ -92,7 +92,7 @@ def test_autoupdate_on_async_event_handler(screen: Screen):
 def test_autoupdate_on_async_timer_callback(screen: Screen):
     async def update():
         ui.label('1')
-        await asyncio.sleep(2.0)
+        await asyncio.sleep(3.0)
         ui.label('2')
     ui.label('0')
     ui.timer(2.0, update, once=True)

+ 27 - 0
tests/test_chat.py

@@ -0,0 +1,27 @@
+from selenium.webdriver.common.by import By
+
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_no_html(screen: Screen):
+    ui.chat_message('<strong>HTML</strong>')
+
+    screen.open('/')
+    screen.should_contain('<strong>HTML</strong>')
+
+
+def test_html(screen: Screen):
+    ui.chat_message('<strong>HTML</strong>', text_html=True)
+
+    screen.open('/')
+    screen.should_contain('HTML')
+    screen.should_not_contain('<strong>HTML</strong>')
+
+
+def test_newline(screen: Screen):
+    ui.chat_message('Hello\nNiceGUI!')
+
+    screen.open('/')
+    assert screen.find('Hello').find_element(By.TAG_NAME, 'br')

+ 4 - 0
tests/test_input.py

@@ -96,11 +96,15 @@ def test_autocompletion(screen: Screen):
     screen.should_contain('oo')
 
     element.send_keys(Keys.TAB)
+    screen.wait(0.2)
     assert element.get_attribute('value') == 'foo'
 
     element.send_keys(Keys.BACKSPACE)
+    screen.wait(0.2)
     element.send_keys(Keys.BACKSPACE)
+    screen.wait(0.2)
     element.send_keys('x')
+    screen.wait(0.2)
     element.send_keys(Keys.TAB)
     screen.wait(0.5)
     assert element.get_attribute('value') == 'fx'

+ 0 - 36
website/documentation.py

@@ -384,42 +384,6 @@ def create_full() -> None:
         ui.label(f'shared auto-index page with ID {CONSTANT_UUID}')
         ui.link('private page', private_page)
 
-    @text_demo('Pages with Path Parameters', '''
-        Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
-        If type-annotated, they are automatically converted to bool, int, float and complex values.
-        If the page function expects a `request` argument, the request object is automatically provided.
-        The `client` argument provides access to the websocket connection, layout, etc.
-    ''')
-    def page_with_path_parameters_demo():
-        @ui.page('/repeat/{word}/{count}')
-        def page(word: str, count: int):
-            ui.label(word * count)
-
-        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
-
-    @text_demo('Wait for Client Connection', '''
-        To wait for a client connection, you can add a `client` argument to the decorated page function
-        and await `client.connected()`.
-        All code below that statement is executed after the websocket connection between server and client has been established.
-
-        For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
-        Also it is possible to do async stuff while the user already sees some content.
-    ''')
-    def wait_for_connected_demo():
-        import asyncio
-
-        from nicegui import Client
-
-        @ui.page('/wait_for_connection')
-        async def wait_for_connection(client: Client):
-            ui.label('This text is displayed immediately.')
-            await client.connected()
-            await asyncio.sleep(2)
-            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
-            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
-
-        ui.link('wait for connection', wait_for_connection)
-
     @text_demo('Page Layout', '''
         With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
         The `fixed` argument controls whether the element should scroll or stay fixed on the screen.

+ 5 - 3
website/documentation_tools.py

@@ -38,8 +38,7 @@ def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[s
     ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
     with ui.row().classes('gap-2 items-center relative'):
         if more_link:
-            with ui.link(text, f'documentation/{more_link}').classes('text-2xl'):
-                ui.icon('open_in_new', size='0.75em').classes('mb-1 ml-2')
+            ui.link(text, f'documentation/{more_link}').classes('text-2xl')
         else:
             ui.label(text).classes('text-2xl')
         with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
@@ -99,7 +98,10 @@ class element_demo:
         with ui.column().classes('w-full mb-8 gap-2'):
             subheading(title, more_link=more_link)
             render_docstring(documentation, with_params=more_link is None)
-            return demo(f)
+            result = demo(f)
+            if more_link:
+                ui.markdown(f'See [more...](documentation/{more_link})').classes('bold-links arrow-links')
+        return result
 
 
 def load_demo(api: Union[type, Callable, str]) -> None:

+ 23 - 0
website/more_documentation/chat_message_documentation.py

@@ -1,8 +1,31 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.chat_message('Hello NiceGUI!',
                     name='Robot',
                     stamp='now',
                     avatar='https://robohash.org/ui')
+
+
+def more() -> None:
+    @text_demo('HTML text', '''
+        Using the `text_html` parameter, you can send HTML text to the chat.
+    ''')
+    def html_text():
+        ui.chat_message('Without <strong>HTML</strong>')
+        ui.chat_message('With <strong>HTML</strong>', text_html=True)
+
+    @text_demo('Newline', '''
+        You can use newlines in the chat message.
+    ''')
+    def newline():
+        ui.chat_message('This is a\nlong line!')
+
+    @text_demo('Multi-part messages', '''
+        You can send multiple message parts by passing a list of strings.
+    ''')
+    def multiple_messages():
+        ui.chat_message(['Hi! 😀', 'How are you?'])

+ 12 - 0
website/more_documentation/markdown_documentation.py

@@ -39,3 +39,15 @@ def more() -> None:
             ui.run(dark=True)
             ```
         ''')
+
+    @text_demo('Markdown tables', '''
+        By activating the "tables" extra, you can use Markdown tables.
+        See the [markdown2 documentation](https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras) for a list of available extras.
+    ''')
+    def markdown_with_code_blocks():
+        ui.markdown('''
+            | First name | Last name |
+            | ---------- | --------- |
+            | Max        | Planck    |
+            | Marie      | Curie     |
+        ''', extras=['tables'])

+ 40 - 0
website/more_documentation/page_documentation.py

@@ -1,5 +1,7 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     @ui.page('/other_page')
@@ -14,3 +16,41 @@ def main_demo() -> None:
 
     ui.link('Visit other page', other_page)
     ui.link('Visit dark page', dark_page)
+
+
+def more() -> None:
+    @text_demo('Pages with Path Parameters', '''
+        Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
+        If type-annotated, they are automatically converted to bool, int, float and complex values.
+        If the page function expects a `request` argument, the request object is automatically provided.
+        The `client` argument provides access to the websocket connection, layout, etc.
+    ''')
+    def page_with_path_parameters_demo():
+        @ui.page('/repeat/{word}/{count}')
+        def page(word: str, count: int):
+            ui.label(word * count)
+
+        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
+
+    @text_demo('Wait for Client Connection', '''
+        To wait for a client connection, you can add a `client` argument to the decorated page function
+        and await `client.connected()`.
+        All code below that statement is executed after the websocket connection between server and client has been established.
+
+        For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
+        Also it is possible to do async stuff while the user already sees some content.
+    ''')
+    def wait_for_connected_demo():
+        import asyncio
+
+        from nicegui import Client
+
+        @ui.page('/wait_for_connection')
+        async def wait_for_connection(client: Client):
+            ui.label('This text is displayed immediately.')
+            await client.connected()
+            await asyncio.sleep(2)
+            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
+            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
+
+        ui.link('wait for connection', wait_for_connection)

+ 14 - 1
website/more_documentation/query_documentation.py

@@ -1,4 +1,6 @@
-from nicegui import ui
+from nicegui import globals, ui
+
+from ..documentation_tools import text_demo
 
 
 def main_demo() -> None:
@@ -10,3 +12,14 @@ def main_demo() -> None:
     # END OF DEMO
     ui.button('Blue', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ddeeff'))
     ui.button('Orange', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ffeedd'))
+
+
+def more() -> None:
+    @text_demo('Set background gradient', '''
+        It's easy to set a background gradient, image or similar. 
+        See [w3schools.com](https://www.w3schools.com/cssref/pr_background-image.php) for more information about setting background with CSS.
+    ''')
+    def background_image():
+        # ui.query('body').classes('bg-gradient-to-t from-blue-400 to-blue-100')
+        # END OF DEMO
+        globals.get_slot_stack()[-1].parent.classes('bg-gradient-to-t from-blue-400 to-blue-100')