-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat: persist unsaved progress in session #9115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ import { applyScriptEnvVars, getScriptModifiedKeys } from 'utils/environments'; | |
| import { getSubdirectoriesFromRoot } from 'utils/common/platform'; | ||
| import toast from 'react-hot-toast'; | ||
| import mime from 'mime-types'; | ||
| import path from 'utils/common/path'; | ||
| import path, { normalizePath } from 'utils/common/path'; | ||
| import { getUniqueTagsFromItems } from 'utils/collections/index'; | ||
| import { DEFAULT_HTTP_ITEM_SETTINGS, GRPC_SCRIPT_KEYS, SCRIPT_TYPES } from '@usebruno/common'; | ||
| import * as exampleReducers from './exampleReducers'; | ||
|
|
@@ -194,6 +194,57 @@ const mergeRequestWithPreservedUids = (existingRequest, newRequest) => | |
| const mergeRootWithPreservedUids = (existingRoot, newRoot) => | ||
| preserveUidsAtPaths(existingRoot, newRoot, ROOT_UID_PATHS); | ||
|
|
||
| const consumePersistedItemDraft = (collection, draftType, pathname) => { | ||
| if (!collection?.persistedDraftSession || !pathname) { | ||
| return null; | ||
| } | ||
|
|
||
| const normalizedPath = normalizePath(pathname); | ||
| const draft = collection.persistedDraftSession?.[draftType]?.[normalizedPath]; | ||
|
|
||
| if (draft) { | ||
| delete collection.persistedDraftSession[draftType][normalizedPath]; | ||
| } | ||
|
|
||
| return draft || null; | ||
| }; | ||
|
|
||
| const applyPersistedFolderDraft = (collection, folderItem, pathname) => { | ||
| if (!folderItem?.draft) { | ||
| const draft = consumePersistedItemDraft(collection, 'folderDrafts', pathname || folderItem?.pathname); | ||
| if (draft) { | ||
| folderItem.draft = draft; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const applyPersistedRequestDraft = (collection, requestItem, pathname) => { | ||
| if (!requestItem?.draft) { | ||
| const draft = consumePersistedItemDraft(collection, 'requestDrafts', pathname || requestItem?.pathname); | ||
| if (draft) { | ||
| requestItem.draft = draft; | ||
|
Comment on lines
+223
to
+225
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline packages/bruno-app/src/providers/ReduxStore/utils/draftSession.js --items all
rg -n -C 4 'safeParse|requestDrafts|folderDrafts|JSON\.parse' \
packages/bruno-app/src/providers/ReduxStore/utils/draftSession.jsRepository: usebruno/bruno Length of output: 2524 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- changed reducer flow ---'
sed -n '180,245p' packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
printf '%s\n' '--- draft session utility ---'
sed -n '100,170p' packages/bruno-app/src/providers/ReduxStore/utils/draftSession.js
printf '%s\n' '--- draft consumers ---'
rg -n -C 5 'draft\.request|draft\?\.request|item\.draft|requestItem\.draft' packages/bruno-app/src --glob '*.{js,jsx,ts,tsx}' | head -n 240Repository: usebruno/bruno Length of output: 25599 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact draft.request consumers ---'
rg -n -C 4 'draft(\?|\.)request|draft\s*\?\s*[^:;]*request' packages/bruno-app/src --glob '*.{js,jsx,ts,tsx}' | head -n 360
printf '%s\n' '--- request execution references ---'
rg -n -C 5 'execute(Request|Collection)?|sendRequest|runRequest|request\.url|request\.method' packages/bruno-app/src --glob '*.{js,jsx,ts,tsx}' | head -n 360Repository: usebruno/bruno Length of output: 50370 Validate persisted request-draft shapes before assignment. If a persisted 🤖 Prompt for AI AgentsSource: Learnings |
||
| } | ||
| } | ||
| }; | ||
|
|
||
| const applyPersistedEnvironmentDraft = (collection, environment) => { | ||
| const persistedDraft = collection?.persistedDraftSession?.environmentsDraft; | ||
| if (collection?.environmentsDraft || !persistedDraft || !environment) { | ||
| return; | ||
| } | ||
|
|
||
| const isMatch = (persistedDraft.environmentUid && persistedDraft.environmentUid === environment.uid) | ||
| || (persistedDraft.environmentName && persistedDraft.environmentName === environment.name); | ||
|
|
||
| if (isMatch) { | ||
| collection.environmentsDraft = { | ||
| environmentUid: environment.uid, | ||
| variables: persistedDraft.variables | ||
| }; | ||
| delete collection.persistedDraftSession.environmentsDraft; | ||
| } | ||
| }; | ||
|
|
||
| const initialState = { | ||
| collections: [], | ||
| collectionSortOrder: 'default', | ||
|
|
@@ -3063,6 +3114,7 @@ export const collectionsSlice = createSlice({ | |
| if (file?.data?.meta?.seq) { | ||
| folderItem.seq = file.data?.meta?.seq; | ||
| } | ||
| applyPersistedFolderDraft(collection, folderItem, folderPath); | ||
| } | ||
| return; | ||
| } | ||
|
|
@@ -3094,6 +3146,7 @@ export const collectionsSlice = createSlice({ | |
| // Update existing folder to be transient if the file is transient | ||
| childItem.isTransient = true; | ||
| } | ||
| applyPersistedFolderDraft(collection, childItem, currentPath); | ||
| currentSubItems = childItem.items; | ||
| } | ||
|
|
||
|
|
@@ -3119,8 +3172,9 @@ export const collectionsSlice = createSlice({ | |
| currentItem.size = file.size; | ||
| currentItem.error = file.error; | ||
| currentItem.isTransient = isTransientFile; | ||
| applyPersistedRequestDraft(collection, currentItem, file.meta.pathname); | ||
| } else { | ||
| currentSubItems.push({ | ||
| const newItem = { | ||
| uid: file.data.uid, | ||
| name: file.data.name, | ||
| type: file.data.type, | ||
|
|
@@ -3139,7 +3193,9 @@ export const collectionsSlice = createSlice({ | |
| size: file.size, | ||
| error: file.error, | ||
| isTransient: isTransientFile | ||
| }); | ||
| }; | ||
| applyPersistedRequestDraft(collection, newItem, file.meta.pathname); | ||
| currentSubItems.push(newItem); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -3191,6 +3247,7 @@ export const collectionsSlice = createSlice({ | |
| // Update existing folder to be transient if the directory is transient | ||
| childItem.isTransient = true; | ||
| } | ||
| applyPersistedFolderDraft(collection, childItem, currentPath); | ||
| currentSubItems = childItem.items; | ||
| }); | ||
| } | ||
|
|
@@ -3218,6 +3275,7 @@ export const collectionsSlice = createSlice({ | |
| folderItem.seq = file?.data?.meta?.seq; | ||
| } | ||
| folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data); | ||
| applyPersistedFolderDraft(collection, folderItem, folderPath); | ||
| } | ||
| return; | ||
| } | ||
|
|
@@ -3281,6 +3339,8 @@ export const collectionsSlice = createSlice({ | |
| item.draft = null; | ||
| } | ||
| } | ||
|
|
||
| applyPersistedRequestDraft(collection, item, file.meta.pathname); | ||
| } | ||
| } | ||
| }, | ||
|
|
@@ -3324,9 +3384,24 @@ export const collectionsSlice = createSlice({ | |
| existingEnv.color = environment.color; | ||
| existingEnv.externalSecrets = environment.externalSecrets; | ||
| existingEnv.extends = environment.extends; | ||
| /* | ||
| Apply temporary (ephemeral) values only to variables that actually exist in the file. This prevents deleted temporaries from “popping back” after a save. If a variable is present in the file, we temporarily override the UI value while also remembering the on-disk value in persistedValue for future saves. | ||
| */ | ||
| prevEphemerals.forEach((ev) => { | ||
| const target = existingEnv.variables?.find((v) => v.name === ev.name); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we store v.name value into hashmap ? this is much faster then find. Example: const variablesByName = new Map( prevEphemerals.forEach((ev) => { if (!target) return; if (target.value !== ev.value) { } target.ephemeral = true; applyPersistedEnvironmentDraft(collection, existingEnv); |
||
| if (target) { | ||
| if (target.value !== ev.value) { | ||
| if (target.persistedValue === undefined) target.persistedValue = target.value; | ||
| target.value = ev.value; | ||
| } | ||
| target.ephemeral = true; | ||
| } | ||
| }); | ||
| applyPersistedEnvironmentDraft(collection, existingEnv); | ||
| } else { | ||
| collection.environments.push(environment); | ||
| collection.environments.sort((a, b) => a.name.localeCompare(b.name)); | ||
| applyPersistedEnvironmentDraft(collection, environment); | ||
|
|
||
| const lastAction = collection.lastAction; | ||
| if (lastAction && lastAction.type === 'ADD_ENVIRONMENT') { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { getDataTypeFromValue, parseValueByDataType, resolveEnvironmentInheritan | |
| import { cloneDeep, isEqual } from 'lodash'; | ||
| import { applyScriptEnvVars, getScriptModifiedKeys, writesCollidingSecrets, DUPLICATE_SECRET_NAMES_ERROR } from 'utils/environments'; | ||
| import { getInvalidVariableNames, invalidVariableNamesError } from 'utils/common/variables'; | ||
| import { getPersistedDraftSession } from 'providers/ReduxStore/utils/draftSession'; | ||
|
|
||
| const initialState = { | ||
| globalEnvironments: [], | ||
|
|
@@ -418,4 +419,29 @@ export const updateGlobalEnvironmentColor = (environmentUid, color) => (dispatch | |
| }); | ||
| }; | ||
|
|
||
| export const restoreGlobalEnvironmentDraftFromSession = () => (dispatch, getState) => { | ||
| const session = getPersistedDraftSession(); | ||
| const persistedDraft = session?.globalEnvironmentDraft; | ||
|
|
||
| if (!persistedDraft) { | ||
| return; | ||
| } | ||
|
|
||
| const state = getState(); | ||
| const globalEnvironments = state.globalEnvironments?.globalEnvironments || []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you fetch frequently, please store into hashmap. find takes time. globalEnvironments value set as map |
||
| const environment = globalEnvironments.find((env) => | ||
| env.uid === persistedDraft.environmentUid | ||
| || (persistedDraft.environmentName && env.name === persistedDraft.environmentName) | ||
| ); | ||
|
Comment on lines
+432
to
+435
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor Reachability: Internal · Exploitability: Moderate Scope global drafts to their workspace. The persisted draft lacks workspace identity. The name fallback can restore Workspace A variables to a same-named environment in Workspace B. Store the workspace UID with the draft and require it to match before restoration. 🤖 Prompt for AI Agents |
||
|
|
||
| if (!environment) { | ||
| return; | ||
| } | ||
|
|
||
| dispatch(setGlobalEnvironmentDraft({ | ||
| environmentUid: environment.uid, | ||
| variables: persistedDraft.variables | ||
| })); | ||
|
Comment on lines
+441
to
+444
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository guidance ---'
head -5 /tmp/coderabbit-repo-knowledge/usebruno-bruno-f320b1f6/*/*.md 2>/dev/null || true
printf '%s\n' '--- target file outline ---'
ast-grep outline packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
printf '%s\n' '--- target restoration path ---'
sed -n '1,80p' packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
sed -n '330,420p' packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
printf '%s\n' '--- direct bindings and callers ---'
rg -n -C 4 'setGlobalEnvironmentDraft|globalEnvironmentDraft|persistedDraft|refresh.*environment|environmentUid' packages/bruno-app/src/providers/ReduxStore packages/bruno-app/src --glob '*.js' --glob '*.jsx'Repository: usebruno/bruno Length of output: 50371 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target file: imports, state, reducers, persistence, restoration ---'
sed -n '1,145p' packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
sed -n '300,395p' packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js
printf '%s\n' '--- exact restoration callers ---'
rg -n -C 8 'restoreGlobalEnvironmentDraftFromSession' packages/bruno-app/src --glob '*.js' --glob '*.jsx'
printf '%s\n' '--- persisted draft writers/readers ---'
rg -n -C 6 'globalEnvironmentDraft|getPersistedDraftSession|persistedDraftSession' packages/bruno-app/src --glob '*.js' --glob '*.jsx' | head -240Repository: usebruno/bruno Length of output: 45245 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- draft session serialization and persistence ---'
sed -n '1,165p' packages/bruno-app/src/providers/ReduxStore/utils/draftSession.js
printf '%s\n' '--- refresh handlers ---'
sed -n '145,225p' packages/bruno-app/src/providers/App/useIpcEvents.js
sed -n '335,358p' packages/bruno-app/src/providers/App/useIpcEvents.js
printf '%s\n' '--- draft restoration tests ---'
rg -n -C 10 'restoreGlobalEnvironmentDraftFromSession|environmentDraft.*Session|localStorage' packages/bruno-app/src/providers/ReduxStore/slices/global-environments.spec.js packages/bruno-app/src/providers/ReduxStore/utils --glob '*.js'Repository: usebruno/bruno Length of output: 15277 Do not overwrite an active global environment draft. The refresh handlers dispatch 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| export default globalEnvironmentsSlice.reducer; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Persist and present standalone app drafts.
Standalone app drafts are collected but excluded from
allDrafts, and they are never serialized. When an app draft is the only unsaved change, the modal treats the session as clean and closes without saving or restoring that code.packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js#L114-L115: includeappDraftsinallDraftsso users can save or discard them.packages/bruno-app/src/providers/ReduxStore/utils/draftSession.js#L71-L78: persist changeditem.type === 'app'drafts through the request-draft restoration path.📍 Affects 2 files
packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js#L114-L115(this comment)packages/bruno-app/src/providers/ReduxStore/utils/draftSession.js#L71-L78🤖 Prompt for AI Agents