Fix draft error when logged out
This commit is contained in:
@@ -740,24 +740,24 @@ export function CommunicationMethodsScreen() {
|
||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||
aria-busy={!recommendationsReady}
|
||||
>
|
||||
{recommendationsReady ? (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={comm.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="flexWrap"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
) : null}
|
||||
{recommendationsReady && (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={comm.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="flexWrap"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -739,24 +739,24 @@ export function ConflictManagementScreen() {
|
||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||
aria-busy={!recommendationsReady}
|
||||
>
|
||||
{recommendationsReady ? (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={cm.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="pyramidFive"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
) : null}
|
||||
{recommendationsReady && (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={cm.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="pyramidFive"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -732,24 +732,24 @@ export function MembershipMethodsScreen() {
|
||||
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||
aria-busy={!recommendationsReady}
|
||||
>
|
||||
{recommendationsReady ? (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={mem.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="pyramidFive"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
) : null}
|
||||
{recommendationsReady && (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardClick}
|
||||
expanded={expanded}
|
||||
onToggleExpand={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
hasMore={true}
|
||||
toggleLabel={mem.page.seeAllLink}
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
compactDesktopLayout="pyramidFive"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -766,37 +766,37 @@ export function DecisionApproachesScreen() {
|
||||
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
|
||||
aria-busy={!recommendationsReady}
|
||||
>
|
||||
{recommendationsReady ? (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardSelect}
|
||||
expanded={expanded}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
hasMore={true}
|
||||
toggleLabel={da.cardStack.toggleSeeAll}
|
||||
showLessLabel={da.cardStack.toggleShowLess}
|
||||
title=""
|
||||
description={
|
||||
expanded ? (
|
||||
<>
|
||||
{da.cardStack.expandedStackDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{da.sidebar.descriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{da.cardStack.expandedStackDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
layout="singleStack"
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
className="w-full"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
) : null}
|
||||
{recommendationsReady && (
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardSelect}
|
||||
expanded={expanded}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
hasMore={true}
|
||||
toggleLabel={da.cardStack.toggleSeeAll}
|
||||
showLessLabel={da.cardStack.toggleShowLess}
|
||||
title=""
|
||||
description={
|
||||
expanded ? (
|
||||
<>
|
||||
{da.cardStack.expandedStackDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{da.sidebar.descriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{da.cardStack.expandedStackDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
layout="singleStack"
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
className="w-full"
|
||||
headerLockupSize={mdUp ? "L" : "M"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Create
|
||||
|
||||
@@ -11,6 +11,14 @@ import { isBackendSyncEnabled } from "../../../../lib/create/backendSyncEnabled"
|
||||
*/
|
||||
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 {
|
||||
if (typeof window === "undefined") return false;
|
||||
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”
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
export function prepareFreshCreateFlowEntrySync(): void {
|
||||
export function prepareFreshCreateFlowEntrySync(
|
||||
options: PrepareFreshCreateFlowEntryOptions = {},
|
||||
): void {
|
||||
const signedIn = options.signedIn === true;
|
||||
clearAnonymousCreateFlowStorage();
|
||||
clearCoreValueDetailsLocalStorage();
|
||||
if (!isBackendSyncEnabled()) return;
|
||||
setFreshEntryPending();
|
||||
void deleteServerDraft().finally(clearFreshEntryPending);
|
||||
clearServerDraftWhenSignedIn(signedIn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,10 +79,13 @@ export function prepareFreshCreateFlowEntrySync(): void {
|
||||
* before continuing (e.g. tests, programmatic reset flows). Most click handlers
|
||||
* 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();
|
||||
clearCoreValueDetailsLocalStorage();
|
||||
if (!isBackendSyncEnabled()) return;
|
||||
if (!signedIn || !isBackendSyncEnabled()) return;
|
||||
setFreshEntryPending();
|
||||
try {
|
||||
await deleteServerDraft();
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function ProfilePageClient() {
|
||||
}, [draft, router]);
|
||||
|
||||
const handleStartNewCustomRule = useCallback(() => {
|
||||
prepareFreshCreateFlowEntrySync();
|
||||
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||
router.push("/create/informational");
|
||||
}, [router]);
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ const TopContainer = memo<TopProps>(
|
||||
* (see {@link prepareFreshCreateFlowEntrySync}).
|
||||
*/
|
||||
const handleCreateRuleClick = useCallback(() => {
|
||||
prepareFreshCreateFlowEntrySync();
|
||||
prepareFreshCreateFlowEntrySync({ signedIn: loggedIn });
|
||||
router.push("/create/informational");
|
||||
}, [router]);
|
||||
}, [loggedIn, router]);
|
||||
|
||||
// Schema markup for site navigation
|
||||
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