From 1e23e3a130928ffda82cf791d4acd33f8f5a7fa2 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Mon, 11 Nov 2024 15:26:28 +0700 Subject: [PATCH 1/8] frontend done --- .../base/src/packaging/taipy-gui-base.d.ts | 3 +- frontend/taipy-gui/package-lock.json | 4 +- frontend/taipy-gui/src/components/Router.tsx | 5 +- .../taipy-gui/src/context/taipyReducers.ts | 54 ++++++++--- frontend/taipy-gui/src/context/wsUtils.ts | 3 +- frontend/taipy-gui/src/hooks/index.ts | 16 +++ .../src/hooks/useLocalStorageWithEvent.ts | 97 +++++++++++++++++++ 7 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 frontend/taipy-gui/src/hooks/index.ts create mode 100644 frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index be8ce809a3..071886d454 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -76,7 +76,8 @@ export type WsMessageType = | "AID" | "GR" | "FV" - | "BC"; + | "BC" + | "LS" export interface WsMessage { type: WsMessageType | string; name: string; diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index f315b6b357..302a870c1c 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -1,12 +1,12 @@ { "name": "taipy-gui", - "version": "4.0.0", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "taipy-gui", - "version": "4.0.0", + "version": "4.1.0", "dependencies": { "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", diff --git a/frontend/taipy-gui/src/components/Router.tsx b/frontend/taipy-gui/src/components/Router.tsx index 1142fdef21..1734c1b379 100644 --- a/frontend/taipy-gui/src/components/Router.tsx +++ b/frontend/taipy-gui/src/components/Router.tsx @@ -44,6 +44,7 @@ import MainPage from "./pages/MainPage"; import TaipyRendered from "./pages/TaipyRendered"; import NotFound404 from "./pages/NotFound404"; import { getBaseURL } from "../utils"; +import { useLocalStorageWithEvent } from "../hooks"; interface AxiosRouter { router: string; @@ -63,6 +64,8 @@ const Router = () => { const themeClass = "taipy-" + state.theme.palette.mode; const baseURL = getBaseURL(); + useLocalStorageWithEvent(dispatch); + useEffect(() => { if (refresh) { // no need to access the backend again, the routes are static @@ -125,7 +128,7 @@ const Router = () => { path !== "/" + (path) => path !== "/", )} /> } diff --git a/frontend/taipy-gui/src/context/taipyReducers.ts b/frontend/taipy-gui/src/context/taipyReducers.ts index 771f7b8ef4..9761da1024 100644 --- a/frontend/taipy-gui/src/context/taipyReducers.ts +++ b/frontend/taipy-gui/src/context/taipyReducers.ts @@ -16,7 +16,7 @@ import { createTheme, Theme } from "@mui/material/styles"; import merge from "lodash/merge"; import { Dispatch } from "react"; import { io, Socket } from "socket.io-client"; -import { nanoid } from 'nanoid'; +import { nanoid } from "nanoid"; import { FilterDesc } from "../components/Taipy/tableUtils"; import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit"; @@ -48,6 +48,8 @@ export enum Types { Partial = "PARTIAL", Acknowledgement = "ACKNOWLEDGEMENT", Broadcast = "BROADCAST", + LocalStorage = "LOCAL_STORAGE", + LocalStorageUpdate = "LOCAL_STORAGE_UPDATE", } /** @@ -180,7 +182,7 @@ const getUserTheme = (mode: PaletteMode) => { }, }, }, - }) + }), ); }; @@ -225,7 +227,7 @@ export const messageToAction = (message: WsMessage) => { (message as unknown as NavigateMessage).to, (message as unknown as NavigateMessage).params, (message as unknown as NavigateMessage).tab, - (message as unknown as NavigateMessage).force + (message as unknown as NavigateMessage).force, ); } else if (message.type === "ID") { return createIdAction((message as unknown as IdMessage).id); @@ -267,7 +269,8 @@ export const getWsMessageListener = (dispatch: Dispatch) => { // Broadcast const __BroadcastRepo: Record> = {}; -const stackBroadcast = (name: string, value: unknown) => (__BroadcastRepo[name] = __BroadcastRepo[name] || []).push(value); +const stackBroadcast = (name: string, value: unknown) => + (__BroadcastRepo[name] = __BroadcastRepo[name] || []).push(value); const broadcast_timeout = 250; @@ -393,7 +396,7 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta const deleteAlertAction = action as unknown as TaipyAlertAction; return { ...state, - alerts: state.alerts.filter(alert => alert.notificationId !== deleteAlertAction.notificationId), + alerts: state.alerts.filter((alert) => alert.notificationId !== deleteAlertAction.notificationId), }; case Types.SetBlock: const blockAction = action as unknown as TaipyBlockAction; @@ -495,7 +498,7 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta action.payload, state.id, action.context, - action.propagate + action.propagate, ); break; case Types.Action: @@ -507,6 +510,10 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta case Types.RequestUpdate: ackId = sendWsMessage(state.socket, "RU", action.name, action.payload, state.id, action.context); break; + case Types.LocalStorage: + case Types.LocalStorageUpdate: + ackId = sendWsMessage(state.socket, "LS", action.name, action.payload, state.id, action.context); + break; } if (ackId) return { ...state, ackList: [...state.ackList, ackId] }; return state; @@ -545,7 +552,7 @@ export const createSendUpdateAction = ( context: string | undefined, onChange?: string, propagate = true, - relName?: string + relName?: string, ): TaipyAction => ({ type: Types.SendUpdate, name: name, @@ -598,7 +605,7 @@ export const createRequestChartUpdateAction = ( context: string | undefined, columns: string[], pageKey: string, - decimatorPayload: unknown | undefined + decimatorPayload: unknown | undefined, ): TaipyAction => createRequestDataUpdateAction( name, @@ -609,7 +616,7 @@ export const createRequestChartUpdateAction = ( { decimatorPayload: decimatorPayload, }, - true + true, ); export const createRequestTableUpdateAction = ( @@ -631,7 +638,7 @@ export const createRequestTableUpdateAction = ( filters?: Array, compare?: string, compareDatas?: string, - stateContext?: Record + stateContext?: Record, ): TaipyAction => createRequestDataUpdateAction( name, @@ -654,7 +661,7 @@ export const createRequestTableUpdateAction = ( compare, compare_datas: compareDatas, state_context: stateContext, - }) + }), ); export const createRequestInfiniteTableUpdateAction = ( @@ -677,7 +684,7 @@ export const createRequestInfiniteTableUpdateAction = ( compare?: string, compareDatas?: string, stateContext?: Record, - reverse?: boolean + reverse?: boolean, ): TaipyAction => createRequestDataUpdateAction( name, @@ -702,7 +709,7 @@ export const createRequestInfiniteTableUpdateAction = ( compare_datas: compareDatas, state_context: stateContext, reverse: !!reverse, - }) + }), ); /** @@ -733,7 +740,7 @@ export const createRequestDataUpdateAction = ( pageKey: string, payload: Record, allData = false, - library?: string + library?: string, ): TaipyAction => { payload = payload || {}; if (id !== undefined) { @@ -771,7 +778,7 @@ export const createRequestUpdateAction = ( context: string | undefined, names: string[], forceRefresh = false, - stateContext?: Record + stateContext?: Record, ): TaipyAction => ({ type: Types.RequestUpdate, name: "", @@ -846,7 +853,7 @@ export const createNavigateAction = ( to?: string, params?: Record, tab?: string, - force?: boolean + force?: boolean, ): TaipyNavigateAction => ({ type: Types.Navigate, to, @@ -882,3 +889,18 @@ export const createPartialAction = (name: string, create: boolean): TaipyPartial name, create, }); + +export const createLocalStorageAction = (localStorageData: Record): TaipyAction => ({ + type: Types.LocalStorage, + name: "init", + payload: localStorageData, +}); + +export const createLocalStorageUpdateAction = (key: string, value: string | null): TaipyAction => ({ + type: Types.LocalStorageUpdate, + name: "update", + payload: { + key: key, + value: value, + }, +}); diff --git a/frontend/taipy-gui/src/context/wsUtils.ts b/frontend/taipy-gui/src/context/wsUtils.ts index e5e8fd1ec1..fc1cc35b0c 100644 --- a/frontend/taipy-gui/src/context/wsUtils.ts +++ b/frontend/taipy-gui/src/context/wsUtils.ts @@ -22,7 +22,8 @@ export type WsMessageType = | "AID" | "GR" | "FV" - | "BC"; + | "BC" + | "LS"; export interface WsMessage { type: WsMessageType; diff --git a/frontend/taipy-gui/src/hooks/index.ts b/frontend/taipy-gui/src/hooks/index.ts new file mode 100644 index 0000000000..e4dbd62cab --- /dev/null +++ b/frontend/taipy-gui/src/hooks/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021-2024 Avaiga Private Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import { useLocalStorageWithEvent } from "./useLocalStorageWithEvent"; + +export { useLocalStorageWithEvent }; diff --git a/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts b/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts new file mode 100644 index 0000000000..4925c47b53 --- /dev/null +++ b/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2021-2024 Avaiga Private Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import { Dispatch, useEffect } from "react"; +import { createLocalStorageAction, createLocalStorageUpdateAction, TaipyBaseAction } from "../context/taipyReducers"; + +const STORAGE_EVENT = "storage"; +const CUSTOM_LOCAL_STORAGE_EVENT = "local-storage"; + +export const useLocalStorageWithEvent = (dispatch: Dispatch) => { + // Override the original setItem and removeItem behaviour for localStorage to dispatch a custom storage event for local tab + useEffect(() => { + // Preserve the original setItem and removeItem method + const _setItem = Storage.prototype.setItem; + const _removeItem = Storage.prototype.removeItem; + + Storage.prototype.setItem = function (key, value) { + if (this === window.localStorage) { + const oldValue = localStorage.getItem(key); + _setItem.call(this, key, value); + + const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, { + detail: { key, oldValue, newValue: value }, + }); + window.dispatchEvent(customEvent); + } else { + _setItem.call(this, key, value); + } + }; + + Storage.prototype.removeItem = function (key: string) { + if (this === window.localStorage) { + const oldValue = localStorage.getItem(key); + _removeItem.call(this, key); + + const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, { + detail: { key, oldValue, newValue: null }, + }); + window.dispatchEvent(customEvent); + } else { + _removeItem.call(this, key); + } + }; + + // Cleanup the override on unmount + return () => { + Storage.prototype.setItem = _setItem; + Storage.prototype.removeItem = _removeItem; + }; + }, []); + + // addEventListener for storage and custom storage event + useEffect(() => { + const handleStorageEvent = ( + event: StorageEvent | CustomEvent<{ key: string; oldValue: string | null; newValue: string | null }>, + ) => { + const isCustomEvent = event instanceof CustomEvent; + const key = isCustomEvent ? event.detail.key : event.key; + const newValue = isCustomEvent ? event.detail.newValue : event.newValue; + if (!key) { + return; + } + dispatch(createLocalStorageUpdateAction(key, newValue)); + }; + + window.addEventListener(STORAGE_EVENT, handleStorageEvent as EventListener); + window.addEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener); + + // Cleanup event listener on unmount + return () => { + window.removeEventListener(STORAGE_EVENT, handleStorageEvent as EventListener); + window.removeEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener); + }; + }, [dispatch]); // Not necessary to add dispatch to the dependency array but comply with eslint warning anyway + + // send all localStorage data to backend on init + useEffect(() => { + const localStorageData: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + localStorageData[key] = localStorage.getItem(key) || ""; + } + } + dispatch(createLocalStorageAction(localStorageData)); + }, [dispatch]); // Not necessary to add dispatch to the dependency array but comply with eslint warning anyway +}; From a98cddfa4a05442377d8aa156abb73bc9798ffad Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Mon, 11 Nov 2024 15:56:40 +0700 Subject: [PATCH 2/8] add support to get localStorage from backend (#2190) --- taipy/gui/__init__.py | 1 + taipy/gui/data/data_scope.py | 3 ++- taipy/gui/gui.py | 23 +++++++++++++++++++++++ taipy/gui/gui_actions.py | 13 +++++++++++++ taipy/gui/types.py | 7 +++---- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/taipy/gui/__init__.py b/taipy/gui/__init__.py index d9903a684e..e8571749e7 100644 --- a/taipy/gui/__init__.py +++ b/taipy/gui/__init__.py @@ -78,6 +78,7 @@ from .gui_actions import ( broadcast_callback, download, + get_local_storage, get_module_context, get_module_name_from_state, get_state_id, diff --git a/taipy/gui/data/data_scope.py b/taipy/gui/data/data_scope.py index f47dc4f421..1b8bc055b7 100644 --- a/taipy/gui/data/data_scope.py +++ b/taipy/gui/data/data_scope.py @@ -23,7 +23,8 @@ class _DataScopes: _GLOBAL_ID = "global" _META_PRE_RENDER = "pre_render" - _DEFAULT_METADATA = {_META_PRE_RENDER: False} + _META_LOCAL_STORAGE = "local_storage" + _DEFAULT_METADATA = {_META_PRE_RENDER: False, _META_LOCAL_STORAGE: {}} def __init__(self, gui: "Gui") -> None: self.__gui = gui diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index c9488ddebd..0d4de5266b 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -691,6 +691,8 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None: self.__handle_ws_app_id(message) elif msg_type == _WsType.GET_ROUTES.value: self.__handle_ws_get_routes() + elif msg_type == _WsType.LOCAL_STORAGE.value: + self.__handle_ws_local_storage(message) else: self._manage_external_message(msg_type, message) self.__send_ack(message.get("ack_id")) @@ -1281,6 +1283,27 @@ def __handle_ws_get_routes(self): send_back_only=True, ) + def __handle_ws_local_storage(self, message: t.Any): + if not isinstance(message, dict): + return + name = message.get("name", "") + payload = message.get("payload", None) + scope_metadata = self._get_data_scope_metadata() + if payload is None: + return + if name == "init": + scope_metadata[_DataScopes._META_LOCAL_STORAGE] = payload + elif name == "update": + key = payload.get("key", "") + value = payload.get("value", None) + if value is None: + del scope_metadata[_DataScopes._META_LOCAL_STORAGE][key] + else: + scope_metadata[_DataScopes._META_LOCAL_STORAGE][key] = value + + def _get_local_storage(self): + return self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE] + def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None: grouping_message = self.__get_message_grouping() if allow_grouping else None if grouping_message is None: diff --git a/taipy/gui/gui_actions.py b/taipy/gui/gui_actions.py index 979c141562..9e92b683e8 100644 --- a/taipy/gui/gui_actions.py +++ b/taipy/gui/gui_actions.py @@ -440,3 +440,16 @@ def thread_status(name: str, period_s: float, count: int): thread.start() if isinstance(period, int) and period >= 500 and callable(user_status_function): thread_status(thread.name, period / 1000.0, 0) + + +def get_local_storage(state: State) -> t.Optional[t.Dict[str, str]]: + """Get all local storage values + Arguments: + state (State^): The current user state as received in any callback. + Returns: + All local storage values + """ + if state and isinstance(state._gui, Gui): + return state._gui._get_local_storage() + _warn("'get_local_storage()' must be called in the context of a callback.") + return None diff --git a/taipy/gui/types.py b/taipy/gui/types.py index 886f4f19c7..e290a21c36 100644 --- a/taipy/gui/types.py +++ b/taipy/gui/types.py @@ -53,6 +53,7 @@ class _WsType(Enum): GET_ROUTES = "GR" FAVICON = "FV" BROADCAST = "BC" + LOCAL_STORAGE = "LS" NumberTypes = {"int", "int64", "float", "float64"} @@ -158,8 +159,7 @@ class PropertyType(Enum): @t.overload # noqa: F811 -def _get_taipy_type(a_type: None) -> None: - ... +def _get_taipy_type(a_type: None) -> None: ... @t.overload @@ -175,8 +175,7 @@ def _get_taipy_type(a_type: PropertyType) -> t.Type[_TaipyBase]: # noqa: F811 @t.overload def _get_taipy_type( # noqa: F811 a_type: t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]], -) -> t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]]: - ... +) -> t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]]: ... def _get_taipy_type( # noqa: F811 From 66feb622219e86c7bf4ae1dc62abbc9b11c6a60b Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 12 Nov 2024 10:37:05 +0700 Subject: [PATCH 3/8] add on_loca_storage_change --- taipy/gui/data/data_scope.py | 10 +++++++-- taipy/gui/gui.py | 40 ++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/taipy/gui/data/data_scope.py b/taipy/gui/data/data_scope.py index 1b8bc055b7..2bbe192d7f 100644 --- a/taipy/gui/data/data_scope.py +++ b/taipy/gui/data/data_scope.py @@ -31,10 +31,16 @@ def __init__(self, gui: "Gui") -> None: self.__scopes: t.Dict[str, SimpleNamespace] = {_DataScopes._GLOBAL_ID: SimpleNamespace()} # { scope_name: { metadata: value } } self.__scopes_metadata: t.Dict[str, t.Dict[str, t.Any]] = { - _DataScopes._GLOBAL_ID: _DataScopes._DEFAULT_METADATA.copy() + _DataScopes._GLOBAL_ID: _DataScopes._get_new_default_metadata() } self.__single_client = True + @staticmethod + def _get_new_default_metadata() -> t.Dict[str, t.Any]: + metadata = _DataScopes._DEFAULT_METADATA.copy() + metadata[_DataScopes._META_LOCAL_STORAGE] = {} + return metadata + def set_single_client(self, value: bool) -> None: self.__single_client = value @@ -67,7 +73,7 @@ def create_scope(self, id: str) -> None: return if id not in self.__scopes: self.__scopes[id] = SimpleNamespace() - self.__scopes_metadata[id] = _DataScopes._DEFAULT_METADATA.copy() + self.__scopes_metadata[id] = _DataScopes._get_new_default_metadata() # Propagate shared variables to the new scope from the global scope for var in self.__gui._get_shared_variables(): if hasattr(self.__scopes[_DataScopes._GLOBAL_ID], var): diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 0d4de5266b..be4cf3aa79 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -358,6 +358,19 @@ def __init__( The returned HTML content can therefore use both the variables stored in the *state* and the parameters provided in the call to `get_user_content_url()^`. """ + self.on_local_storage_change: t.Optional[t.Callable] = None + """The function that is called when the local storage is modified. + + It defaults to the `on_local_storage_change()` global function defined in the Python + application. If there is no such function, local storage modifications will not trigger + anything.
+ + The signature of the *on_local_storage_change* callback function must be: + + - *state*: the `State^` instance of the caller. + - *key*: the key of the local storage item that was modified. + - *value*: the new value of the local storage item. + """ # sid from client_id self.__client_id_2_sid: t.Dict[str, t.Set[str]] = {} @@ -1288,18 +1301,32 @@ def __handle_ws_local_storage(self, message: t.Any): return name = message.get("name", "") payload = message.get("payload", None) - scope_metadata = self._get_data_scope_metadata() + scope_meta_ls = self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE] + updated_items = {} if payload is None: return if name == "init": - scope_metadata[_DataScopes._META_LOCAL_STORAGE] = payload + for key, value in payload.items(): + if value is not None and scope_meta_ls.get(key) != value: + scope_meta_ls[key] = value + updated_items[key] = value elif name == "update": key = payload.get("key", "") value = payload.get("value", None) - if value is None: - del scope_metadata[_DataScopes._META_LOCAL_STORAGE][key] - else: - scope_metadata[_DataScopes._META_LOCAL_STORAGE][key] = value + if value is None and key in scope_meta_ls: + del scope_meta_ls[key] + updated_items[key] = None + if value is not None and scope_meta_ls.get(key) != value: + scope_meta_ls[key] = value + updated_items[key] = value + # Call the on_local_storage_change function + if hasattr(self, "on_local_storage_change") and _is_function(self.on_local_storage_change): + try: + for key, value in updated_items.items(): + self._call_function_with_state(t.cast(t.Callable, self.on_local_storage_change), [key, value]) + except Exception as e: # pragma: no cover + if not self._call_on_exception("on_local_storage_change", e): + _warn("Exception raised in on_local_storage_change()", e) def _get_local_storage(self): return self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE] @@ -2644,6 +2671,7 @@ def __bind_default_function(self): self.__bind_local_func("on_exception") self.__bind_local_func("on_status") self.__bind_local_func("on_user_content") + self.__bind_local_func("on_local_storage_change") def __register_blueprint(self): # add en empty main page if it is not defined From a960e78f886688b96bb3a70cc5022e6c9c89181d Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 12 Nov 2024 10:50:42 +0700 Subject: [PATCH 4/8] update get_local_storage function --- taipy/gui/gui.py | 15 +++++++++++++-- taipy/gui/gui_actions.py | 7 ++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index be4cf3aa79..ed280dec78 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -1328,8 +1328,19 @@ def __handle_ws_local_storage(self, message: t.Any): if not self._call_on_exception("on_local_storage_change", e): _warn("Exception raised in on_local_storage_change()", e) - def _get_local_storage(self): - return self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE] + def _get_local_storage(self, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: + if not keys: + return None + if len(keys) == 1: + if keys[0] in self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE]: + return self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE][keys[0]] + return None + # case of multiple keys + ls_items = {} + for key in keys: + if key in self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE]: + ls_items[key] = self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE][key] + return ls_items def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None: grouping_message = self.__get_message_grouping() if allow_grouping else None diff --git a/taipy/gui/gui_actions.py b/taipy/gui/gui_actions.py index 9e92b683e8..58a69c82d3 100644 --- a/taipy/gui/gui_actions.py +++ b/taipy/gui/gui_actions.py @@ -442,14 +442,15 @@ def thread_status(name: str, period_s: float, count: int): thread_status(thread.name, period / 1000.0, 0) -def get_local_storage(state: State) -> t.Optional[t.Dict[str, str]]: - """Get all local storage values +def get_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: + """Get local storage value(s). Arguments: state (State^): The current user state as received in any callback. + *keys (string): The keys to get from the local storage Returns: All local storage values """ if state and isinstance(state._gui, Gui): - return state._gui._get_local_storage() + return state._gui._get_local_storage(*keys) _warn("'get_local_storage()' must be called in the context of a callback.") return None From 0c2fb22baf814a29bb194a78ff41a820ee29c31e Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 15 Nov 2024 15:08:23 +0700 Subject: [PATCH 5/8] update local storage simplify --- .../taipy-gui/src/context/taipyReducers.ts | 13 +--- .../src/hooks/useLocalStorageWithEvent.ts | 70 +------------------ taipy/gui/gui.py | 55 +++------------ 3 files changed, 11 insertions(+), 127 deletions(-) diff --git a/frontend/taipy-gui/src/context/taipyReducers.ts b/frontend/taipy-gui/src/context/taipyReducers.ts index 9761da1024..e8307db821 100644 --- a/frontend/taipy-gui/src/context/taipyReducers.ts +++ b/frontend/taipy-gui/src/context/taipyReducers.ts @@ -49,7 +49,6 @@ export enum Types { Acknowledgement = "ACKNOWLEDGEMENT", Broadcast = "BROADCAST", LocalStorage = "LOCAL_STORAGE", - LocalStorageUpdate = "LOCAL_STORAGE_UPDATE", } /** @@ -511,7 +510,6 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta ackId = sendWsMessage(state.socket, "RU", action.name, action.payload, state.id, action.context); break; case Types.LocalStorage: - case Types.LocalStorageUpdate: ackId = sendWsMessage(state.socket, "LS", action.name, action.payload, state.id, action.context); break; } @@ -892,15 +890,6 @@ export const createPartialAction = (name: string, create: boolean): TaipyPartial export const createLocalStorageAction = (localStorageData: Record): TaipyAction => ({ type: Types.LocalStorage, - name: "init", + name: "", payload: localStorageData, }); - -export const createLocalStorageUpdateAction = (key: string, value: string | null): TaipyAction => ({ - type: Types.LocalStorageUpdate, - name: "update", - payload: { - key: key, - value: value, - }, -}); diff --git a/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts b/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts index 4925c47b53..267dc7a253 100644 --- a/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts +++ b/frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts @@ -12,77 +12,9 @@ */ import { Dispatch, useEffect } from "react"; -import { createLocalStorageAction, createLocalStorageUpdateAction, TaipyBaseAction } from "../context/taipyReducers"; - -const STORAGE_EVENT = "storage"; -const CUSTOM_LOCAL_STORAGE_EVENT = "local-storage"; +import { createLocalStorageAction, TaipyBaseAction } from "../context/taipyReducers"; export const useLocalStorageWithEvent = (dispatch: Dispatch) => { - // Override the original setItem and removeItem behaviour for localStorage to dispatch a custom storage event for local tab - useEffect(() => { - // Preserve the original setItem and removeItem method - const _setItem = Storage.prototype.setItem; - const _removeItem = Storage.prototype.removeItem; - - Storage.prototype.setItem = function (key, value) { - if (this === window.localStorage) { - const oldValue = localStorage.getItem(key); - _setItem.call(this, key, value); - - const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, { - detail: { key, oldValue, newValue: value }, - }); - window.dispatchEvent(customEvent); - } else { - _setItem.call(this, key, value); - } - }; - - Storage.prototype.removeItem = function (key: string) { - if (this === window.localStorage) { - const oldValue = localStorage.getItem(key); - _removeItem.call(this, key); - - const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, { - detail: { key, oldValue, newValue: null }, - }); - window.dispatchEvent(customEvent); - } else { - _removeItem.call(this, key); - } - }; - - // Cleanup the override on unmount - return () => { - Storage.prototype.setItem = _setItem; - Storage.prototype.removeItem = _removeItem; - }; - }, []); - - // addEventListener for storage and custom storage event - useEffect(() => { - const handleStorageEvent = ( - event: StorageEvent | CustomEvent<{ key: string; oldValue: string | null; newValue: string | null }>, - ) => { - const isCustomEvent = event instanceof CustomEvent; - const key = isCustomEvent ? event.detail.key : event.key; - const newValue = isCustomEvent ? event.detail.newValue : event.newValue; - if (!key) { - return; - } - dispatch(createLocalStorageUpdateAction(key, newValue)); - }; - - window.addEventListener(STORAGE_EVENT, handleStorageEvent as EventListener); - window.addEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener); - - // Cleanup event listener on unmount - return () => { - window.removeEventListener(STORAGE_EVENT, handleStorageEvent as EventListener); - window.removeEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener); - }; - }, [dispatch]); // Not necessary to add dispatch to the dependency array but comply with eslint warning anyway - // send all localStorage data to backend on init useEffect(() => { const localStorageData: Record = {}; diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 1d44599381..190baaf3b0 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -358,19 +358,6 @@ def __init__( The returned HTML content can therefore use both the variables stored in the *state* and the parameters provided in the call to `get_user_content_url()^`. """ - self.on_local_storage_change: t.Optional[t.Callable] = None - """The function that is called when the local storage is modified. - - It defaults to the `on_local_storage_change()` global function defined in the Python - application. If there is no such function, local storage modifications will not trigger - anything.
- - The signature of the *on_local_storage_change* callback function must be: - - - *state*: the `State^` instance of the caller. - - *key*: the key of the local storage item that was modified. - - *value*: the new value of the local storage item. - """ # sid from client_id self.__client_id_2_sid: t.Dict[str, t.Set[str]] = {} @@ -1030,17 +1017,17 @@ def __upload_files(self): complete = part == total - 1 # Extract upload path (when single file is selected, path="" does not change the path) - upload_root = os.path.abspath( self._get_config( "upload_folder", tempfile.gettempdir() ) ) - upload_path = os.path.abspath( os.path.join( upload_root, os.path.dirname(path) ) ) - if upload_path.startswith( upload_root ): - upload_path = Path( upload_path ).resolve() - os.makedirs( upload_path, exist_ok=True ) + upload_root = os.path.abspath(self._get_config("upload_folder", tempfile.gettempdir())) + upload_path = os.path.abspath(os.path.join(upload_root, os.path.dirname(path))) + if upload_path.startswith(upload_root): + upload_path = Path(upload_path).resolve() + os.makedirs(upload_path, exist_ok=True) # Save file into upload_path directory file_path = _get_non_existent_file_path(upload_path, secure_filename(file.filename)) - file.save( os.path.join( upload_path, (file_path.name + suffix) ) ) + file.save(os.path.join(upload_path, (file_path.name + suffix))) else: _warn(f"upload files: Path {path} points outside of upload root.") - return("upload files: Path part points outside of upload root.", 400) + return ("upload files: Path part points outside of upload root.", 400) if complete: if part > 0: @@ -1076,9 +1063,7 @@ def __upload_files(self): if not _is_function(file_fn): file_fn = _getscopeattr(self, on_upload_action) if _is_function(file_fn): - self._call_function_with_state( - t.cast(t.Callable, file_fn), ["file_upload", {"args": [data]}] - ) + self._call_function_with_state(t.cast(t.Callable, file_fn), ["file_upload", {"args": [data]}]) else: setattr(self._bindings(), var_name, newvalue) return ("", 200) @@ -1315,34 +1300,13 @@ def __handle_ws_get_routes(self): def __handle_ws_local_storage(self, message: t.Any): if not isinstance(message, dict): return - name = message.get("name", "") payload = message.get("payload", None) scope_meta_ls = self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE] - updated_items = {} if payload is None: return - if name == "init": - for key, value in payload.items(): - if value is not None and scope_meta_ls.get(key) != value: - scope_meta_ls[key] = value - updated_items[key] = value - elif name == "update": - key = payload.get("key", "") - value = payload.get("value", None) - if value is None and key in scope_meta_ls: - del scope_meta_ls[key] - updated_items[key] = None + for key, value in payload.items(): if value is not None and scope_meta_ls.get(key) != value: scope_meta_ls[key] = value - updated_items[key] = value - # Call the on_local_storage_change function - if hasattr(self, "on_local_storage_change") and _is_function(self.on_local_storage_change): - try: - for key, value in updated_items.items(): - self._call_function_with_state(t.cast(t.Callable, self.on_local_storage_change), [key, value]) - except Exception as e: # pragma: no cover - if not self._call_on_exception("on_local_storage_change", e): - _warn("Exception raised in on_local_storage_change()", e) def _get_local_storage(self, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: if not keys: @@ -2698,7 +2662,6 @@ def __bind_default_function(self): self.__bind_local_func("on_exception") self.__bind_local_func("on_status") self.__bind_local_func("on_user_content") - self.__bind_local_func("on_local_storage_change") def __register_blueprint(self): # add en empty main page if it is not defined From eb1895c53508cd800ade9db2801d96f05c7fee79 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 15 Jan 2025 09:39:38 +0100 Subject: [PATCH 6/8] Improve documentation --- taipy/gui/__init__.py | 2 +- taipy/gui/gui.py | 2 +- taipy/gui/gui_actions.py | 23 +++++++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/taipy/gui/__init__.py b/taipy/gui/__init__.py index e8571749e7..6c25a08452 100644 --- a/taipy/gui/__init__.py +++ b/taipy/gui/__init__.py @@ -78,7 +78,6 @@ from .gui_actions import ( broadcast_callback, download, - get_local_storage, get_module_context, get_module_name_from_state, get_state_id, @@ -88,6 +87,7 @@ invoke_long_callback, navigate, notify, + query_local_storage, resume_control, ) from .icon import Icon diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 2a799acb5b..63f70a3fde 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -1345,7 +1345,7 @@ def __handle_ws_local_storage(self, message: t.Any): if value is not None and scope_meta_ls.get(key) != value: scope_meta_ls[key] = value - def _get_local_storage(self, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: + def _query_local_storage(self, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: if not keys: return None if len(keys) == 1: diff --git a/taipy/gui/gui_actions.py b/taipy/gui/gui_actions.py index 05e59017e6..9ae2c5ecaf 100644 --- a/taipy/gui/gui_actions.py +++ b/taipy/gui/gui_actions.py @@ -444,15 +444,26 @@ def thread_status(name: str, period_s: float, count: int): thread_status(thread.name, period / 1000.0, 0) -def get_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: - """Get local storage value(s). +def query_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]: + """Retrieve values from the browser's local storage. + + This function queries the local storage of the client identified by *state* and returns the + values associated with the specified keys. Local storage is a key-value store available in the + user's browser, typically manipulated by client-side code. + Arguments: state (State^): The current user state as received in any callback. - *keys (string): The keys to get from the local storage + *keys (string): One or more keys to retrieve values for from the client's local storage. + Returns: - All local storage values + The requested values from the browser's local storage.
+ - If a single key is provided (*keys* has a single element), this function returns the + corresponding value as a string.
+ - If multiple keys are provided, this function returns a dictionary mapping each key to its + value in the client's local storage.
+ If no value is found for a key, that key will not appear in the dictionary. """ if state and isinstance(state._gui, Gui): - return state._gui._get_local_storage(*keys) - _warn("'get_local_storage()' must be called in the context of a callback.") + return state._gui._query_local_storage(*keys) + _warn("'query_local_storage()' must be called in the context of a callback.") return None From b99f8b850257f32e398e1a3218c4f070cac5e93e Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 15 Jan 2025 10:19:46 +0100 Subject: [PATCH 7/8] Doc formatting --- taipy/gui/__init__.py | 1 + taipy/gui/gui_actions.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/taipy/gui/__init__.py b/taipy/gui/__init__.py index 6c25a08452..e3a04c72b5 100644 --- a/taipy/gui/__init__.py +++ b/taipy/gui/__init__.py @@ -77,6 +77,7 @@ from ._renderers.json import JsonAdapter from .gui_actions import ( broadcast_callback, + close_notification, download, get_module_context, get_module_name_from_state, diff --git a/taipy/gui/gui_actions.py b/taipy/gui/gui_actions.py index 9ae2c5ecaf..67262fdc86 100644 --- a/taipy/gui/gui_actions.py +++ b/taipy/gui/gui_actions.py @@ -456,12 +456,13 @@ def query_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.D *keys (string): One or more keys to retrieve values for from the client's local storage. Returns: - The requested values from the browser's local storage.
+ The requested values from the browser's local storage. + - If a single key is provided (*keys* has a single element), this function returns the - corresponding value as a string.
+ corresponding value as a string.
- If multiple keys are provided, this function returns a dictionary mapping each key to its - value in the client's local storage.
- If no value is found for a key, that key will not appear in the dictionary. + value in the client's local storage.
+ If no value is found for a key, that key will not appear in the dictionary. """ if state and isinstance(state._gui, Gui): return state._gui._query_local_storage(*keys) From d614a4f4ff53b2b3face4de4fbcca3c48b98b0df Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Wed, 15 Jan 2025 12:18:38 +0100 Subject: [PATCH 8/8] Fix RefMan generation bug. --- taipy/gui/gui_actions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/taipy/gui/gui_actions.py b/taipy/gui/gui_actions.py index 67262fdc86..ef9c45130f 100644 --- a/taipy/gui/gui_actions.py +++ b/taipy/gui/gui_actions.py @@ -226,8 +226,8 @@ def get_state_id(state: State) -> t.Optional[str]: state (State^): The current user state as received in any callback. Returns: - A string that uniquely identifies the state. If this value None, it indicates that *state* is not - handled by a `Gui^` instance. + A string that uniquely identifies the state.
+ If this value None, it indicates that *state* is not handled by a `Gui^` instance. """ if state and isinstance(state._gui, Gui): return state._gui._get_client_id() @@ -241,7 +241,7 @@ def get_module_context(state: State) -> t.Optional[str]: state (State^): The current user state as received in any callback. Returns: - The name of the current module + The name of the current module. """ if state and isinstance(state._gui, Gui): return state._gui._get_locals_context() @@ -458,11 +458,11 @@ def query_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.D Returns: The requested values from the browser's local storage. - - If a single key is provided (*keys* has a single element), this function returns the - corresponding value as a string.
- - If multiple keys are provided, this function returns a dictionary mapping each key to its - value in the client's local storage.
- If no value is found for a key, that key will not appear in the dictionary. + - If a single key is provided (*keys* has a single element), this function returns the + corresponding value as a string. + - If multiple keys are provided, this function returns a dictionary mapping each key to + its value in the client's local storage. + - If no value is found for a key, that key will not appear in the dictionary. """ if state and isinstance(state._gui, Gui): return state._gui._query_local_storage(*keys)