mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 07:12:18 +10:00
refactor(stylesheet): move Semantic CSS to the browser (#3329)
This commit is contained in:
@@ -17,11 +17,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||
import { useResumeStore } from "@/features/resume/builder/draft";
|
||||
import {
|
||||
lockStylesheetStoreForRestore,
|
||||
replaceStylesheetStoreAfterRestore,
|
||||
unlockStylesheetStoreAfterRestore,
|
||||
} from "@/features/resume/stylesheet/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
import { getResumeErrorMessage } from "@/libs/error-message";
|
||||
import { formatRelativeTime } from "@/libs/locale";
|
||||
@@ -53,26 +48,13 @@ export function BuilderVersionHistory({ resumeId }: BuilderVersionHistoryProps)
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const token = lockStylesheetStoreForRestore(resumeId);
|
||||
if (!token) return;
|
||||
try {
|
||||
const restored = await restoreVersion({ resumeId, versionId });
|
||||
const applied = replaceStylesheetStoreAfterRestore({
|
||||
resumeId,
|
||||
resumeData: restored.resume.data,
|
||||
initial: restored.stylesheetState,
|
||||
token,
|
||||
});
|
||||
if (!applied) {
|
||||
unlockStylesheetStoreAfterRestore(token);
|
||||
return;
|
||||
}
|
||||
replaceResumeFromServer(restored.resume as Resume);
|
||||
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored.resume);
|
||||
replaceResumeFromServer(restored as Resume);
|
||||
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
|
||||
void queryClient.invalidateQueries({ queryKey: orpc.resume.listVersions.queryKey({ input: { resumeId } }) });
|
||||
toast.success(t`Your resume has been restored to the selected version.`);
|
||||
} catch (error) {
|
||||
unlockStylesheetStoreAfterRestore(token);
|
||||
toast.error(getResumeErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
@@ -17,16 +18,8 @@ const resumeMock = vi.hoisted(() => ({
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
data: typeof defaultResumeData;
|
||||
data: ResumeData;
|
||||
},
|
||||
stylesheet: {
|
||||
resumeId: "r1" as string | undefined,
|
||||
mode: "semantic" as "legacy" | "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname {" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
revision: 42,
|
||||
renderDataVersion: 7,
|
||||
},
|
||||
}));
|
||||
|
||||
type SectionBaseProps = {
|
||||
@@ -50,9 +43,6 @@ vi.mock("@/libs/resume/section-title-locale", () => ({
|
||||
vi.mock("@/features/resume/builder/draft", () => ({
|
||||
useResume: () => resumeMock.resume,
|
||||
}));
|
||||
vi.mock("@/features/resume/stylesheet/store", () => ({
|
||||
useStylesheetStore: (selector: (state: typeof resumeMock.stylesheet) => unknown) => selector(resumeMock.stylesheet),
|
||||
}));
|
||||
|
||||
const { ExportSectionBuilder } = await import("./export");
|
||||
|
||||
@@ -61,15 +51,12 @@ beforeAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data: defaultResumeData };
|
||||
resumeMock.stylesheet = {
|
||||
resumeId: "r1",
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname {" },
|
||||
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
revision: 42,
|
||||
renderDataVersion: 7,
|
||||
};
|
||||
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -115,7 +102,7 @@ describe("ExportSectionBuilder", () => {
|
||||
expect(filename).toBe("My Resume.md");
|
||||
});
|
||||
|
||||
it("downloads canonical stylesheet content in JSON without concurrency metadata", async () => {
|
||||
it("downloads the current stylesheet source in JSON", async () => {
|
||||
renderExport();
|
||||
openDialog();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Download JSON" }));
|
||||
@@ -127,13 +114,7 @@ describe("ExportSectionBuilder", () => {
|
||||
expect((blob as Blob).type).toBe("application/json");
|
||||
expect(filename).toBe("My Resume.json");
|
||||
const exported = JSON.parse(await (blob as Blob).text());
|
||||
expect(exported.metadata.stylesheet).toEqual({
|
||||
mode: "semantic",
|
||||
source: resumeMock.stylesheet.source,
|
||||
applied: resumeMock.stylesheet.applied,
|
||||
});
|
||||
expect(JSON.stringify(exported)).not.toContain("revision");
|
||||
expect(JSON.stringify(exported)).not.toContain("renderDataVersion");
|
||||
expect(exported.metadata.stylesheet).toEqual(resumeMock.resume?.data.metadata.stylesheet);
|
||||
});
|
||||
|
||||
it("calls buildDocx and downloads the resulting blob when DOCX is clicked", async () => {
|
||||
@@ -155,12 +136,7 @@ describe("ExportSectionBuilder", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
expect(createResumePdfBlob).toHaveBeenCalledTimes(1);
|
||||
expect(createResumePdfBlob).toHaveBeenCalledWith(defaultResumeData, undefined, undefined, {
|
||||
stylesheet: {
|
||||
mode: "semantic",
|
||||
applied: resumeMock.stylesheet.applied,
|
||||
},
|
||||
});
|
||||
expect(createResumePdfBlob).toHaveBeenCalledWith(resumeMock.resume?.data, undefined, undefined);
|
||||
expect(downloadWithAnchor).toHaveBeenCalledTimes(1);
|
||||
expect(downloadWithAnchor.mock.calls[0]?.[1]).toBe("My Resume.pdf");
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { BuilderLayout } from "./-store/sidebar";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
import { useBuilderResumeUpdateSubscription, useResumeCleanup, useResumeStore } from "@/features/resume/builder/draft";
|
||||
import { initializeStylesheetStore, useStylesheetStore } from "@/features/resume/stylesheet/store";
|
||||
import { orpc } from "@/libs/orpc/client";
|
||||
import { createNoindexFollowMeta } from "@/libs/seo";
|
||||
import { DesktopBuilderShell } from "./-components/desktop-builder-shell";
|
||||
@@ -21,9 +20,6 @@ export const Route = createFileRoute("/builder/$resumeId")({
|
||||
const [layout, resume] = await Promise.all([
|
||||
getBuilderLayout(),
|
||||
context.queryClient.ensureQueryData(orpc.resume.getById.queryOptions({ input: { id: params.resumeId } })),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.resume.stylesheet.getState.queryOptions({ input: { id: params.resumeId } }),
|
||||
),
|
||||
]);
|
||||
|
||||
return { layout, name: resume.name };
|
||||
@@ -40,17 +36,11 @@ function RouteComponent() {
|
||||
|
||||
const { resumeId } = Route.useParams();
|
||||
const { data: resume } = useSuspenseQuery(orpc.resume.getById.queryOptions({ input: { id: resumeId } }));
|
||||
const { data: stylesheet } = useSuspenseQuery(
|
||||
orpc.resume.stylesheet.getState.queryOptions({ input: { id: resumeId } }),
|
||||
);
|
||||
const initializeResumeStore = useResumeStore((state) => state.initialize);
|
||||
const mergeResumeMetadata = useResumeStore((state) => state.mergeResumeMetadata);
|
||||
const isReady = useResumeStore((state) => state.isReady);
|
||||
const initializedResumeId = useResumeStore((state) => state.resumeId);
|
||||
const isInitialized = isReady && initializedResumeId === resumeId;
|
||||
const isStylesheetInitialized = useStylesheetStore((state) => state.resumeId === resumeId);
|
||||
const stylesheetInitialization = useRef({ resume, stylesheet });
|
||||
stylesheetInitialization.current = { resume, stylesheet };
|
||||
|
||||
useResumeCleanup();
|
||||
useBuilderResumeUpdateSubscription();
|
||||
@@ -60,16 +50,6 @@ function RouteComponent() {
|
||||
initializeResumeStore(resume);
|
||||
}, [initializeResumeStore, isInitialized, resume]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return;
|
||||
const initial = stylesheetInitialization.current;
|
||||
return initializeStylesheetStore({
|
||||
resumeId,
|
||||
initial: initial.stylesheet,
|
||||
resumeData: initial.resume.data,
|
||||
});
|
||||
}, [isInitialized, resumeId]);
|
||||
|
||||
useEffect(() => {
|
||||
mergeResumeMetadata(resume);
|
||||
}, [
|
||||
@@ -85,7 +65,7 @@ function RouteComponent() {
|
||||
resume,
|
||||
]);
|
||||
|
||||
if (!isInitialized || !isStylesheetInitialized) return null;
|
||||
if (!isInitialized) return null;
|
||||
|
||||
return <BuilderLayoutShell initialLayout={initialLayout} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user