diff --git a/apps/default/ui.json b/apps/default/ui.json index 9e210de40..471d19b6d 100644 --- a/apps/default/ui.json +++ b/apps/default/ui.json @@ -1,6 +1,6 @@ { "metadata": { - "writer_version": "0.5.0" + "writer_version": "0.6.2rc3" }, "components": { "root": { @@ -11,8 +11,7 @@ }, "isCodeManaged": false, "position": 0, - "handlers": {}, - "visible": true + "handlers": {} }, "c0f99a9e-5004-4e75-a6c6-36f17490b134": { "id": "c0f99a9e-5004-4e75-a6c6-36f17490b134", @@ -23,8 +22,7 @@ "isCodeManaged": false, "position": 0, "parentId": "root", - "handlers": {}, - "visible": true + "handlers": {} }, "bebc5fe9-63a7-46a7-b0fa-62303555cfaf": { "id": "bebc5fe9-63a7-46a7-b0fa-62303555cfaf", @@ -35,8 +33,7 @@ "isCodeManaged": false, "position": 0, "parentId": "c0f99a9e-5004-4e75-a6c6-36f17490b134", - "handlers": {}, - "visible": true + "handlers": {} }, "28d3885b-0fb8-4d41-97c6-978540015431": { "id": "28d3885b-0fb8-4d41-97c6-978540015431", @@ -48,8 +45,7 @@ "isCodeManaged": false, "position": 1, "parentId": "c0f99a9e-5004-4e75-a6c6-36f17490b134", - "handlers": {}, - "visible": true + "handlers": {} }, "9556c0e3-8584-4ac9-903f-908a775a33ec": { "id": "9556c0e3-8584-4ac9-903f-908a775a33ec", @@ -63,8 +59,7 @@ "parentId": "0d05bc9f-1655-4d0b-bc9b-c2f4c71a5117", "handlers": { "click": "increment" - }, - "visible": true + } }, "51d1554e-1b88-461c-9353-1419cba0053a": { "id": "51d1554e-1b88-461c-9353-1419cba0053a", @@ -78,8 +73,7 @@ "parentId": "0d05bc9f-1655-4d0b-bc9b-c2f4c71a5117", "handlers": { "click": "decrement" - }, - "visible": true + } }, "0d05bc9f-1655-4d0b-bc9b-c2f4c71a5117": { "id": "0d05bc9f-1655-4d0b-bc9b-c2f4c71a5117", @@ -90,8 +84,7 @@ "isCodeManaged": false, "position": 0, "parentId": "f3777e75-3659-4d44-8ef7-aeec0d06855b", - "handlers": {}, - "visible": true + "handlers": {} }, "172a14df-f73a-44fa-8fb1-e8648e7d32d2": { "id": "172a14df-f73a-44fa-8fb1-e8648e7d32d2", @@ -104,8 +97,7 @@ "isCodeManaged": false, "position": 0, "parentId": "c2519671-9ce7-44e7-ba4e-b0efda9cb20e", - "handlers": {}, - "visible": true + "handlers": {} }, "d4a5e62c-c6fe-49c4-80d4-33862af8727d": { "id": "d4a5e62c-c6fe-49c4-80d4-33862af8727d", @@ -114,8 +106,7 @@ "isCodeManaged": false, "position": 0, "parentId": "28d3885b-0fb8-4d41-97c6-978540015431", - "handlers": {}, - "visible": true + "handlers": {} }, "f3777e75-3659-4d44-8ef7-aeec0d06855b": { "id": "f3777e75-3659-4d44-8ef7-aeec0d06855b", @@ -129,8 +120,7 @@ "isCodeManaged": false, "position": 2, "parentId": "d4a5e62c-c6fe-49c4-80d4-33862af8727d", - "handlers": {}, - "visible": true + "handlers": {} }, "c2519671-9ce7-44e7-ba4e-b0efda9cb20e": { "id": "c2519671-9ce7-44e7-ba4e-b0efda9cb20e", @@ -141,8 +131,7 @@ "isCodeManaged": false, "position": 0, "parentId": "d4a5e62c-c6fe-49c4-80d4-33862af8727d", - "handlers": {}, - "visible": true + "handlers": {} }, "d4a71819-7444-4083-a1c7-7995452a7abf": { "id": "d4a71819-7444-4083-a1c7-7995452a7abf", @@ -151,8 +140,7 @@ "isCodeManaged": false, "position": 1, "parentId": "d4a5e62c-c6fe-49c4-80d4-33862af8727d", - "handlers": {}, - "visible": true + "handlers": {} } } } \ No newline at end of file diff --git a/apps/hello/ui.json b/apps/hello/ui.json index 42858f31c..6db57e02f 100644 --- a/apps/hello/ui.json +++ b/apps/hello/ui.json @@ -1,6 +1,6 @@ { "metadata": { - "writer_version": "0.5.0" + "writer_version": "0.6.2rc3" }, "components": { "root": { @@ -86,7 +86,12 @@ }, "isCodeManaged": false, "position": 0, - "parentId": "9c30af6d-4ee5-4782-9169-0f361d67fa76" + "parentId": "9c30af6d-4ee5-4782-9169-0f361d67fa76", + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "ee919cd6-8153-4f34-8c6a-bfc1153df360": { "id": "ee919cd6-8153-4f34-8c6a-bfc1153df360", @@ -623,7 +628,11 @@ "position": 0, "parentId": "6a490318-239e-4fe9-a56b-f0f33d628c87", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "6d895924-e808-44aa-a119-f4e2d7f394f3": { "id": "6d895924-e808-44aa-a119-f4e2d7f394f3", @@ -661,7 +670,11 @@ "position": 0, "parentId": "feb9ca67-6670-483d-a895-22b031426a13", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f": { "id": "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f", @@ -671,7 +684,11 @@ "position": 0, "parentId": "c6392876-7cfd-4680-8725-b04f43ff294f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "feb9ca67-6670-483d-a895-22b031426a13": { "id": "feb9ca67-6670-483d-a895-22b031426a13", @@ -683,7 +700,11 @@ "position": 0, "parentId": "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "3b325899-e560-40ea-ba54-9c55967af1e3": { "id": "3b325899-e560-40ea-ba54-9c55967af1e3", @@ -695,7 +716,11 @@ "position": 1, "parentId": "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "a0cd99db-0cbe-40ca-b9cb-b1670ec60dd8": { "id": "a0cd99db-0cbe-40ca-b9cb-b1670ec60dd8", @@ -707,7 +732,11 @@ "position": 2, "parentId": "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "5dcd137b-76bd-4a5f-ae5c-5b629035500e": { "id": "5dcd137b-76bd-4a5f-ae5c-5b629035500e", @@ -719,7 +748,11 @@ "position": 3, "parentId": "1c05b2e7-3a31-40dd-b6b8-77ded7c6bc0f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "6a81f847-4d1d-4110-9cc1-12c716150e66": { "id": "6a81f847-4d1d-4110-9cc1-12c716150e66", @@ -734,7 +767,11 @@ "position": 0, "parentId": "3b325899-e560-40ea-ba54-9c55967af1e3", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "8e54e9d2-a7c8-4f74-897f-fa5791cd82da": { "id": "8e54e9d2-a7c8-4f74-897f-fa5791cd82da", @@ -749,7 +786,11 @@ "position": 0, "parentId": "a0cd99db-0cbe-40ca-b9cb-b1670ec60dd8", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "07f50628-4679-48a8-9a5d-07dcaf171afb": { "id": "07f50628-4679-48a8-9a5d-07dcaf171afb", @@ -764,7 +805,11 @@ "position": 0, "parentId": "5dcd137b-76bd-4a5f-ae5c-5b629035500e", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "4cca0893-5ad7-4152-b805-5c87babc4dee": { "id": "4cca0893-5ad7-4152-b805-5c87babc4dee", @@ -774,7 +819,11 @@ "position": 1, "parentId": "c6392876-7cfd-4680-8725-b04f43ff294f", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "23bc1387-26ed-4ff2-8565-b027c2960c3c": { "id": "23bc1387-26ed-4ff2-8565-b027c2960c3c", @@ -792,7 +841,11 @@ "position": 1, "parentId": "root", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "1d195388-35a3-43e1-b825-1d263b100a28": { "id": "1d195388-35a3-43e1-b825-1d263b100a28", @@ -804,7 +857,11 @@ "position": 0, "parentId": "23bc1387-26ed-4ff2-8565-b027c2960c3c", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "771dc336-69b2-400e-9ea3-e881e2332c9d": { "id": "771dc336-69b2-400e-9ea3-e881e2332c9d", @@ -816,7 +873,11 @@ "position": 0, "parentId": "dfaae7f9-db20-4f70-a376-919bdc7b6010", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "9c77aee4-e2a0-4e8b-9c2b-377f939bb51e": { "id": "9c77aee4-e2a0-4e8b-9c2b-377f939bb51e", @@ -841,7 +902,11 @@ "handlers": { "click": "$goToPage_main" }, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "8fe33adf-a5ea-4c7a-8d1d-59cc4dc14f05": { "id": "8fe33adf-a5ea-4c7a-8d1d-59cc4dc14f05", @@ -851,7 +916,11 @@ "position": 1, "parentId": "23bc1387-26ed-4ff2-8565-b027c2960c3c", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "7402263c-cb8b-412d-b170-e6dc6ffcb706": { "id": "7402263c-cb8b-412d-b170-e6dc6ffcb706", @@ -863,7 +932,11 @@ "position": 1, "parentId": "8fe33adf-a5ea-4c7a-8d1d-59cc4dc14f05", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "dfaae7f9-db20-4f70-a376-919bdc7b6010": { "id": "dfaae7f9-db20-4f70-a376-919bdc7b6010", @@ -875,7 +948,11 @@ "position": 0, "parentId": "8fe33adf-a5ea-4c7a-8d1d-59cc4dc14f05", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "5bc38721-8b48-43d5-a454-ee5ebe713a4c": { "id": "5bc38721-8b48-43d5-a454-ee5ebe713a4c", @@ -908,7 +985,11 @@ "position": 2, "parentId": "771dc336-69b2-400e-9ea3-e881e2332c9d", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "03f422ce-bc36-4000-8eb1-da1cc00948c9": { "id": "03f422ce-bc36-4000-8eb1-da1cc00948c9", @@ -923,7 +1004,11 @@ "handlers": { "click": "$goToPage_story" }, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "2df56a4b-b6e7-423d-a7a1-5d23c77f65fa": { "id": "2df56a4b-b6e7-423d-a7a1-5d23c77f65fa", @@ -944,7 +1029,11 @@ "position": 0, "parentId": "771dc336-69b2-400e-9ea3-e881e2332c9d", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "77cb256b-ef12-4a55-a051-500497f41302": { "id": "77cb256b-ef12-4a55-a051-500497f41302", @@ -956,7 +1045,11 @@ "position": 2, "parentId": "5bc38721-8b48-43d5-a454-ee5ebe713a4c", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "ed010441-0cac-4aa5-9e6f-97228e0c3536": { "id": "ed010441-0cac-4aa5-9e6f-97228e0c3536", @@ -971,7 +1064,11 @@ "handlers": { "click": "handle_story_download" }, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "e1ax8ctt8lrao0e4": { "id": "e1ax8ctt8lrao0e4", @@ -983,7 +1080,11 @@ "position": 3, "parentId": "ee919cd6-8153-4f34-8c6a-bfc1153df360", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "j3jkho6tb97u0onr": { "id": "j3jkho6tb97u0onr", @@ -997,7 +1098,11 @@ "position": 0, "parentId": "e1ax8ctt8lrao0e4", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "4wzaubf275w17gac": { "id": "4wzaubf275w17gac", @@ -1010,7 +1115,11 @@ "position": 0, "parentId": "j3jkho6tb97u0onr", "handlers": {}, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "19binb4yi70gesho": { "id": "19binb4yi70gesho", @@ -1042,7 +1151,11 @@ "wf-change-page": "handle_paginated_members_page_change", "wf-change-page-size": "handle_paginated_members_page_size_change" }, - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "b27lw9ex8ig3x17p": { "id": "b27lw9ex8ig3x17p", @@ -1054,7 +1167,11 @@ "isCodeManaged": false, "position": 2, "parentId": "9c30af6d-4ee5-4782-9169-0f361d67fa76", - "visible": true + "visible": { + "expression": true, + "binding": "", + "reversed": false + } }, "804e15bf-11a7-463d-8082-f46ea3acac1b": { "id": "804e15bf-11a7-463d-8082-f46ea3acac1b", diff --git a/docs/framework/application-state.mdx b/docs/framework/application-state.mdx index 50b65383f..0b200f8f2 100644 --- a/docs/framework/application-state.mdx +++ b/docs/framework/application-state.mdx @@ -113,3 +113,33 @@ The front-end cannot directly display complex data types such as Pandas datafram Pandas dataframes are converted to JSON and can be used in _Dataframe_ components. + +## State schema + +State schema is a feature that allows you to define the structure of the state. +This is useful for ensuring that the state is always in the expected format. + +Schema allows you to use features like + +* typing checking with mypy / ruff +* autocomplete in IDEs +* declare dictionaries +* automatically calculate mutations on properties + +more into [Advanced > State schema](./state-schema) + +```python +import writer as wf + +class AppSchema(wf.WriterState): + counter: int + +initial_state = wf.init_state({ + "counter": 0 +}, schema=AppSchema) + +# Event handler +# It receives the session state as an argument and mutates it +def increment(state: AppSchema): + state.counter += 1 +``` diff --git a/docs/framework/event-handlers.mdx b/docs/framework/event-handlers.mdx index 6617c8357..cbb41b5dd 100644 --- a/docs/framework/event-handlers.mdx +++ b/docs/framework/event-handlers.mdx @@ -136,6 +136,63 @@ def hande_click_cleaner(state): ``` +## Mutation event + +You can subscribe to mutations on a specific key in the state. +This is useful when you want to trigger a function every time a specific key is mutated. + + +```python simple subscription +import writer as wf + +def _increment_counter(state): + state['my_counter'] += 1 + +state = wf.init_state({"a": 1, "my_counter": 0}) +state.subscribe_mutation('a', _increment_counter) + +state['a'] = 2 # trigger _increment_counter mutation +``` + +```python multiple subscriptions +import writer as wf + +def _increment_counter(state): + state['my_counter'] += 1 + +state = wf.init_state({ + 'title': 'Hello', + 'app': {'title', 'Writer Framework'}, + 'my_counter': 0} +) + +state.subscribe_mutation(['title', 'app.title'], _increment_counter) # subscribe to multiple keys + +state['title'] = "Hello Pigeon" # trigger _increment_counter mutation +``` + +```python trigger event handler +import writer as wf + +def _increment_counter(state, context: dict, payload: dict, session: dict, ui: WriterUIManager): + if context['event'] == 'mutation' and context['mutation'] == 'a': + if payload['previous_value'] > payload['new_value']: + state['my_counter'] += 1 + +state = wf.init_state({"a": 1, "my_counter": 0}) +state.subscribe_mutation('a', _increment_counter) + +state['a'] = 2 # increment my_counter +state['a'] = 3 # increment my_counter +state['a'] = 2 # do nothing +``` + + + +`subscribe_mutation` is compatible with event handler signature. It will accept all the arguments +of the event handler (`context`, `payload`, ...). + + ## Receiving a payload Several events include additional data, known as the event's payload. The event handler can receive that data using the `payload` argument. diff --git a/docs/framework/public/components/sliderinput.png b/docs/framework/public/components/sliderinput.png index ab4527598..b3c2efdde 100644 Binary files a/docs/framework/public/components/sliderinput.png and b/docs/framework/public/components/sliderinput.png differ diff --git a/docs/framework/state-schema.mdx b/docs/framework/state-schema.mdx index 4ee875ed2..7305d9b5f 100644 --- a/docs/framework/state-schema.mdx +++ b/docs/framework/state-schema.mdx @@ -62,6 +62,30 @@ initial_state = wf.init_state({ }, schema=AppSchema) ``` +## Calculated properties + +Calculated properties are updated automatically when a dependency changes. +They can be used to calculate values derived from application state. + +```python +class MyAppState(wf.State): + counter: List[int] + +class MyState(wf.WriterState): + counter: List[int] + + @wf.property(['counter', 'app.counter']) + def total_counter(self): + return sum(self.counter) + sum(self.app.counter) + +initial_state = wf.init_state({ + "counter": 0, + "my_app": { + "counter": 0 + } +}, schema=MyState) +``` + ## Multi-level dictionary Some components like _Vega Lite Chart_ require specifying a graph in the form of a multi-level dictionary. diff --git a/package.json b/package.json index 8ea98e1d8..2d749778c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dev": "npm run -w writer-ui dev", "storybook": "npm run -w writer-ui storybook", "storybook.build": "npm run -w writer-ui storybook.build", - "custom.dev": "npm run -w writer-ui dev", + "custom.dev": "npm run -w writer-ui custom.dev", "cli:test": "pytest tests -o log_cli=true ", "cli:lint": "mypy ./src/writer --exclude app_templates/* && ruff check", diff --git a/pyproject.toml b/pyproject.toml index 44fc02097..fdc8dd5be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "writer" -version = "0.7.0" +version = "0.7.1" description = "An open-source, Python framework for building feature-rich apps that are fully integrated with the Writer platform." authors = ["Writer, Inc."] readme = "README.md" diff --git a/src/ui/src/builder/BuilderHeader.vue b/src/ui/src/builder/BuilderHeader.vue index 4c13288a7..8e3f90e4c 100644 --- a/src/ui/src/builder/BuilderHeader.vue +++ b/src/ui/src/builder/BuilderHeader.vue @@ -82,16 +82,27 @@ const animate = () => { }; const syncHealthStatus = () => { + let s = ""; switch (wf.syncHealth.value) { case "offline": - return "Offline. Not syncing."; + s += "Offline. Not syncing."; + break; case "connected": - return "Online. Syncing..."; + s += "Online. Syncing..."; + break; case "idle": - return "Sync not initialised."; + s += "Sync not initialised."; + break; case "suspended": - return "Sync suspended."; + s += "Sync suspended."; + break; } + + if (wf.featureFlags.value.length > 0) { + s += ` Feature flags: ${wf.featureFlags.value.join(", ")}`; + } + + return s; }; function showStateExplorer() { @@ -134,6 +145,7 @@ const customHandlerModalCloseAction: ModalAction = { } .syncHealth { + cursor: pointer; background: var(--builderHeaderBackgroundHoleColor); border-radius: 18px; padding-left: 16px; diff --git a/src/ui/src/builder/BuilderSettingsVisibility.vue b/src/ui/src/builder/BuilderSettingsVisibility.vue index d3cf31a2e..f899059e5 100644 --- a/src/ui/src/builder/BuilderSettingsVisibility.vue +++ b/src/ui/src/builder/BuilderSettingsVisibility.vue @@ -11,7 +11,7 @@ :class="{ active: typeof component.visible == 'undefined' || - component.visible === true, + component.visible.expression === true, }" @click="() => setVisibleValue(component.id, true)" > @@ -19,26 +19,39 @@
No
Custom
Visibility value setVisibleValue( component.id, + 'custom', (ev.target as HTMLInputElement).value, + component.visible.reversed, ) " /> +
+ Reverse +
Reference a state or context element that will evaluate to true or false. Reference the element directly, i.e. use @@ -87,4 +117,10 @@ const component = computed(() => wf.getComponentById(ssbm.getSelectedId())); .content { padding: 16px 12px 12px 12px; } + +.flexRow { + display: flex; + flex-direction: row; + gap: 8px; +} diff --git a/src/ui/src/builder/BuilderSidebarTitleSearch.vue b/src/ui/src/builder/BuilderSidebarTitleSearch.vue new file mode 100644 index 000000000..c82230646 --- /dev/null +++ b/src/ui/src/builder/BuilderSidebarTitleSearch.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/ui/src/builder/BuilderSidebarToolbar.vue b/src/ui/src/builder/BuilderSidebarToolbar.vue index deb50f3aa..1c369f994 100644 --- a/src/ui/src/builder/BuilderSidebarToolbar.vue +++ b/src/ui/src/builder/BuilderSidebarToolbar.vue @@ -1,22 +1,23 @@