diff --git a/apps/hello/main.py b/apps/hello/main.py index 4d582a8c8..fb11524dc 100644 --- a/apps/hello/main.py +++ b/apps/hello/main.py @@ -20,7 +20,10 @@ def update(state, session): main_df = _get_main_df() main_df = main_df[main_df['length_cm'] >= state["filter"]["min_length"]] main_df = main_df[main_df['weight_g'] >= state["filter"]["min_weight"]] - state["main_df"] = main_df + state['main_df'] = main_df + + paginated_members = _get_paginated_members(state['paginated_members_page'] - 1, state['paginated_members_page_size']) + state['paginated_members'] = paginated_members state["session"] = session _update_metrics(state) _update_role_chart(state) @@ -33,6 +36,22 @@ def handle_story_download(state): state.file_download(data, file_name) +def handle_paginated_members_page_change(state, payload): + page = payload + maxpage = int(state["paginated_members_total_items"] / state["paginated_members_page_size"]) + 1 + if page > maxpage: + state["paginated_members_page"] = maxpage - 2 + else: + state["paginated_members_page"] = page + + update(state, None) + + +def handle_paginated_members_page_size_change(state, payload): + state['paginated_members_page_size'] = payload + update(state, None) + + # LOAD / GENERATE DATA @@ -47,12 +66,15 @@ def _get_main_df(): main_df = pd.read_csv("assets/main_df.csv") return main_df - def _get_highlighted_members(): sample_df = _get_main_df().sample(3).set_index("name", drop=False) sample = sample_df.to_dict("index") return sample +def _get_paginated_members(offset: int, limit: int): + paginated_df = _get_main_df()[offset:offset + limit].set_index("name", drop=False) + paginated = paginated_df.to_dict("index") + return paginated def _get_story_text(): with open("assets/story.txt", "r") as f: @@ -117,6 +139,10 @@ def _update_scatter_chart(state): "highlighted_members": _get_highlighted_members(), "random_df": _generate_random_df(), "hue_rotation": 26, + "paginated_members": _get_paginated_members(0, 2), + "paginated_members_page": 1, + "paginated_members_total_items": len(_get_main_df()), + "paginated_members_page_size": 2, "story": { "text": _get_story_text(), # For display }, diff --git a/apps/hello/ui.json b/apps/hello/ui.json index 6c184f207..b67253f77 100644 --- a/apps/hello/ui.json +++ b/apps/hello/ui.json @@ -270,7 +270,7 @@ "width": "1", "isCollapsible": "", "startCollapsed": "", - "horizontalAlignment": "center" + "contentHAlign": "center" }, "parentId": "fb22acfc-cdb5-44b6-9e97-76c3a51a8fff", "position": 2 @@ -351,7 +351,7 @@ "name": "Timer" }, "parentId": "ee919cd6-8153-4f34-8c6a-bfc1153df360", - "position": 3 + "position": 4 }, "db4c66d6-1eb7-44d3-a2d4-65d0b3e5cf12": { "id": "db4c66d6-1eb7-44d3-a2d4-65d0b3e5cf12", @@ -367,7 +367,7 @@ "id": "09ddb2da-6fa3-4157-8da3-4d5d44a6a58d", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "85120b55-69c6-4b50-853a-bbbf73ff8121", "position": 0 @@ -591,7 +591,7 @@ "content": { "width": "1", "verticalAlignment": "", - "horizontalAlignment": "" + "contentHAlign": "" }, "parentId": "b9cb10e5-1ead-448b-afcc-909e23afb72a", "position": 0 @@ -843,7 +843,7 @@ "id": "9bb8a686-7013-4af7-a89e-d89c7754120d", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "771dc336-69b2-400e-9ea3-e881e2332c9d", "position": 2, @@ -911,6 +911,70 @@ "click": "handle_story_download" }, "visible": true + }, + "e1ax8ctt8lrao0e4": { + "id": "e1ax8ctt8lrao0e4", + "type": "tab", + "content": { + "name": "Pagination" + }, + "parentId": "ee919cd6-8153-4f34-8c6a-bfc1153df360", + "position": 3, + "handlers": {}, + "visible": true + }, + "j3jkho6tb97u0onr": { + "id": "j3jkho6tb97u0onr", + "type": "repeater", + "content": { + "keyVariable": "itemId", + "valueVariable": "item", + "repeaterObject": "@{paginated_members}" + }, + "parentId": "e1ax8ctt8lrao0e4", + "position": 0, + "handlers": {}, + "visible": true + }, + "4wzaubf275w17gac": { + "id": "4wzaubf275w17gac", + "type": "section", + "content": { + "title": "@{item.name} \u2b50\ufe0f" + }, + "parentId": "j3jkho6tb97u0onr", + "position": 0, + "handlers": {}, + "visible": true + }, + "19binb4yi70gesho": { + "id": "19binb4yi70gesho", + "type": "text", + "content": { + "text": "**Role:** @{item.role}\n", + "useMarkdown": "yes" + }, + "parentId": "4wzaubf275w17gac", + "position": 0 + }, + "zfp1koasiuleygmz": { + "id": "zfp1koasiuleygmz", + "type": "pagination", + "content": { + "page": "@{paginated_members_page}", + "pageSize": "@{paginated_members_page_size}", + "totalItems": "@{paginated_members_total_items}", + "pageSizeOptions": "1,2,5", + "pageSizeShowAll": "no", + "jumpTo": "no" + }, + "parentId": "e1ax8ctt8lrao0e4", + "position": 1, + "handlers": { + "ss-change-page": "handle_paginated_members_page_change", + "ss-change-page-size": "handle_paginated_members_page_size_change" + }, + "visible": true } } } \ No newline at end of file diff --git a/docs/docs/frontend-scripts.md b/docs/docs/frontend-scripts.md index 7ca609e8e..425f9981f 100644 --- a/docs/docs/frontend-scripts.md +++ b/docs/docs/frontend-scripts.md @@ -71,6 +71,20 @@ initial_state.import_script("my_script", "/static/script.js") Importing scripts is useful to import libraries that don't support ES6 modules. When possible, use ES6 modules. The `import_script` syntax is only used for side effects; you'll only be able to call functions from the backend using modules that have been previously imported via `import_frontend_module`. ::: +## Importing a script or stylesheet from a URL + +Streamsync can also import scripts and stylesheets from URLs. This is useful for importing libraries from CDNs. The `import_script` and `import_stylesheet` methods take a `url` argument, which is the URL to the script or stylesheet. + +```python +initial_state = ss.init_state({ + "my_app": { + "title": "My App" + }, +}) + +initial_state.import_script("lodash", "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.js") +``` + ## Frontend core You can access Streamsync's frontend core via `globalThis.core`, unlocking all sorts of functionality. Notably, you can use `getUserState()` to get values from state. diff --git a/src/streamsync/__init__.py b/src/streamsync/__init__.py index 71eb1a9aa..d982e2a37 100644 --- a/src/streamsync/__init__.py +++ b/src/streamsync/__init__.py @@ -2,7 +2,7 @@ from streamsync.core import Readable, FileWrapper, BytesWrapper, Config from streamsync.core import initial_state, component_manager, session_manager, session_verifier -VERSION = "0.2.8" +VERSION = "0.3.0" component_manager session_manager diff --git a/src/streamsync/app_runner.py b/src/streamsync/app_runner.py index 2d43c9be0..af2d3aa5b 100644 --- a/src/streamsync/app_runner.py +++ b/src/streamsync/app_runner.py @@ -25,6 +25,8 @@ import watchdog.events from streamsync import VERSION +logging.basicConfig(level=logging.INFO, format='%(message)s') + class MessageHandlingException(Exception): pass @@ -631,7 +633,7 @@ async def dispatch_message(self, session_id: Optional[str], request: AppProcessS def _load_persisted_script(self) -> str: try: contents = None - with open(os.path.join(self.app_path, "main.py"), "r") as f: + with open(os.path.join(self.app_path, "main.py"), "r", encoding='utf-8') as f: contents = f.read() return contents except FileNotFoundError: @@ -799,4 +801,4 @@ async def notify_of_code_update(self): try: self.code_update_condition.notify_all() finally: - self.code_update_condition.release() \ No newline at end of file + self.code_update_condition.release() diff --git a/src/streamsync/app_templates/hello/main.py b/src/streamsync/app_templates/hello/main.py index 4d582a8c8..fb11524dc 100644 --- a/src/streamsync/app_templates/hello/main.py +++ b/src/streamsync/app_templates/hello/main.py @@ -20,7 +20,10 @@ def update(state, session): main_df = _get_main_df() main_df = main_df[main_df['length_cm'] >= state["filter"]["min_length"]] main_df = main_df[main_df['weight_g'] >= state["filter"]["min_weight"]] - state["main_df"] = main_df + state['main_df'] = main_df + + paginated_members = _get_paginated_members(state['paginated_members_page'] - 1, state['paginated_members_page_size']) + state['paginated_members'] = paginated_members state["session"] = session _update_metrics(state) _update_role_chart(state) @@ -33,6 +36,22 @@ def handle_story_download(state): state.file_download(data, file_name) +def handle_paginated_members_page_change(state, payload): + page = payload + maxpage = int(state["paginated_members_total_items"] / state["paginated_members_page_size"]) + 1 + if page > maxpage: + state["paginated_members_page"] = maxpage - 2 + else: + state["paginated_members_page"] = page + + update(state, None) + + +def handle_paginated_members_page_size_change(state, payload): + state['paginated_members_page_size'] = payload + update(state, None) + + # LOAD / GENERATE DATA @@ -47,12 +66,15 @@ def _get_main_df(): main_df = pd.read_csv("assets/main_df.csv") return main_df - def _get_highlighted_members(): sample_df = _get_main_df().sample(3).set_index("name", drop=False) sample = sample_df.to_dict("index") return sample +def _get_paginated_members(offset: int, limit: int): + paginated_df = _get_main_df()[offset:offset + limit].set_index("name", drop=False) + paginated = paginated_df.to_dict("index") + return paginated def _get_story_text(): with open("assets/story.txt", "r") as f: @@ -117,6 +139,10 @@ def _update_scatter_chart(state): "highlighted_members": _get_highlighted_members(), "random_df": _generate_random_df(), "hue_rotation": 26, + "paginated_members": _get_paginated_members(0, 2), + "paginated_members_page": 1, + "paginated_members_total_items": len(_get_main_df()), + "paginated_members_page_size": 2, "story": { "text": _get_story_text(), # For display }, diff --git a/src/streamsync/app_templates/hello/ui.json b/src/streamsync/app_templates/hello/ui.json index 6c184f207..f68ce177e 100644 --- a/src/streamsync/app_templates/hello/ui.json +++ b/src/streamsync/app_templates/hello/ui.json @@ -270,7 +270,7 @@ "width": "1", "isCollapsible": "", "startCollapsed": "", - "horizontalAlignment": "center" + "contentHAlign": "center" }, "parentId": "fb22acfc-cdb5-44b6-9e97-76c3a51a8fff", "position": 2 @@ -351,7 +351,7 @@ "name": "Timer" }, "parentId": "ee919cd6-8153-4f34-8c6a-bfc1153df360", - "position": 3 + "position": 4 }, "db4c66d6-1eb7-44d3-a2d4-65d0b3e5cf12": { "id": "db4c66d6-1eb7-44d3-a2d4-65d0b3e5cf12", @@ -367,7 +367,7 @@ "id": "09ddb2da-6fa3-4157-8da3-4d5d44a6a58d", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "85120b55-69c6-4b50-853a-bbbf73ff8121", "position": 0 @@ -591,7 +591,7 @@ "content": { "width": "1", "verticalAlignment": "", - "horizontalAlignment": "" + "contentHAlign": "" }, "parentId": "b9cb10e5-1ead-448b-afcc-909e23afb72a", "position": 0 @@ -843,7 +843,7 @@ "id": "9bb8a686-7013-4af7-a89e-d89c7754120d", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "771dc336-69b2-400e-9ea3-e881e2332c9d", "position": 2, @@ -911,6 +911,71 @@ "click": "handle_story_download" }, "visible": true + }, + "e1ax8ctt8lrao0e4": { + "id": "e1ax8ctt8lrao0e4", + "type": "tab", + "content": { + "name": "Pagination" + }, + "parentId": "ee919cd6-8153-4f34-8c6a-bfc1153df360", + "position": 3, + "handlers": {}, + "visible": true + }, + "j3jkho6tb97u0onr": { + "id": "j3jkho6tb97u0onr", + "type": "repeater", + "content": { + "keyVariable": "itemId", + "valueVariable": "item", + "repeaterObject": "@{paginated_members}" + }, + "parentId": "e1ax8ctt8lrao0e4", + "position": 0, + "handlers": {}, + "visible": true + }, + "4wzaubf275w17gac": { + "id": "4wzaubf275w17gac", + "type": "section", + "content": { + "title": "@{item.name} \u2b50\ufe0f" + }, + "parentId": "j3jkho6tb97u0onr", + "position": 0, + "handlers": {}, + "visible": true + }, + "19binb4yi70gesho": { + "id": "19binb4yi70gesho", + "type": "text", + "content": { + "text": "**Role:** @{item.role}\n", + "useMarkdown": "yes" + }, + "parentId": "4wzaubf275w17gac", + "position": 0 + }, + "zfp1koasiuleygmz": { + "id": "zfp1koasiuleygmz", + "type": "pagination", + "content": { + "page": "@{paginated_members_page}", + "pageSize": "@{paginated_members_page_size}", + "totalItems": "@{paginated_members_total_items}", + "pageSizeOptions": "1,2,5", + "pageSizeShowAll": "no", + "jumpTo": "no", + "urlParam": "no" + }, + "parentId": "e1ax8ctt8lrao0e4", + "position": 1, + "handlers": { + "ss-change-page": "handle_paginated_members_page_change", + "ss-change-page-size": "handle_paginated_members_page_size_change" + }, + "visible": true } } } \ No newline at end of file diff --git a/src/streamsync/core.py b/src/streamsync/core.py index 4e6b20b76..2c1353915 100644 --- a/src/streamsync/core.py +++ b/src/streamsync/core.py @@ -120,6 +120,8 @@ def serialise(self, v: Any) -> Union[Dict, List, str, bool, int, float, None]: if "matplotlib.figure.Figure" in v_mro: return self._serialise_matplotlib_fig(v) + if "plotly.graph_objs._figure.Figure" in v_mro: + return v.to_json() if "numpy.float64" in v_mro: return float(v) if "numpy.ndarray" in v_mro: @@ -229,7 +231,7 @@ def apply(self, key) -> None: def get_mutations_as_dict(self) -> Dict[str, Any]: serialised_mutations: Dict[str, Union[Dict, List, str, bool, int, float, None]] = {} - for key, value in self.state.items(): + for key, value in list(self.state.items()): if key.startswith("_"): continue escaped_key = key.replace(".", "\.") @@ -237,7 +239,7 @@ def get_mutations_as_dict(self) -> Dict[str, Any]: serialised_value = None if isinstance(value, StateProxy): if value.initial_assignment: - serialised_mutations[key] = serialised_value + serialised_mutations[escaped_key] = serialised_value value.initial_assignment = False child_mutations = value.get_mutations_as_dict() if child_mutations is None: @@ -330,19 +332,38 @@ def add_notification(self, type: Literal["info", "success", "warning", "error"], "message": message, }) - def _log_entry_in_logger(self, type: Literal["info", "error"], title: str, message: str, code: Optional[str] = None) -> None: + def _log_entry_in_logger(self, type: Literal["debug", "info", "warning", "error", "critical"], title: str, message: str, code: Optional[str] = None) -> None: if not Config.logger: return log_args: Tuple[str, ...] = () + if code: log_args = (title, message, code) else: log_args = (title, message) + + log_colors = { + "debug": "\x1b[36;20m", # Cyan for debug + "info": "\x1b[34;20m", # Blue for info + "warning": "\x1b[33;20m", # Yellow for warning + "error": "\x1b[31;20m", # Red for error + "critical": "\x1b[35;20m" # Magenta for critical + } + + log_methods = { + "debug": Config.logger.debug, + "info": Config.logger.info, + "warning": Config.logger.warning, + "error": Config.logger.error, + "critical": Config.logger.critical + } + log_message = "From app log: " + ("\n%s" * len(log_args)) - if type == "info": - Config.logger.info(f"\x1b[34;20m{log_message}\x1b[0m", *log_args) - elif type == "error": - Config.logger.error(f"\x1b[31;20m{log_message}\x1b[0m", *log_args) + + color = log_colors.get(type, "\x1b[0m") # Default to no color if type not found + log_method = log_methods.get(type, Config.logger.info) # Default to info level if type not found + + log_method(f"{color}{log_message}\x1b[0m", *log_args) def add_log_entry(self, type: Literal["info", "error"], title: str, message: str, code: Optional[str] = None) -> None: self._log_entry_in_logger(type, title, message, code) @@ -643,6 +664,19 @@ def _transform_date_change(self, ev) -> str: return payload + def _transform_change_page_size(self, ev) -> Optional[int]: + try: + return int(ev.payload) + except ValueError: + return None + + def _transform_change_page(self, ev) -> Optional[int]: + try: + return int(ev.payload) + except ValueError: + return None + + class Evaluator: diff --git a/src/streamsync/serve.py b/src/streamsync/serve.py index 0bbe480e6..0bedcd9e4 100644 --- a/src/streamsync/serve.py +++ b/src/streamsync/serve.py @@ -2,6 +2,7 @@ import mimetypes from contextlib import asynccontextmanager import sys +import textwrap from typing import Any, Callable, Dict, List, Optional, Set, Union import typing from fastapi import FastAPI, Request, HTTPException @@ -41,7 +42,9 @@ async def lifespan(app: FastAPI): app_runner.hook_to_running_event_loop() app_runner.load() - if on_load is not None: + if on_load is not None \ + and hasattr(app.state, 'is_server_static_mounted') \ + and app.state.is_server_static_mounted: on_load() try: @@ -359,8 +362,23 @@ async def stream(websocket: WebSocket): server_path = os.path.dirname(__file__) server_static_path = pathlib.Path(server_path) / "static" - asgi_app.mount( - "/", StaticFiles(directory=str(server_static_path), html=True), name="server_static") + if server_static_path.exists(): + asgi_app.mount( + "/", StaticFiles(directory=str(server_static_path), html=True), name="server_static") + asgi_app.state.is_server_static_mounted = True + else: + logging.error( + textwrap.dedent( + """\ + \x1b[31;20mError: Failed to acquire server static path. Streamsync may not be properly built. + + To resolve this issue, try the following steps: + 1. Run the 'npm run build' script in the 'ui' directory and then restart the app. + 2. Alternatively, launch a UI instance by running 'npm run dev' in the 'ui' directory. + + Please refer to the CONTRIBUTING.md for detailed instructions.\x1b[0m""" + ) + ) # Return diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..3892a47f8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +test_app_dir = Path(__file__).resolve().parent / 'testapp' diff --git a/tests/test_app_runner.py b/tests/test_app_runner.py index 6bb7d4d9d..39b9d3a2b 100644 --- a/tests/test_app_runner.py +++ b/tests/test_app_runner.py @@ -7,6 +7,8 @@ from streamsync.ss_types import EventRequest, InitSessionRequest, InitSessionRequestPayload, StreamsyncEvent import asyncio +from tests import test_app_dir + class TestAppRunner: numberinput_instance_path = [ @@ -26,11 +28,11 @@ def test_init_wrong_path(self) -> None: def test_init_wrong_mode(self) -> None: with pytest.raises(ValueError): - AppRunner("./testapp", "virus") + AppRunner(test_app_dir, "virus") @pytest.mark.asyncio async def test_pre_session(self) -> None: - ar = AppRunner("./testapp", "run") + ar = AppRunner(test_app_dir, "run") er = EventRequest( type="event", payload=StreamsyncEvent( @@ -48,7 +50,7 @@ async def test_pre_session(self) -> None: @pytest.mark.asyncio async def test_valid_session_invalid_event(self) -> None: - ar = AppRunner("./testapp", "run") + ar = AppRunner(test_app_dir, "run") ar.load() si = InitSessionRequest( type="sessionInit", @@ -74,7 +76,7 @@ async def test_valid_session_invalid_event(self) -> None: @pytest.mark.asyncio async def test_valid_event(self) -> None: - ar = AppRunner("./testapp", "run") + ar = AppRunner(test_app_dir, "run") ar.load() si = InitSessionRequest( type="sessionInit", @@ -103,7 +105,7 @@ async def test_valid_event(self) -> None: @pytest.mark.asyncio async def test_bad_event_handler(self) -> None: - ar = AppRunner("./testapp", "run") + ar = AppRunner(test_app_dir, "run") ar.load() si = InitSessionRequest( type="sessionInit", @@ -131,7 +133,7 @@ async def test_bad_event_handler(self) -> None: ar.shut_down() def test_run_code_edit(self) -> None: - ar = AppRunner("./testapp", "run") + ar = AppRunner(test_app_dir, "run") with pytest.raises(PermissionError): ar.update_code(None, "exec(virus)") with pytest.raises(PermissionError): @@ -151,7 +153,7 @@ async def wait_for_code_update(self, app_runner: AppRunner) -> None: @pytest.mark.asyncio async def test_code_update(self) -> None: - ar = AppRunner("./testapp", "edit") + ar = AppRunner(test_app_dir, "edit") ar.hook_to_running_event_loop() ar.load() wait_update_task = asyncio.create_task(self.wait_for_code_update(ar)) diff --git a/tests/test_core.py b/tests/test_core.py index ce60c4cb7..33fd53845 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,11 +8,15 @@ import streamsync as ss from streamsync.ss_types import StreamsyncEvent import pandas as pd +import plotly.express as px import pytest import altair import pyarrow as pa import urllib +from pathlib import Path +from tests import test_app_dir + raw_state_dict = { "name": "Robert", "age": 1, @@ -33,7 +37,7 @@ } sc = None -with open("testapp/ui.json", "r") as f: +with open(test_app_dir / "ui.json", "r") as f: sc = json.load(f).get("components") ss.Config.is_mail_enabled_for_log = True @@ -364,7 +368,7 @@ def test_date_change(self) -> None: class TestFileWrapper(): - file_path = "testapp/assets/myfile.csv" + file_path = str(test_app_dir / "assets/myfile.csv") def test_get_as_dataurl(self) -> None: fw = FileWrapper(self.file_path, "text/plain") @@ -381,8 +385,8 @@ def test_get_as_dataurl(self) -> None: class TestStateSerialiser(): sts = StateSerialiser() - file_path = "testapp/assets/myfile.csv" - df_path = "testapp/assets/main_df.csv" + file_path = str(test_app_dir / "assets/myfile.csv") + df_path = str(test_app_dir / "assets/main_df.csv") def test_nested_dict(self) -> None: d = { @@ -487,6 +491,26 @@ def test_unserialisable_altair(self) -> None: with pytest.warns(UserWarning): with pytest.raises(ValueError): self.sts.serialise(d) + + def test_plotly_should_be_serialize_to_json(self) -> None: + """ + Test that plotly figure should be serialised to json string directly. Serializing the json directly allows you + to display datasets that exceed 10,000 records. + + With the default json serializer, a dataset like this blows up memory. Plotly is using internaly orjson as serializer. + """ + # Arrange + df = px.data.iris() + fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species", symbol="species") + + # Acts + json_code = self.sts.serialise(fig) + + # Assert + assert isinstance(json_code, str) + o = json.loads(json_code) + assert 'data' in o + assert 'layout' in o def test_pandas_df(self) -> None: d = { diff --git a/tests/test_serve.py b/tests/test_serve.py index 1f5f528b6..9665590ff 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -6,12 +6,14 @@ import fastapi.testclient import pytest +from tests import test_app_dir + class TestServe: def test_valid(self) -> None: asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app( - "./testapp", "run") + test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app) as client: res = client.post("/api/init", json={ "proposedSessionId": None @@ -46,7 +48,7 @@ def test_valid(self) -> None: def test_bad_session(self) -> None: asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app( - "./testapp", "run") + test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app) as client: with client.websocket_connect("/api/stream") as websocket: websocket.send_json({ @@ -61,7 +63,7 @@ def test_bad_session(self) -> None: def test_session_verifier_header(self) -> None: asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app( - "./testapp", "run") + test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app) as client: res = client.post("/api/init", json={ "proposedSessionId": None @@ -73,7 +75,7 @@ def test_session_verifier_header(self) -> None: def test_session_verifier_cookies(self) -> None: asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app( - "./testapp", "run") + test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app, cookies={ "fail_cookie": "yes" }) as client: @@ -86,7 +88,7 @@ def test_session_verifier_cookies(self) -> None: def test_session_verifier_pass(self) -> None: asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app( - "./testapp", "run") + test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app, cookies={ "another_cookie": "yes" }) as client: @@ -102,7 +104,7 @@ def test_serve_javascript_file_with_a_valid_content_type(self) -> None: # Arrange mimetypes.add_type("text/plain", ".js") - asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app("./testapp", "run") + asgi_app: fastapi.FastAPI = streamsync.serve.get_asgi_app(test_app_dir, "run") with fastapi.testclient.TestClient(asgi_app) as client: # Acts res = client.get("/static/file.js") diff --git a/tests/testapp/main.py b/tests/testapp/main.py index 2567ac5b0..4efd171b4 100644 --- a/tests/testapp/main.py +++ b/tests/testapp/main.py @@ -217,7 +217,7 @@ def _get_altair_chart(): "b": { "pet_count": 8 }, - "utfࠀ": 23, + "utfࠀ": "ثعلب كلب", "prog_languages": { "c": {"name": "C"}, "ts": {"name": "TypeScript"}, diff --git a/tests/testapp/ui.json b/tests/testapp/ui.json index 35736852c..11d36bee7 100644 --- a/tests/testapp/ui.json +++ b/tests/testapp/ui.json @@ -1,6 +1,6 @@ { "metadata": { - "streamsync_version": "0.2.5" + "streamsync_version": "0.3.0a1" }, "components": { "root": { @@ -64,8 +64,7 @@ "id": "9c30af6d-4ee5-4782-9169-0f361d67fa76", "type": "section", "content": { - "title": "", - "snapMode": "" + "title": "" }, "parentId": "d1e01ce1-fab1-4a6e-91a1-1f45f9e57aa5", "position": 0 @@ -137,8 +136,7 @@ "id": "7e625201-20c2-4b05-951c-d825de28b216", "type": "section", "content": { - "title": "Filter data", - "snapMode": "no" + "title": "Filter data" }, "parentId": "fbad9feb-5c88-4425-bb17-0d138286a875", "position": 0 @@ -185,8 +183,7 @@ "id": "70d82458-a08f-4005-8f96-dc8d3ba92fad", "type": "section", "content": { - "title": "About this app", - "snapMode": "no" + "title": "About this app" }, "parentId": "fbad9feb-5c88-4425-bb17-0d138286a875", "position": 1 @@ -264,7 +261,7 @@ "type": "column", "content": { "width": "1", - "verticalAlignment": "" + "contentVAlign": "" }, "parentId": "fb22acfc-cdb5-44b6-9e97-76c3a51a8fff", "position": 0 @@ -337,8 +334,7 @@ "type": "section", "content": { "title": "", - "containerBackgroundColor": "#ebfcff", - "snapMode": "no" + "containerBackgroundColor": "#ebfcff" }, "parentId": "3cc9c5e9-6c77-401d-ab82-7805d9df760c", "position": 1 @@ -347,7 +343,7 @@ "id": "919b0d26-ea9b-4364-b5af-865236b3fc3a", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "ec5bc32e-1456-4abd-8d3e-97c640e32339", "position": 0 @@ -402,7 +398,7 @@ "id": "09ddb2da-6fa3-4157-8da3-4d5d44a6a58d", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "85120b55-69c6-4b50-853a-bbbf73ff8121", "position": 0 @@ -580,7 +576,6 @@ "type": "section", "content": { "title": "@{item.name}", - "snapMode": "no", "containerBackgroundColor": "#eff4f6" }, "parentId": "0dd29423-3867-478a-997e-eeaafb6b811e", @@ -657,8 +652,8 @@ "content": { "width": "1", "not-a-real-field": "not-a-real-value", - "verticalAlignment": "", - "horizontalAlignment": "" + "contentHAlign": "", + "contentVAlign": "" }, "parentId": "b9cb10e5-1ead-448b-afcc-909e23afb72a", "position": 0 @@ -1025,8 +1020,7 @@ "id": "2ab19af7-1efa-4012-af35-f01c3d39a409", "type": "section", "content": { - "title": "Payload", - "snapMode": "no" + "title": "Payload" }, "parentId": "7730df5b-8731-4123-bacc-898e7347b124", "position": 3, @@ -1079,8 +1073,7 @@ "id": "3eb28922-ef5c-47de-88aa-d100c503a2f5", "type": "section", "content": { - "title": "Bindings", - "snapMode": "no" + "title": "Bindings" }, "parentId": "7730df5b-8731-4123-bacc-898e7347b124", "position": 5, @@ -1338,8 +1331,7 @@ "id": "ca8f9355-c26b-44a6-85c1-46d2182a576e", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "c55caf0b-39c1-4fe5-8756-7506b3e3a19a", "position": 0, @@ -1361,8 +1353,7 @@ "id": "ee91f05a-6cac-4cf0-96dd-090a7bf09bd6", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "d4d9fd0d-b347-42d9-9786-a9873cdd0912", "position": 0, @@ -1404,8 +1395,7 @@ "id": "04acdbfb-2e72-45b3-9fad-367cda5164b5", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "c937bbfd-a510-432c-8e58-c50eecd5c67a", "position": 0, @@ -1458,8 +1448,7 @@ "id": "c72a3364-03d9-4fb1-ac1f-13a1785e18c0", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "ad8bd11b-3f89-4839-b2d3-4c7f52c267bd", "position": 0, @@ -1470,8 +1459,7 @@ "id": "065e4f07-c707-4cc7-8cbe-143be5ab2486", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "e702f506-8ec1-495e-9d8e-e488120b9a7b", "position": 0, @@ -1482,8 +1470,7 @@ "id": "511bedb8-f856-451f-8ee4-a123d934b744", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "065e4f07-c707-4cc7-8cbe-143be5ab2486", "position": 0, @@ -1494,8 +1481,7 @@ "id": "5ab397b2-4283-44ab-ba9f-4cde3b422641", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "e702f506-8ec1-495e-9d8e-e488120b9a7b", "position": 1, @@ -1506,7 +1492,7 @@ "id": "ba15571d-4baa-4c65-b600-45af1c32ca75", "type": "horizontalstack", "content": { - "alignment": "left" + "contentHAlign": "start" }, "parentId": "88ea37a5-eb07-4740-ae42-a3eeeacca310", "position": 4, @@ -1594,8 +1580,7 @@ "id": "1b3c8d83-36c1-4c74-9dcd-110160c16081", "type": "section", "content": { - "title": "iframe", - "snapMode": "no" + "title": "iframe" }, "parentId": "f2c445a4-9d3c-4c38-9fe3-cda02126d5d0", "position": 0, @@ -1870,8 +1855,7 @@ "id": "9f589947-0ea9-4a70-9d57-4ae52543be42", "type": "section", "content": { - "title": "", - "snapMode": "no" + "title": "" }, "parentId": "d15b9cf1-7e79-4ee5-9f0d-7c38c6b6c070", "position": 0, @@ -2098,8 +2082,7 @@ "id": "575542f1-bb18-429c-9d6a-706edc7512be", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "35986d56-3a1a-4ded-bb5c-b60c2046756f", "position": 7, @@ -2119,8 +2102,7 @@ "id": "35f579dd-4205-413a-9c4f-1caf015a598a", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 1, @@ -2131,8 +2113,7 @@ "id": "4c65268c-b536-4671-8081-c69be3e84161", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 2, @@ -2143,8 +2124,7 @@ "id": "436ba1be-1194-4da9-9eb3-a10e9ec019f1", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 5, @@ -2155,8 +2135,7 @@ "id": "7a89f965-8c1d-473c-a565-947be62faa43", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 45, @@ -2167,8 +2146,7 @@ "id": "e2d3c1be-c43e-4b8a-84a8-90649da0b55e", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 6, @@ -2179,8 +2157,7 @@ "id": "ddbedb50-5fbe-43a3-b221-8f7d59c617da", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 7, @@ -2191,8 +2168,7 @@ "id": "21dda1e9-a972-4f74-972d-d13f5e2941de", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 8, @@ -2203,8 +2179,7 @@ "id": "6ee3de2e-7333-469f-9ced-1e08d9231117", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 9, @@ -2215,8 +2190,7 @@ "id": "f0659dad-aa37-457e-80fe-02b900f38694", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 10, @@ -2227,8 +2201,7 @@ "id": "380f8a0b-38f3-414a-ab9f-7034629f5e8e", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 11, @@ -2239,8 +2212,7 @@ "id": "e43471a7-2e58-437f-8daf-489b221b464a", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 12, @@ -2262,8 +2234,7 @@ "id": "71500567-d3db-4d67-89aa-86ea8fecc4ed", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 14, @@ -2274,8 +2245,7 @@ "id": "0e8d0885-954a-4b95-9cfd-0a8ed2ed8683", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 15, @@ -2286,8 +2256,7 @@ "id": "a039b488-a523-42bc-84d0-f9b3f5185052", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 16, @@ -2298,8 +2267,7 @@ "id": "8582c124-474c-4da5-80c5-16bca90f4d98", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 17, @@ -2310,8 +2278,7 @@ "id": "d88e0ff3-4acb-474c-b37c-9cd7ea51fd26", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 18, @@ -2322,8 +2289,7 @@ "id": "de7a750f-6c88-4fbb-8962-93714b6be485", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 19, @@ -2334,8 +2300,7 @@ "id": "110625bc-bad4-4b44-b20f-51339554d77b", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 20, @@ -2346,8 +2311,7 @@ "id": "eb568d28-e6de-4deb-978b-a10d6cb3c454", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 21, @@ -2358,8 +2322,7 @@ "id": "4a94898f-01b2-4424-8db9-9ed75e788611", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 22, @@ -2370,8 +2333,7 @@ "id": "f8136c45-f868-4858-928a-f007f9a7dfbe", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 23, @@ -2382,8 +2344,7 @@ "id": "1a8ca9b4-afc5-4055-9709-bd27cd7fd7aa", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 24, @@ -2394,8 +2355,7 @@ "id": "6a308e3d-c5a0-400e-9036-6183aecac714", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 25, @@ -2406,8 +2366,7 @@ "id": "a71a65fc-8710-40fb-8410-6c676072dab5", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 26, @@ -2418,8 +2377,7 @@ "id": "ff213e0d-4e33-494d-8765-6738010ea932", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 27, @@ -2430,8 +2388,7 @@ "id": "25f8e06e-5a86-4e5c-afbc-6191c4146bd8", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 28, @@ -2442,8 +2399,7 @@ "id": "48cb52fa-bb6a-43e7-aa7a-cbc0b30156b1", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 29, @@ -2454,8 +2410,7 @@ "id": "7d914a22-e7aa-4340-9b88-36423a5566a3", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 30, @@ -2466,8 +2421,7 @@ "id": "bcbae34f-72fd-40b3-8678-d4b986b47cd5", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 31, @@ -2478,8 +2432,7 @@ "id": "574ec17c-ed69-49a6-b991-29354b8a8a76", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 32, @@ -2490,8 +2443,7 @@ "id": "27a21b92-0ed0-4044-8eb4-69979a285e2a", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 33, @@ -2502,8 +2454,7 @@ "id": "c759135c-72ed-43b8-ab91-8bce748dcd58", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 35, @@ -2514,8 +2465,7 @@ "id": "210f45a4-c8da-48b2-a2ff-bca06ca7f8a3", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 36, @@ -2526,8 +2476,7 @@ "id": "0bdf78ac-2824-4116-8e14-af2c363c3b45", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 37, @@ -2538,8 +2487,7 @@ "id": "a4a6316d-5fb1-47e2-976b-97265533fc97", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 38, @@ -2550,8 +2498,7 @@ "id": "aa8ab024-25a5-4d33-82cf-f20146e9cf28", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "4c65268c-b536-4671-8081-c69be3e84161", "position": 0, @@ -2562,8 +2509,7 @@ "id": "1c5b7db1-f8fa-4c7b-9daf-db60045fd16e", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 39, @@ -2585,8 +2531,7 @@ "id": "857dc6fc-d6f7-4dd1-bbc3-6f0dea011d54", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 42, @@ -2597,8 +2542,7 @@ "id": "aac79665-3341-4784-a105-433baf45dde3", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 43, @@ -2609,8 +2553,7 @@ "id": "d0f010ac-2048-49f3-91ff-e17e145298ed", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 44, @@ -2621,8 +2564,7 @@ "id": "b3aa5f2f-d952-4021-a506-7a4515388580", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 46, @@ -2633,8 +2575,7 @@ "id": "d07b8382-dca7-4061-b0ca-22edd43e06b1", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 47, @@ -2645,8 +2586,7 @@ "id": "9005a1fc-c560-4ca4-9626-b660031c9c8e", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 48, @@ -2657,8 +2597,7 @@ "id": "440de4a5-b901-4626-a282-3aee83a007c5", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 49, @@ -2669,8 +2608,7 @@ "id": "2e8069dd-0017-42f3-a13b-44e19f132c4f", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 50, @@ -2681,8 +2619,7 @@ "id": "cab6894d-98e1-426f-876c-ca1c81bb7a30", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 51, @@ -2693,8 +2630,7 @@ "id": "8a937df7-c974-455e-b2bc-40445621ef40", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 52, @@ -2792,8 +2728,7 @@ "id": "1faaa29d-96d3-404a-b8f5-7d392cd55177", "type": "section", "content": { - "title": "Section Title", - "snapMode": "no" + "title": "Section Title" }, "parentId": "aade8074-13be-4e11-a405-a71b8138e6ae", "position": 40, @@ -2874,33 +2809,33 @@ "c9e4e0d9-771e-47c2-863a-55ce8a98bfa5": { "id": "c9e4e0d9-771e-47c2-863a-55ce8a98bfa5", "type": "multiselectinput", - "parentId": "6010765e-9ac3-4570-84bf-913ae404e03a", "content": { "label": "Default checkbox mirrored in multiselect" }, - "handlers": {}, + "parentId": "6010765e-9ac3-4570-84bf-913ae404e03a", "position": 6, - "visible": true, + "handlers": {}, "binding": { "eventType": "ss-options-change", "stateRef": "b.default_checkbox" - } + }, + "visible": true }, "c1d1f478-3dbb-4009-94a0-9b92404348cd": { "id": "c1d1f478-3dbb-4009-94a0-9b92404348cd", "type": "selectinput", - "parentId": "6010765e-9ac3-4570-84bf-913ae404e03a", "content": { "label": "Language", "options": "{\n \"en\": \"English\",\n \"sp\": \"Spanish\",\n \"pl\": \"Polish\"\n}" }, - "handlers": {}, + "parentId": "6010765e-9ac3-4570-84bf-913ae404e03a", "position": 4, - "visible": true, + "handlers": {}, "binding": { "eventType": "ss-option-change", "stateRef": "b.language" - } + }, + "visible": true } } } \ No newline at end of file diff --git a/ui/src/assets/padding-4-side.svg b/ui/src/assets/padding-4-side.svg new file mode 100644 index 000000000..5e9978938 --- /dev/null +++ b/ui/src/assets/padding-4-side.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/padding-bottom-side.svg b/ui/src/assets/padding-bottom-side.svg new file mode 100644 index 000000000..ea2213d05 --- /dev/null +++ b/ui/src/assets/padding-bottom-side.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/padding-left-side.svg b/ui/src/assets/padding-left-side.svg new file mode 100644 index 000000000..77fd70dab --- /dev/null +++ b/ui/src/assets/padding-left-side.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/padding-right-side.svg b/ui/src/assets/padding-right-side.svg new file mode 100644 index 000000000..a3893d6b7 --- /dev/null +++ b/ui/src/assets/padding-right-side.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/padding-top-side.svg b/ui/src/assets/padding-top-side.svg new file mode 100644 index 000000000..a02038512 --- /dev/null +++ b/ui/src/assets/padding-top-side.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/assets/padding-x-side.svg b/ui/src/assets/padding-x-side.svg new file mode 100644 index 000000000..855629910 --- /dev/null +++ b/ui/src/assets/padding-x-side.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/assets/padding-y-side.svg b/ui/src/assets/padding-y-side.svg new file mode 100644 index 000000000..19f6981e9 --- /dev/null +++ b/ui/src/assets/padding-y-side.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/builder/BuilderFieldsAlign.vue b/ui/src/builder/BuilderFieldsAlign.vue new file mode 100644 index 000000000..6b21edd45 --- /dev/null +++ b/ui/src/builder/BuilderFieldsAlign.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/ui/src/builder/BuilderFieldsPadding.vue b/ui/src/builder/BuilderFieldsPadding.vue new file mode 100644 index 000000000..51885c197 --- /dev/null +++ b/ui/src/builder/BuilderFieldsPadding.vue @@ -0,0 +1,355 @@ + + + + + diff --git a/ui/src/builder/BuilderSelect.vue b/ui/src/builder/BuilderSelect.vue new file mode 100644 index 000000000..22d6db290 --- /dev/null +++ b/ui/src/builder/BuilderSelect.vue @@ -0,0 +1,207 @@ +Usage: + +```js +// icon is using https://remixicon.com +const options = [ + { value: "auto", label: "Default" }, + { value: "fit-content", label: "Fit content", icon: "ri-split-cells-horizontal" }, +]; + +const select = (value: string) => { + console.log(value); +}; +``` + +``` + +``` + +``` + +``` + +You can specify a default icon from https://remixicon.com + +``` + +``` + + + + + + \ No newline at end of file diff --git a/ui/src/builder/BuilderSettingsProperties.vue b/ui/src/builder/BuilderSettingsProperties.vue index a12352e10..f952584a2 100644 --- a/ui/src/builder/BuilderSettingsProperties.vue +++ b/ui/src/builder/BuilderSettingsProperties.vue @@ -76,6 +76,36 @@ v-if="fieldValue.type == FieldType.Object" > + + + + + + + +
{{ fieldValue.desc }}
@@ -94,6 +124,9 @@ import BuilderFieldsColor from "./BuilderFieldsColor.vue"; import BuilderFieldsShadow from "./BuilderFieldsShadow.vue"; import BuilderFieldsText from "./BuilderFieldsText.vue"; import BuilderFieldsObject from "./BuilderFieldsObject.vue"; +import BuilderFieldsWidth from "./BuilderFieldsWidth.vue"; +import BuilderFieldsAlign from "./BuilderFieldsAlign.vue"; +import BuilderFieldsPadding from "./BuilderFieldsPadding.vue"; import injectionKeys from "../injectionKeys"; const ss = inject(injectionKeys.core); diff --git a/ui/src/builder/ico.css b/ui/src/builder/ico.css new file mode 100644 index 000000000..f23bd8a27 --- /dev/null +++ b/ui/src/builder/ico.css @@ -0,0 +1,34 @@ +.ico { + background-size: 12px 12px; + width: 12px; + height: 12px; + display: block; +} + +.ico-padding-4-side { + background: url("../assets/padding-4-side.svg"); +} + +.ico-padding-x-side { + background: url("../assets/padding-x-side.svg"); +} + +.ico-padding-y-side { + background: url("../assets/padding-y-side.svg"); +} + +.ico-padding-left-side { + background: url("../assets/padding-left-side.svg"); +} + +.ico-padding-right-side { + background: url("../assets/padding-right-side.svg"); +} + +.ico-padding-top-side { + background: url("../assets/padding-top-side.svg"); +} + +.ico-padding-bottom-side { + background: url("../assets/padding-bottom-side.svg"); +} \ No newline at end of file diff --git a/ui/src/core/auditAndFix.ts b/ui/src/core/auditAndFix.ts index d467b694a..9c8908020 100644 --- a/ui/src/core/auditAndFix.ts +++ b/ui/src/core/auditAndFix.ts @@ -16,7 +16,7 @@ export function auditAndFixComponents(components: ComponentMap): boolean { Object.entries(components).forEach(([componentId, component]) => { if (componentId !== "root" && !components[component.parentId]) { console.warn( - `Component ${component.id} (${component.type}). Orphan component.` + `Component ${component.id} (${component.type}). Orphan component.`, ); } isFixApplied = @@ -30,32 +30,34 @@ export function auditComponent(component: Component) { const def = getComponentDefinition(component.type); if (!def || def.category == "Fallback") { console.error( - `Component ${component.id} (${component.type}). Invalid component type.` + `Component ${component.id} (${component.type}). Invalid component type.`, ); return; } + fixComponentDeprecatedContent(component, def); + auditComponentFieldKeys(component, def); auditComponentBinding(component, def); } function auditComponentFieldKeys( component: Component, - def: StreamsyncComponentDefinition + def: StreamsyncComponentDefinition, ) { const fieldKeys = Object.keys(def.fields ?? {}); if (!component.content) return; Object.keys(component.content).forEach((contentFieldKey) => { if (fieldKeys.includes(contentFieldKey)) return; console.warn( - `Component ${component.id} (${component.type}). Field key "${contentFieldKey}" is defined in the component but not in the template.` + `Component ${component.id} (${component.type}). Field key "${contentFieldKey}" is defined in the component but not in the template.`, ); }); } function auditComponentBinding( component: Component, - def: StreamsyncComponentDefinition + def: StreamsyncComponentDefinition, ) { const eventKeys = Object.keys(def.events ?? {}); if (!component.binding) return; @@ -66,7 +68,7 @@ function auditComponentBinding( ) return; console.warn( - `Component ${component.id} (${component.type}). The component is bound to event "${component.binding.eventType}" but the template doesn't define that event or it's not bindable.` + `Component ${component.id} (${component.type}). The component is bound to event "${component.binding.eventType}" but the template doesn't define that event or it's not bindable.`, ); } @@ -80,7 +82,7 @@ function auditComponentBinding( */ function auditAndFixPositions( component: Component, - components: ComponentMap + components: ComponentMap, ): boolean { let isFixApplied = false; if (component.id == "root") { @@ -90,12 +92,12 @@ function auditAndFixPositions( } if (component.position == -1) { console.error( - `Component ${component.id} (${component.type}). Invalid position.` + `Component ${component.id} (${component.type}). Invalid position.`, ); } const positionfulChildren = Object.values(components).filter( - (c) => c.parentId === component.id && c.position !== -2 + (c) => c.parentId === component.id && c.position !== -2, ); let positionSum = 0; positionfulChildren.forEach((c) => { @@ -106,7 +108,7 @@ function auditAndFixPositions( ((positionfulChildren.length - 1) * positionfulChildren.length) / 2; if (arithmeticProgression !== positionSum) { console.error( - `Component ${component.id} (${component.type}). Invalid children positions. Automated fix will be applied.` + `Component ${component.id} (${component.type}). Invalid children positions. Automated fix will be applied.`, ); fixPositions(positionfulChildren); isFixApplied = true; @@ -120,3 +122,59 @@ function fixPositions(positionfulChildren: Component[]) { component.position = index; }); } + +/** + * Corrects the mapping of deprecated properties following component changes. + * + * @param component + * @param def + */ +function fixComponentDeprecatedContent( + component: Component, + def: StreamsyncComponentDefinition, +) { + if (component.type == "column") { + if ("horizontalAlignment" in component.content) { + const mapping = { + left: "start", + center: "center", + right: "end", + }; + + component.content["contentHAlign"] = + mapping[component.content["horizontalAlignment"]] || + component.content["horizontalAlignment"]; + delete component.content["horizontalAlignment"]; + } + if ("verticalAlignment" in component.content) { + const mapping = { + normal: "unset", + top: "start", + center: "center", + bottom: "end", + }; + + component.content["contentVAlign"] = + mapping[component.content["verticalAlignment"]] || + component.content["verticalAlignment"]; + delete component.content["verticalAlignment"]; + } + } else if (component.type == "horizontalstack") { + if ("alignment" in component.content) { + const mapping = { + left: "start", + center: "center", + right: "end", + }; + + component.content["contentHAlign"] = + mapping[component.content["alignment"]] || + component.content["alignment"]; + delete component.content["alignment"]; + } + } else if (component.type == "section") { + if ("snapMode" in component.content) { + delete component.content["snapMode"]; + } + } +} diff --git a/ui/src/core/templateMap.ts b/ui/src/core/templateMap.ts index 8e05dd762..0cd543eca 100644 --- a/ui/src/core/templateMap.ts +++ b/ui/src/core/templateMap.ts @@ -4,11 +4,13 @@ import CorePage from "../core_components/CorePage.vue"; import CoreSidebar from "../core_components/CoreSidebar.vue"; import CoreText from "../core_components/CoreText.vue"; import CoreButton from "../core_components/CoreButton.vue"; +import CoreIcon from "../core_components/CoreIcon.vue"; import CoreSection from "../core_components/CoreSection.vue"; import CoreHeader from "../core_components/CoreHeader.vue"; import CoreHeading from "../core_components/CoreHeading.vue"; import CoreDataframe from "../core_components/CoreDataframe.vue"; import CoreHtml from "../core_components/CoreHtml.vue"; +import CorePagination from "../core_components/CorePagination.vue"; import CoreRepeater from "../core_components/CoreRepeater.vue"; import CoreColumn from "../core_components/CoreColumn.vue"; import CoreColumns from "../core_components/CoreColumns.vue"; @@ -40,6 +42,8 @@ import CoreVideoPlayer from "../core_components/CoreVideoPlayer.vue"; import { StreamsyncComponentDefinition } from "../streamsyncTypes"; import { h } from "vue"; + + const templateMap = { root: CoreRoot, page: CorePage, @@ -51,6 +55,7 @@ const templateMap = { heading: CoreHeading, dataframe: CoreDataframe, html: CoreHtml, + pagination: CorePagination, repeater: CoreRepeater, column: CoreColumn, columns: CoreColumns, @@ -59,6 +64,7 @@ const templateMap = { horizontalstack: CoreHorizontalStack, separator: CoreSeparator, image: CoreImage, + icon: CoreIcon, timer: CoreTimer, textinput: CoreTextInput, textareainput: CoreTextareaInput, diff --git a/ui/src/core_components/CoreColumn.vue b/ui/src/core_components/CoreColumn.vue index f953d6d02..8cfc3dd33 100644 --- a/ui/src/core_components/CoreColumn.vue +++ b/ui/src/core_components/CoreColumn.vue @@ -29,19 +29,26 @@
{{ fields.title.value }}
-
-
+ @@ -301,7 +266,7 @@ watch( .CoreColumn > .container { flex: 1 0 0; align-self: stretch; - width: 100%; + display: flex; } .CoreColumn.collapsible.collapsed > .container { diff --git a/ui/src/core_components/CoreDataframe.vue b/ui/src/core_components/CoreDataframe.vue index b2f9e4123..e4f9c36f4 100644 --- a/ui/src/core_components/CoreDataframe.vue +++ b/ui/src/core_components/CoreDataframe.vue @@ -272,7 +272,7 @@ const rowOffset = computed(() => { maxOffset = rowCount.value - displayRowCount.value; } const newOffset = Math.min( - Math.floor(relativePosition.value * maxOffset), + Math.ceil(relativePosition.value * maxOffset), maxOffset, ); return newOffset; diff --git a/ui/src/core_components/CoreHorizontalStack.vue b/ui/src/core_components/CoreHorizontalStack.vue index 9fed702c1..a915aea22 100644 --- a/ui/src/core_components/CoreHorizontalStack.vue +++ b/ui/src/core_components/CoreHorizontalStack.vue @@ -1,16 +1,21 @@ diff --git a/ui/src/core_components/CoreIcon.vue b/ui/src/core_components/CoreIcon.vue new file mode 100644 index 000000000..0cd5aeafe --- /dev/null +++ b/ui/src/core_components/CoreIcon.vue @@ -0,0 +1,67 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/core_components/CorePage.vue b/ui/src/core_components/CorePage.vue index 598380a6f..b14c0a350 100644 --- a/ui/src/core_components/CorePage.vue +++ b/ui/src/core_components/CorePage.vue @@ -23,7 +23,20 @@ \ No newline at end of file diff --git a/ui/src/core_components/CoreRoot.vue b/ui/src/core_components/CoreRoot.vue index 0ada89591..31167631a 100644 --- a/ui/src/core_components/CoreRoot.vue +++ b/ui/src/core_components/CoreRoot.vue @@ -185,33 +185,23 @@ function handleHashChange() { } async function importStylesheet(stylesheetKey: string, path: string) { - const req = await fetch(path); - if (req.status > 399) { - console.warn(`Couldn't import stylesheet at "${path}".`); - return; - } const existingEl = document.querySelector(`[data-streamsync-stylesheet-key="${stylesheetKey}"]`); existingEl?.remove(); - const el = document.createElement("style"); + const el = document.createElement("link"); el.dataset.streamsyncStylesheetKey = stylesheetKey; - const cssText = await req.text(); - el.textContent = cssText; - document.head.appendChild(el); + el.setAttribute('href', path) + el.setAttribute("rel", "stylesheet"); + document.head.appendChild(el); } async function importScript(scriptKey: string, path: string) { - const req = await fetch(path); - if (req.status > 399) { - console.warn(`Couldn't import script at "${path}".`); - return; - } const existingEl = document.querySelector(`[data-streamsync-script-key="${scriptKey}"]`); existingEl?.remove(); const el = document.createElement("script"); el.dataset.streamsyncScriptKey = scriptKey; - const scriptText = await req.text(); - el.textContent = scriptText; - document.head.appendChild(el); + el.src = path; + el.setAttribute("rel", "modulepreload"); + document.head.appendChild(el); } async function importModule(moduleKey: string, specifier: string) { diff --git a/ui/src/core_components/CoreSection.vue b/ui/src/core_components/CoreSection.vue index ccb6f7c06..a01a01b30 100644 --- a/ui/src/core_components/CoreSection.vue +++ b/ui/src/core_components/CoreSection.vue @@ -1,12 +1,17 @@ @@ -73,19 +74,14 @@ const fields = inject(injectionKeys.evaluatedFields); diff --git a/ui/src/core_components/CoreTab.vue b/ui/src/core_components/CoreTab.vue index 105bc0a59..16f9db156 100644 --- a/ui/src/core_components/CoreTab.vue +++ b/ui/src/core_components/CoreTab.vue @@ -21,17 +21,18 @@ > {{ fields.name.value }} -
-
+ @@ -54,7 +55,11 @@ const CONTENT_DISPLAYING_INSTANCE_NUMBER = 1; import { Component, FieldType, InstancePath } from "../streamsyncTypes"; import { useEvaluator } from "../renderer/useEvaluator"; -import { cssClasses } from "../renderer/sharedStyleFields"; +import { + contentHAlign, + cssClasses, + contentPadding, +} from "../renderer/sharedStyleFields"; const description = "A container component that displays its child components as a tab inside a Tab Container."; @@ -73,6 +78,11 @@ export default { init: "Tab Name", type: FieldType.Text, }, + contentPadding: { + ...contentPadding, + default: "16px" + }, + contentHAlign, cssClasses, }, previewField: "name", @@ -82,6 +92,7 @@ export default { + + diff --git a/ui/src/renderer/sharedStyleFields.ts b/ui/src/renderer/sharedStyleFields.ts index 62633b711..4507955e3 100644 --- a/ui/src/renderer/sharedStyleFields.ts +++ b/ui/src/renderer/sharedStyleFields.ts @@ -86,3 +86,32 @@ export const cssClasses = { desc: "CSS classes, separated by spaces. You can define classes in custom stylesheets." }; +export const contentWidth = { + name: "Content width", + type: FieldType.Width, + default: "100%", + category: FieldCategory.Style, + desc: "Configure content width using CSS units, e.g. 100px, 50%, 10vw, etc.", +}; + +export const contentHAlign = { + name: "Content alignment (H)", + type: FieldType.HAlign, + default: "unset", + category: FieldCategory.Style, +}; + +export const contentVAlign = { + name: "Content alignment (V)", + type: FieldType.VAlign, + default: "unset", + category: FieldCategory.Style, +}; + +export const contentPadding = { + name: "Padding", + type: FieldType.Padding, + default: "0", + category: FieldCategory.Style, +}; + diff --git a/ui/src/renderer/sharedStyles.css b/ui/src/renderer/sharedStyles.css index d0efa68ba..6616c1083 100644 --- a/ui/src/renderer/sharedStyles.css +++ b/ui/src/renderer/sharedStyles.css @@ -144,6 +144,7 @@ button:hover { } 100% { + height: 100%; min-height: 24px; min-width: 4px; opacity: 1; @@ -158,6 +159,7 @@ button:hover { } 100% { + width: 100%; min-height: 4px; min-width: 24px; opacity: 1; diff --git a/ui/src/streamsyncTypes.ts b/ui/src/streamsyncTypes.ts index 6d07322e5..1d1018e13 100644 --- a/ui/src/streamsyncTypes.ts +++ b/ui/src/streamsyncTypes.ts @@ -88,6 +88,10 @@ export const enum FieldType { Number = "Number", Object = "Object", IdKey = "Identifying Key", + Width = "Width", + HAlign = "Align (H)", + VAlign = "Align (V)", + Padding = "Padding", } export const enum FieldCategory {