Skip to content

feat: persist editor state on page refresh#619

Closed
Shubh-Raj wants to merge 10 commits intoaccordproject:mainfrom
Shubh-Raj:feat/editor-persistence
Closed

feat: persist editor state on page refresh#619
Shubh-Raj wants to merge 10 commits intoaccordproject:mainfrom
Shubh-Raj:feat/editor-persistence

Conversation

@Shubh-Raj
Copy link
Contributor

Closes #618

Implement localStorage persistence for editor content (Template, Model, and Data) to prevent data loss on page refresh. State is automatically saved with debouncing and loaded on init with proper priority handling.

Changes

  • Add EDITOR_STATE_KEY constant and persistence helper functions (getInitialEditorState, saveEditorState)
  • Implement debounced auto-save (1000ms delay) triggered on all editor changes
  • Modify init() to load from localStorage with priority: URL params > localStorage > default sample
  • Add nullish coalescing guards to prevent undefined fields from overwriting defaults
  • Implement resetToDefault() action that clears localStorage and reloads default sample
  • Add "Reset to Default" button in PlaygroundSidebar with confirmation dialog
  • Handle QuotaExceededError gracefully by clearing old state and retrying
  • Add Modal.confirm before reset with warning about data loss

Flags

  • Auto-save is always enabled - No toggle implemented to keep PR focused (can be added in follow-up if needed)
  • All existing 34 tests pass and manual testing completed locally
  • Uses same localStorage pattern as existing theme/UI state persistence

Screenshots or Video

Manual testing verified:

  • Editor content persists across page refresh
  • Reset button clears state and shows confirmation dialog
  • URL Share links take priority over localStorage
  • Debounce prevents excessive localStorage writes
  • Quota errors handled gracefully

Can be tested on the preview link.

Related Issues

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Merging to main from fork:branchname

Implement localStorage persistence for editor content (template, model, and data) to prevent data loss on page refresh. State is saved with 1s debounce and loaded on init if no URL param exists.

Changes:
- Add EDITOR_STATE_KEY constant
- Create getInitialEditorState() and saveEditorState() helpers
- Debounce saves with 1000ms delay
- Modify init() to load from localStorage (priority: URL > localStorage > default)
- Add saveEditorStateDeBounced() calls to 6 setter functions

Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Use nullish coalescing when restoring from localStorage to prevent
undefined fields from overwriting playground defaults. This ensures
rebuild() won't fail if savedState has missing fields.

Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Add resetToDefault() action to store that clears localStorage and reloads the default playground sample. Add Reset button to PlaygroundSidebar with RefreshCcw icon positioned between Share and Start Tour buttons.

Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Wrap localStorage.setItem in try/catch to handle QuotaExceededError.
On quota exceeded, clear old editor state and retry once. Log errors
to console for debugging.

Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
Add Modal.confirm before resetToDefault to prevent accidental data loss.
Shows warning message with danger-styled OK button. User must explicitly
confirm before clearing saved work.

Signed-off-by: Shubh-Raj <shubhraj625@gmail.com>
@Shubh-Raj Shubh-Raj requested a review from a team as a code owner January 23, 2026 11:02
@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for ap-template-playground ready!

Name Link
🔨 Latest commit 49b0608
🔍 Latest deploy log https://app.netlify.com/projects/ap-template-playground/deploys/699eacf7973805000817aa7c
😎 Deploy Preview https://deploy-preview-619--ap-template-playground.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Shubh-Raj
Copy link
Contributor Author

Resolved the merge conflicts.

Copilot AI review requested due to automatic review settings February 25, 2026 08:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds localStorage-backed persistence for the Template/Model/Data editors so work survives a page refresh, and introduces a UI action to clear saved content and return to the default playground sample.

Changes:

  • Add editor-state persistence helpers + debounced auto-save for editor fields.
  • Update store initialization to restore editor content from localStorage when no share-link data is present.
  • Add a “Reset” sidebar action with a confirmation dialog to clear persisted editor state and reload defaults.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/store/store.ts Implements localStorage read/write helpers, debounced persistence, init-time restore, and a resetToDefault action.
src/components/PlaygroundSidebar.tsx Adds a “Reset” nav item that confirms and invokes resetToDefault.

Comment on lines +189 to +190
const saveEditorStateDeBounced = debounce(saveEditorState, 1000);

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds new persistent editor behavior + a new reset action, but there are existing unit tests for the store and localStorage-backed settings. Please add/extend store tests to cover: (1) debounced writes to editor-state, (2) init restoring state, and (3) resetToDefault clearing storage (including the case where a debounced save is pending).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +165 to +169
{
title: "Reset",
icon: FiRefreshCcw,
onClick: () => void handleReset()
},
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new "Reset" sidebar action is user-facing and destructive; there are existing component tests for PlaygroundSidebar, but they don't cover rendering/clicking this new item or asserting the confirmation flow. Please extend the sidebar test to include the Reset button and verify it calls Modal.confirm and triggers resetToDefault on confirmation.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +143 to +152
/* --- Helper to safely load editor state --- */
const getInitialEditorState = () => {
if(typeof window !== 'undefined'){
try{
const saved = localStorage.getItem(EDITOR_STATE_KEY);
if(saved){
return JSON.parse(saved);
}
} catch(e){
/* ignore */
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInitialEditorState currently returns the raw JSON.parse() result, which is typed as any. That makes the function effectively return any (the null branch is erased), and it also means malformed/legacy saved values can silently flow into the store without shape validation. Consider defining a PersistedEditorState interface (only the 6 persisted fields), parsing as unknown, validating/narrowing, and returning PersistedEditorState | null.

Suggested change
/* --- Helper to safely load editor state --- */
const getInitialEditorState = () => {
if(typeof window !== 'undefined'){
try{
const saved = localStorage.getItem(EDITOR_STATE_KEY);
if(saved){
return JSON.parse(saved);
}
} catch(e){
/* ignore */
interface PersistedEditorState {
readonly editorValue?: string;
readonly templateMarkdown?: string;
readonly editorModelCto?: string;
readonly modelCto?: string;
readonly data?: string;
readonly editorAgreementData?: string;
}
const isPersistedEditorState = (value: unknown): value is PersistedEditorState => {
if (value === null || typeof value !== "object") {
return false;
}
const candidate = value as Record<string, unknown>;
const isStringOrUndefined = (v: unknown): v is string | undefined =>
typeof v === "string" || typeof v === "undefined";
return (
(!("editorValue" in candidate) || isStringOrUndefined(candidate.editorValue)) &&
(!("templateMarkdown" in candidate) || isStringOrUndefined(candidate.templateMarkdown)) &&
(!("editorModelCto" in candidate) || isStringOrUndefined(candidate.editorModelCto)) &&
(!("modelCto" in candidate) || isStringOrUndefined(candidate.modelCto)) &&
(!("data" in candidate) || isStringOrUndefined(candidate.data)) &&
(!("editorAgreementData" in candidate) || isStringOrUndefined(candidate.editorAgreementData))
);
};
/* --- Helper to safely load editor state --- */
const getInitialEditorState = (): PersistedEditorState | null => {
if (typeof window !== "undefined") {
try {
const saved = localStorage.getItem(EDITOR_STATE_KEY);
if (saved) {
const parsed: unknown = JSON.parse(saved);
if (isPersistedEditorState(parsed)) {
return parsed;
}
}
} catch (e) {
/* ignore */

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +175
} catch (e) {
// Handle quota exceeded error
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
console.warn('localStorage quota exceeded, clearing editor state and retrying');
localStorage.removeItem(EDITOR_STATE_KEY);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quota handling is currently gated on e.name === 'QuotaExceededError'. In practice, quota-exceeded failures use different names in some browsers (e.g., Firefox's NS_ERROR_DOM_QUOTA_REACHED) and sometimes only provide a numeric code. Consider broadening the detection (e.g., check DOMException code/name variants) so the graceful fallback runs consistently cross-browser.

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +468
// Clear saved editor state from localStorage
if (typeof window !== 'undefined') {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetToDefault removes the localStorage key, but any in-flight saveEditorStateDeBounced call scheduled from recent edits can still fire after the reset and repopulate editor-state with the pre-reset content. To make reset reliable, cancel/flush the pending debounced save before clearing storage (and/or temporarily disable saving during the reset flow).

Suggested change
// Clear saved editor state from localStorage
if (typeof window !== 'undefined') {
// Cancel any in-flight debounced save to avoid restoring stale editor state
try {
// saveEditorStateDeBounced is created via ts-debounce and exposes a cancel method
saveEditorStateDeBounced.cancel();
} catch {
// If for some reason cancellation fails, continue with reset
}
// Clear saved editor state from localStorage
if (typeof window !== "undefined") {

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +190
const saveEditorStateDeBounced = debounce(saveEditorState, 1000);

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persistence is only triggered from the individual setter actions. Store-driven content changes like loadSample (and potentially other bulk loads) won't update editor-state unless the user edits afterward, so a refresh immediately after loading a sample can restore stale persisted content. Consider invoking the debounced save after bulk state updates that change the editors (e.g., at the end of loadSample).

Copilot uses AI. Check for mistakes.
@mttrbrts
Copy link
Member

Thanks for the PR @Shubh-Raj

We discussed this PR on a working group call and decided to proceed with an explicit import / export behaviour through our Archive format, rather than any browser-based local storage.

There's a partial PR here. #562

@mttrbrts mttrbrts closed this Feb 25, 2026
@Shubh-Raj
Copy link
Contributor Author

Thanks for the PR @Shubh-Raj

We discussed this PR on a working group call and decided to proceed with an explicit import / export behaviour through our Archive format, rather than any browser-based local storage.

There's a partial PR here. #562

Yes, thanks for the discussion. I am working on #562

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Persist Editor State on Page Refresh

3 participants