Backend / staging cleanup, performance substrate, and create-flow polish #60

Merged
an.di merged 16 commits from adilallo/Backend/StagingCleanup into main 2026-05-26 15:11:47 +00:00
8 changed files with 203 additions and 95 deletions
Showing only changes of commit 62efb6a0cc - Show all commits
@@ -740,7 +740,7 @@ export function CommunicationMethodsScreen() {
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
aria-busy={!recommendationsReady}
>
{recommendationsReady ? (
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
@@ -757,7 +757,7 @@ export function CommunicationMethodsScreen() {
compactDesktopLayout="flexWrap"
headerLockupSize={mdUp ? "L" : "M"}
/>
) : null}
)}
</div>
</div>
@@ -739,7 +739,7 @@ export function ConflictManagementScreen() {
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
aria-busy={!recommendationsReady}
>
{recommendationsReady ? (
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
@@ -756,7 +756,7 @@ export function ConflictManagementScreen() {
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
) : null}
)}
</div>
</div>
@@ -732,7 +732,7 @@ export function MembershipMethodsScreen() {
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
aria-busy={!recommendationsReady}
>
{recommendationsReady ? (
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
@@ -749,7 +749,7 @@ export function MembershipMethodsScreen() {
compactDesktopLayout="pyramidFive"
headerLockupSize={mdUp ? "L" : "M"}
/>
) : null}
)}
</div>
</div>
@@ -766,7 +766,7 @@ export function DecisionApproachesScreen() {
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
aria-busy={!recommendationsReady}
>
{recommendationsReady ? (
{recommendationsReady && (
<CardStack
cards={sampleCards}
selectedIds={selectedIds}
@@ -796,7 +796,7 @@ export function DecisionApproachesScreen() {
className="w-full"
headerLockupSize={mdUp ? "L" : "M"}
/>
) : null}
)}
</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();
+1 -1
View File
@@ -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);
});
});