Fix draft error when logged out
This commit is contained in:
@@ -740,7 +740,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
aria-busy={!recommendationsReady}
|
aria-busy={!recommendationsReady}
|
||||||
>
|
>
|
||||||
{recommendationsReady ? (
|
{recommendationsReady && (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -757,7 +757,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
compactDesktopLayout="flexWrap"
|
compactDesktopLayout="flexWrap"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -739,7 +739,7 @@ export function ConflictManagementScreen() {
|
|||||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
aria-busy={!recommendationsReady}
|
aria-busy={!recommendationsReady}
|
||||||
>
|
>
|
||||||
{recommendationsReady ? (
|
{recommendationsReady && (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -756,7 +756,7 @@ export function ConflictManagementScreen() {
|
|||||||
compactDesktopLayout="pyramidFive"
|
compactDesktopLayout="pyramidFive"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ export function MembershipMethodsScreen() {
|
|||||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
aria-busy={!recommendationsReady}
|
aria-busy={!recommendationsReady}
|
||||||
>
|
>
|
||||||
{recommendationsReady ? (
|
{recommendationsReady && (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -749,7 +749,7 @@ export function MembershipMethodsScreen() {
|
|||||||
compactDesktopLayout="pyramidFive"
|
compactDesktopLayout="pyramidFive"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ export function DecisionApproachesScreen() {
|
|||||||
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
|
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
|
||||||
aria-busy={!recommendationsReady}
|
aria-busy={!recommendationsReady}
|
||||||
>
|
>
|
||||||
{recommendationsReady ? (
|
{recommendationsReady && (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -796,7 +796,7 @@ export function DecisionApproachesScreen() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Create
|
<Create
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import { isBackendSyncEnabled } from "../../../../lib/create/backendSyncEnabled"
|
|||||||
*/
|
*/
|
||||||
export const FRESH_ENTRY_PENDING_KEY = "create:fresh-entry-pending";
|
export const FRESH_ENTRY_PENDING_KEY = "create:fresh-entry-pending";
|
||||||
|
|
||||||
|
export type PrepareFreshCreateFlowEntryOptions = {
|
||||||
|
/**
|
||||||
|
* When `true`, and backend sync is on, also `DELETE /api/drafts/me`.
|
||||||
|
* Omit or pass `false` for guests — they have no server draft to clear.
|
||||||
|
*/
|
||||||
|
signedIn?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function hasFreshEntryPending(): boolean {
|
export function hasFreshEntryPending(): boolean {
|
||||||
if (typeof window === "undefined") return false;
|
if (typeof window === "undefined") return false;
|
||||||
try {
|
try {
|
||||||
@@ -38,10 +46,17 @@ function clearFreshEntryPending(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearServerDraftWhenSignedIn(signedIn: boolean): void {
|
||||||
|
if (!signedIn || !isBackendSyncEnabled()) return;
|
||||||
|
setFreshEntryPending();
|
||||||
|
void deleteServerDraft().finally(clearFreshEntryPending);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call **before** navigating into `/create` from marketing or profile “new rule”
|
* Call **before** navigating into `/create` from marketing or profile “new rule”
|
||||||
* entry points so signed-in + sync matches an anonymous fresh start: wipe
|
* entry points so signed-in + sync matches an anonymous fresh start: wipe
|
||||||
* `localStorage` draft keys and, when sync is on, `DELETE /api/drafts/me`.
|
* `localStorage` draft keys and, when sync is on and the user is signed in,
|
||||||
|
* `DELETE /api/drafts/me`.
|
||||||
*
|
*
|
||||||
* Synchronous variant: returns immediately after clearing local state and
|
* Synchronous variant: returns immediately after clearing local state and
|
||||||
* scheduling the server draft delete in the background. Sets a sessionStorage
|
* scheduling the server draft delete in the background. Sets a sessionStorage
|
||||||
@@ -50,12 +65,13 @@ function clearFreshEntryPending(): void {
|
|||||||
*
|
*
|
||||||
* Do **not** use for “Continue draft” — that path should load the server draft.
|
* Do **not** use for “Continue draft” — that path should load the server draft.
|
||||||
*/
|
*/
|
||||||
export function prepareFreshCreateFlowEntrySync(): void {
|
export function prepareFreshCreateFlowEntrySync(
|
||||||
|
options: PrepareFreshCreateFlowEntryOptions = {},
|
||||||
|
): void {
|
||||||
|
const signedIn = options.signedIn === true;
|
||||||
clearAnonymousCreateFlowStorage();
|
clearAnonymousCreateFlowStorage();
|
||||||
clearCoreValueDetailsLocalStorage();
|
clearCoreValueDetailsLocalStorage();
|
||||||
if (!isBackendSyncEnabled()) return;
|
clearServerDraftWhenSignedIn(signedIn);
|
||||||
setFreshEntryPending();
|
|
||||||
void deleteServerDraft().finally(clearFreshEntryPending);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,10 +79,13 @@ export function prepareFreshCreateFlowEntrySync(): void {
|
|||||||
* before continuing (e.g. tests, programmatic reset flows). Most click handlers
|
* before continuing (e.g. tests, programmatic reset flows). Most click handlers
|
||||||
* should use {@link prepareFreshCreateFlowEntrySync} for instant navigation.
|
* should use {@link prepareFreshCreateFlowEntrySync} for instant navigation.
|
||||||
*/
|
*/
|
||||||
export async function prepareFreshCreateFlowEntry(): Promise<void> {
|
export async function prepareFreshCreateFlowEntry(
|
||||||
|
options: PrepareFreshCreateFlowEntryOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const signedIn = options.signedIn === true;
|
||||||
clearAnonymousCreateFlowStorage();
|
clearAnonymousCreateFlowStorage();
|
||||||
clearCoreValueDetailsLocalStorage();
|
clearCoreValueDetailsLocalStorage();
|
||||||
if (!isBackendSyncEnabled()) return;
|
if (!signedIn || !isBackendSyncEnabled()) return;
|
||||||
setFreshEntryPending();
|
setFreshEntryPending();
|
||||||
try {
|
try {
|
||||||
await deleteServerDraft();
|
await deleteServerDraft();
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export default function ProfilePageClient() {
|
|||||||
}, [draft, router]);
|
}, [draft, router]);
|
||||||
|
|
||||||
const handleStartNewCustomRule = useCallback(() => {
|
const handleStartNewCustomRule = useCallback(() => {
|
||||||
prepareFreshCreateFlowEntrySync();
|
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||||
router.push("/create/informational");
|
router.push("/create/informational");
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ const TopContainer = memo<TopProps>(
|
|||||||
* (see {@link prepareFreshCreateFlowEntrySync}).
|
* (see {@link prepareFreshCreateFlowEntrySync}).
|
||||||
*/
|
*/
|
||||||
const handleCreateRuleClick = useCallback(() => {
|
const handleCreateRuleClick = useCallback(() => {
|
||||||
prepareFreshCreateFlowEntrySync();
|
prepareFreshCreateFlowEntrySync({ signedIn: loggedIn });
|
||||||
router.push("/create/informational");
|
router.push("/create/informational");
|
||||||
}, [router]);
|
}, [loggedIn, router]);
|
||||||
|
|
||||||
// Schema markup for site navigation
|
// Schema markup for site navigation
|
||||||
const schemaData = {
|
const schemaData = {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
FRESH_ENTRY_PENDING_KEY,
|
||||||
|
hasFreshEntryPending,
|
||||||
|
prepareFreshCreateFlowEntry,
|
||||||
|
prepareFreshCreateFlowEntrySync,
|
||||||
|
} from "../../app/(app)/create/utils/prepareFreshCreateFlowEntry";
|
||||||
|
import { CREATE_FLOW_ANONYMOUS_KEY } from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||||
|
|
||||||
|
const deleteServerDraft = vi.fn();
|
||||||
|
const isBackendSyncEnabled = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../../lib/create/api", () => ({
|
||||||
|
deleteServerDraft: (...args: unknown[]) => deleteServerDraft(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../lib/create/backendSyncEnabled", () => ({
|
||||||
|
isBackendSyncEnabled: () => isBackendSyncEnabled(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("prepareFreshCreateFlowEntrySync", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteServerDraft.mockReset();
|
||||||
|
deleteServerDraft.mockResolvedValue(undefined);
|
||||||
|
isBackendSyncEnabled.mockReturnValue(true);
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears local draft storage for guests without calling deleteServerDraft", () => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
CREATE_FLOW_ANONYMOUS_KEY,
|
||||||
|
JSON.stringify({ title: "Stale" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
prepareFreshCreateFlowEntrySync();
|
||||||
|
|
||||||
|
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||||
|
expect(deleteServerDraft).not.toHaveBeenCalled();
|
||||||
|
expect(hasFreshEntryPending()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears local draft storage and deletes the server draft when signed in", async () => {
|
||||||
|
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||||
|
|
||||||
|
expect(deleteServerDraft).toHaveBeenCalledTimes(1);
|
||||||
|
expect(window.sessionStorage.getItem(FRESH_ENTRY_PENDING_KEY)).toBe("1");
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(hasFreshEntryPending()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips server draft delete when backend sync is disabled", () => {
|
||||||
|
isBackendSyncEnabled.mockReturnValue(false);
|
||||||
|
|
||||||
|
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||||
|
|
||||||
|
expect(deleteServerDraft).not.toHaveBeenCalled();
|
||||||
|
expect(hasFreshEntryPending()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("prepareFreshCreateFlowEntry", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteServerDraft.mockReset();
|
||||||
|
deleteServerDraft.mockResolvedValue(undefined);
|
||||||
|
isBackendSyncEnabled.mockReturnValue(true);
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("awaits deleteServerDraft only when signed in", async () => {
|
||||||
|
await prepareFreshCreateFlowEntry();
|
||||||
|
expect(deleteServerDraft).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await prepareFreshCreateFlowEntry({ signedIn: true });
|
||||||
|
expect(deleteServerDraft).toHaveBeenCalledTimes(1);
|
||||||
|
expect(hasFreshEntryPending()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user