diff --git a/CHANGELOG.md b/CHANGELOG.md index cbdb5fa317..ed2ea067fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). + +## [UNRELEASED] + +## Fixed +- [#3279](https://github.com/plotly/dash/pull/3279) Fix an issue where persisted values were incorrectly pruned when updated via callback. Now, callback returned values are correctly stored in the persistence storage. Fix [#2678](https://github.com/plotly/dash/issues/2678) + ## [3.0.4] - 2025-04-24 ## Fixed diff --git a/dash/dash-renderer/src/actions/index.js b/dash/dash-renderer/src/actions/index.js index 86649c2d7d..fd8c314d78 100644 --- a/dash/dash-renderer/src/actions/index.js +++ b/dash/dash-renderer/src/actions/index.js @@ -1,4 +1,4 @@ -import {once} from 'ramda'; +import {once, path} from 'ramda'; import {createAction} from 'redux-actions'; import {addRequestedCallbacks} from './callbacks'; import {getAppState} from '../reducers/constants'; @@ -7,6 +7,7 @@ import cookie from 'cookie'; import {validateCallbacksToLayout} from './dependencies'; import {includeObservers, getLayoutCallbacks} from './dependencies_ts'; import {computePaths, getPath} from './paths'; +import {recordUiEdit} from '../persistence'; export const onError = createAction(getAction('ON_ERROR')); export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE')); @@ -17,10 +18,19 @@ export const setHooks = createAction(getAction('SET_HOOKS')); export const setLayout = createAction(getAction('SET_LAYOUT')); export const setPaths = createAction(getAction('SET_PATHS')); export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); -export const updateProps = createAction(getAction('ON_PROP_CHANGE')); export const insertComponent = createAction(getAction('INSERT_COMPONENT')); export const removeComponent = createAction(getAction('REMOVE_COMPONENT')); +export const onPropChange = createAction(getAction('ON_PROP_CHANGE')); + +export function updateProps(payload) { + return (dispatch, getState) => { + const component = path(payload.itempath, getState().layout); + recordUiEdit(component, payload.props, dispatch); + dispatch(onPropChange(payload)); + }; +} + export const addComponentToLayout = payload => (dispatch, getState) => { const {paths} = getState(); dispatch(insertComponent(payload)); diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index f82a24bdf0..85d97299bc 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -11,6 +11,9 @@ import { pathOr } from 'ramda'; +import {ThunkDispatch} from 'redux-thunk'; +import {AnyAction} from 'redux'; + import {IStoreState} from '../store'; import { @@ -63,7 +66,7 @@ const observer: IStoreObserverDefinition = { // In case the update contains whole components, see if any of // those components have props to update to persist user edits. const {props} = applyPersistence({props: updatedProps}, dispatch); - dispatch( + (dispatch as ThunkDispatch)( updateProps({ itempath, props, diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index 1698c09972..c58bb4b916 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -318,7 +318,14 @@ export function recordUiEdit(layout, newProps, dispatch) { persisted_props, persistence_type } = getProps(layout); - if (!canPersist || !persistence) { + + // if the "persistence" property is changed as a callback output, + // skip the persistence storage overwriting. + const isPersistenceMismatch = + newProps?.persistence !== undefined && + newProps.persistence !== persistence; + + if (!canPersist || !persistence || isPersistenceMismatch) { return; } @@ -501,46 +508,21 @@ export function prunePersistence(layout, newProps, dispatch) { depersistedProps = mergeRight(props, update); } - if (finalPersistence) { + if (finalPersistence && persistenceChanged) { const finalStorage = getStore(finalPersistenceType, dispatch); - - if (persistenceChanged) { - // apply new persistence - forEach( - persistedProp => - modProp( - getValsKey(id, persistedProp, finalPersistence), - finalStorage, - element, - depersistedProps, - persistedProp, - update - ), - filter(notInNewProps, finalPersistedProps) - ); - } - - // now the main point - clear any edit of a prop that changed - // note that this is independent of the new prop value. - const transforms = element.persistenceTransforms || {}; - for (const propName in newProps) { - const propTransforms = transforms[propName]; - if (propTransforms) { - for (const propPart in propTransforms) { - finalStorage.removeItem( - getValsKey( - id, - `${propName}.${propPart}`, - finalPersistence - ) - ); - } - } else { - finalStorage.removeItem( - getValsKey(id, propName, finalPersistence) - ); - } - } + // apply new persistence + forEach( + persistedProp => + modProp( + getValsKey(id, persistedProp, finalPersistence), + finalStorage, + element, + depersistedProps, + persistedProp, + update + ), + filter(notInNewProps, finalPersistedProps) + ); } return persistenceChanged ? mergeRight(newProps, update) : newProps; } diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 322978a228..57e152ebcc 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -23,7 +23,6 @@ import {DashLayoutPath, UpdatePropsPayload} from '../types/component'; import {DashConfig} from '../config'; import {notifyObservers, onError, updateProps} from '../actions'; import {getWatchedKeys, stringifyId} from '../actions/dependencies'; -import {recordUiEdit} from '../persistence'; import { createElement, getComponentLayout, @@ -132,10 +131,6 @@ function DashWrapper({ const watchedKeys = getWatchedKeys(id, keys(changedProps), graphs); batch(() => { - // setProps here is triggered by the UI - record these changes - // for persistence - recordUiEdit(renderComponent, newProps, dispatch); - // Only dispatch changes to Dash if a watched prop changed if (watchedKeys.length) { dispatch(