feat(career): clarify workspace actions
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
|
||||
import { cvBuilderApi } from "./cvBuilder";
|
||||
import CareerWorkspaceOverview, { CareerWorkspaceImportRun } from "./views/career/CareerWorkspaceOverview";
|
||||
|
||||
jest.mock("./cvBuilder", () => ({
|
||||
cvBuilderApi: { list: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockedList = cvBuilderApi.list as jest.MockedFunction<typeof cvBuilderApi.list>;
|
||||
|
||||
function renderOverview({
|
||||
percent = 0,
|
||||
missing = [],
|
||||
runs = [],
|
||||
loading = false,
|
||||
loadError = null,
|
||||
}: {
|
||||
percent?: number;
|
||||
missing?: string[];
|
||||
runs?: CareerWorkspaceImportRun[];
|
||||
loading?: boolean;
|
||||
loadError?: string | null;
|
||||
} = {}) {
|
||||
return render(
|
||||
<CareerWorkspaceOverview
|
||||
completeness={{ percent, missing }}
|
||||
runs={runs}
|
||||
loading={loading}
|
||||
loadError={loadError}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockedList.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("first-run workspace presents concise profile, import, general CV and job-specific actions", async () => {
|
||||
renderOverview();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Career Workspace", level: 1 })).toBeInTheDocument();
|
||||
expect(await screen.findByText(/start by adding career information manually or importing a cv/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/job-specific CVs stay separate and never overwrite/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Edit career profile" })).toHaveAttribute("href", "#career-profile-editor");
|
||||
expect(screen.getAllByRole("link", { name: "Import a CV" })[0]).toHaveAttribute("href", "#career-cv-import");
|
||||
expect(screen.getByRole("link", { name: "Create a general CV" })).toHaveAttribute("href", "/career/builder");
|
||||
expect(screen.getByRole("link", { name: "Choose a job" })).toHaveAttribute("href", "/jobs");
|
||||
expect(await screen.findByText("No CV documents yet.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("returning workspace shows missing profile information and recent general and job-specific CVs", async () => {
|
||||
mockedList.mockResolvedValue([
|
||||
{ id: 8, name: "Backend CV", themeId: "modern", publicSlug: "backend", isPublic: false, version: 2, jobApplicationId: 42, updatedAtUtc: "2026-08-08T10:00:00Z" },
|
||||
{ id: 7, name: "General CV", themeId: "modern", publicSlug: "general", isPublic: false, version: 1, jobApplicationId: null, updatedAtUtc: "2026-08-07T10:00:00Z" },
|
||||
]);
|
||||
|
||||
renderOverview({ percent: 70, missing: ["Projects", "Education", "Languages", "Interests"] });
|
||||
|
||||
expect(screen.getByText("70%")).toBeInTheDocument();
|
||||
expect(screen.getByText("Add next: Projects, Education, Languages")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Backend CV")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("General CV").length).toBeGreaterThan(1);
|
||||
expect(screen.getByText("Job-specific")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("link", { name: "Open", exact: true })[0]).toHaveAttribute("href", "/career/builder/8");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["pending_review", "Review imported CV", /waiting for your approval/i],
|
||||
["queued", "View CV processing", /is processing/i],
|
||||
["failed", "Resolve CV import", /needs attention/i],
|
||||
])("workspace exposes the %s import state as a resumable action", async (status, label, message) => {
|
||||
renderOverview({ runs: [{ id: 13, status }] });
|
||||
|
||||
expect(screen.getAllByRole("link", { name: label })[0]).toHaveAttribute("href", "#career-cv-import");
|
||||
expect(screen.getByText(message)).toBeInTheDocument();
|
||||
await waitFor(() => expect(mockedList).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test("workspace represents loading and profile-load errors without inventing status", async () => {
|
||||
renderOverview({ loading: true, loadError: "offline" });
|
||||
|
||||
expect(screen.getByRole("progressbar", { name: "Career profile completeness" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/workspace status is unavailable/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/start by adding career information/i)).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(mockedList).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
||||
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
||||
import CropImageDialog from "../components/CropImageDialog";
|
||||
import ProfileCompleteness from "./career/ProfileCompleteness";
|
||||
import CareerWorkspaceOverview from "./career/CareerWorkspaceOverview";
|
||||
import {
|
||||
EducationSection,
|
||||
InterestsSection,
|
||||
@@ -284,12 +285,15 @@ export default function CareerProfilePage() {
|
||||
const cvLabel = profileCvText.trim() ? t("profileCvReady", { count: cvWordCount }) : t("profileCvMissing");
|
||||
const latestRun = extractionRuns[0];
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: 2.5, borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<Box sx={{ display: "grid", gap: 2 }}>
|
||||
<CareerWorkspaceOverview completeness={completeness} runs={extractionRuns} loading={loading} loadError={loadError} />
|
||||
<Paper id="career-profile-editor" sx={{ mt: 0, p: { xs: 1.5, sm: 2.5 }, borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)", scrollMarginTop: 96 }}>
|
||||
<ProfileCompleteness
|
||||
completeness={completeness}
|
||||
versions={versions}
|
||||
loading={loading}
|
||||
onRestore={(version) => void restoreVersion(version)}
|
||||
showSummary={false}
|
||||
/>
|
||||
<CropImageDialog
|
||||
open={cropOpen}
|
||||
@@ -384,7 +388,6 @@ export default function CareerProfilePage() {
|
||||
<Chip label={providerLabel} color={me?.provider === "local" ? "primary" : "default"} />
|
||||
<Chip label={googleLabel} color={me?.googleLink?.linked ? "success" : "default"} variant={me?.googleLink?.linked ? "filled" : "outlined"} />
|
||||
<Chip label={cvLabel} color={profileCvText.trim() ? "success" : "warning"} variant={profileCvText.trim() ? "filled" : "outlined"} />
|
||||
<Button size="small" variant="contained" href="/career/builder">Open CV Builder</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -392,7 +395,7 @@ export default function CareerProfilePage() {
|
||||
<Box sx={{ mt: 3, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", display: careerOnly ? "block" : "none" }}>
|
||||
<Box id="career-cv-import" sx={{ gridColumn: "1 / -1", p: { xs: 1.5, sm: 2 }, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", display: careerOnly ? "block" : "none", scrollMarginTop: 96 }}>
|
||||
{!canUseAi && <Alert severity="info" sx={{ mb: 2 }} action={<Button href="/settings" size="small">View Pro</Button>}>AI CV import, rebuilding, improvement, and reprocessing require Pro. Manual profile editing remains available.</Alert>}
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
|
||||
<Box>
|
||||
@@ -746,5 +749,6 @@ export default function CareerProfilePage() {
|
||||
</Box>
|
||||
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,7 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Alert, Box, Button, Paper, Typography } from "@mui/material";
|
||||
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
|
||||
import CareerProfilePage from "./CareerProfilePage";
|
||||
|
||||
// Phase 2 / 2.2: /career is the Career Workspace — the master career profile, the single source of
|
||||
// truth for all generated documents. It is a dedicated component (CareerProfilePage), no longer a
|
||||
// careerOnly fork of ProfilePage. The CV Builder is deliberately NOT here yet (Phase 4).
|
||||
export default function CareerWorkspacePage() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900, mb: 0.5 }}>Career Profile</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
This information powers your CVs, applications, cover letters and AI assistance.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button variant="contained" startIcon={<DescriptionOutlinedIcon />} onClick={() => navigate("/career/builder")}>Open CV Builder</Button>
|
||||
</Paper>
|
||||
<Alert severity="info" sx={{ borderRadius: 3 }}>
|
||||
Your career profile holds your information. The CV Builder creates documents from it — job-specific CVs stay separate and never overwrite your profile.
|
||||
</Alert>
|
||||
<Paper sx={{ borderRadius: 4, p: { xs: 1.5, md: 2.5 }, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<CareerProfilePage />
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
return <CareerProfilePage />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Alert, Box, Button, Chip, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
|
||||
import UploadFileOutlinedIcon from "@mui/icons-material/UploadFileOutlined";
|
||||
import WorkOutlineIcon from "@mui/icons-material/WorkOutline";
|
||||
|
||||
import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder";
|
||||
import type { UserOperation } from "../../types";
|
||||
|
||||
export type CareerWorkspaceCompleteness = {
|
||||
percent: number;
|
||||
missing: string[];
|
||||
};
|
||||
|
||||
export type CareerWorkspaceImportRun = {
|
||||
id: number;
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
operation?: UserOperation | null;
|
||||
};
|
||||
|
||||
const activeStatuses = new Set(["queued", "running", "waiting_for_retry", "waiting_for_external_fallback"]);
|
||||
|
||||
function isActive(run: CareerWorkspaceImportRun) {
|
||||
return run.operation
|
||||
? activeStatuses.has(run.operation.status)
|
||||
: run.status === "queued" || run.status === "running";
|
||||
}
|
||||
|
||||
function ActionCard({ title, body, href, label, icon }: {
|
||||
title: string;
|
||||
body: string;
|
||||
href: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Paper variant="outlined" component="article" sx={{ p: 2, borderRadius: 3, display: "flex", flexDirection: "column", gap: 1.25 }}>
|
||||
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||
<Box sx={{ color: "primary.main", display: "flex" }}>{icon}</Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{title}</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1 }}>{body}</Typography>
|
||||
<Button href={href} variant="text" sx={{ alignSelf: "flex-start", px: 0.5 }}>{label}</Button>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CareerWorkspaceOverview({ completeness, runs, loading, loadError }: {
|
||||
completeness: CareerWorkspaceCompleteness | null;
|
||||
runs: CareerWorkspaceImportRun[];
|
||||
loading: boolean;
|
||||
loadError: string | null;
|
||||
}) {
|
||||
const [recentCvs, setRecentCvs] = useState<CvVariantSummary[]>([]);
|
||||
const [recentLoading, setRecentLoading] = useState(true);
|
||||
const [recentError, setRecentError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
cvBuilderApi.list()
|
||||
.then((items) => {
|
||||
if (!cancelled) {
|
||||
setRecentCvs(Array.isArray(items)
|
||||
? [...items].sort((left, right) => Date.parse(right.updatedAtUtc) - Date.parse(left.updatedAtUtc)).slice(0, 3)
|
||||
: []);
|
||||
setRecentError(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setRecentError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setRecentLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const importState = useMemo(() => {
|
||||
const review = runs.filter((run) => run.status === "pending_review");
|
||||
const active = runs.filter(isActive);
|
||||
const failed = runs.filter((run) => run.status === "failed" || run.operation?.status === "failed");
|
||||
if (review.length > 0) return { severity: "warning" as const, label: "Review imported CV", message: `${review.length} import${review.length === 1 ? " is" : "s are"} waiting for your approval.` };
|
||||
if (active.length > 0) return { severity: "info" as const, label: "View CV processing", message: `${active.length} CV import${active.length === 1 ? " is" : "s are"} processing.` };
|
||||
if (failed.length > 0) return { severity: "error" as const, label: "Resolve CV import", message: `${failed.length} CV import${failed.length === 1 ? " needs" : "s need"} attention.` };
|
||||
return { severity: "success" as const, label: "Import a CV", message: runs.length > 0 ? "Your latest CV import is complete." : "No CV has been imported yet." };
|
||||
}, [runs]);
|
||||
|
||||
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;
|
||||
|
||||
return (
|
||||
<Box component="section" aria-labelledby="career-workspace-title" sx={{ display: "grid", gap: 2 }}>
|
||||
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 4 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }}>
|
||||
<Box>
|
||||
<Typography id="career-workspace-title" component="h1" variant="h5" sx={{ fontWeight: 900 }}>Career Workspace</Typography>
|
||||
<Typography color="text.secondary">Choose what you want to work on next.</Typography>
|
||||
</Box>
|
||||
<Button variant="contained" startIcon={<DescriptionOutlinedIcon />} href="/career/builder">Open CV Builder</Button>
|
||||
</Box>
|
||||
|
||||
{isFirstRun ? <Alert severity="info" sx={{ mt: 2 }}>Start by adding career information manually or importing a CV.</Alert> : null}
|
||||
{loadError ? <Alert severity="error" sx={{ mt: 2 }}>Workspace status is unavailable. The profile editor below can retry the request.</Alert> : null}
|
||||
|
||||
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr", md: "minmax(0, 1fr) minmax(0, 1fr)" }, gap: 2 }}>
|
||||
<Box aria-label="Profile status" sx={{ p: 2, borderRadius: 3, bgcolor: "background.default", border: "1px solid", borderColor: "divider" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center" }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>Profile completeness</Typography>
|
||||
{loading ? <Skeleton width={42} /> : <Typography sx={{ fontWeight: 900 }}>{completeness?.percent ?? 0}%</Typography>}
|
||||
</Box>
|
||||
<LinearProgress aria-label="Career profile completeness" variant={loading ? "indeterminate" : "determinate"} value={completeness?.percent ?? 0} sx={{ mt: 1, height: 8, borderRadius: 999 }} />
|
||||
{!loading && completeness?.missing.length ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>Add next: {completeness.missing.slice(0, 3).join(", ")}</Typography>
|
||||
) : null}
|
||||
<Button href="#career-profile-editor" size="small" sx={{ mt: 1, px: 0.5 }}>Improve profile</Button>
|
||||
</Box>
|
||||
|
||||
<Alert severity={importState.severity} sx={{ borderRadius: 3, alignItems: "flex-start" }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>CV import</Typography>
|
||||
<Typography variant="body2">{importState.message}</Typography>
|
||||
<Button href="#career-cv-import" color="inherit" size="small" sx={{ mt: 0.75, px: 0.5 }}>{importState.label}</Button>
|
||||
</Alert>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Box component="nav" aria-label="Career workspace actions" sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(2, minmax(0, 1fr))", xl: "repeat(4, minmax(0, 1fr))" }, gap: 1.5 }}>
|
||||
<ActionCard title="Career profile" body="Add or improve the facts used in every CV." href="#career-profile-editor" label="Edit career profile" icon={<PersonOutlineIcon />} />
|
||||
<ActionCard title="Import a CV" body="Extract information, then review every proposed change before applying it." href="#career-cv-import" label={importState.label} icon={<UploadFileOutlinedIcon />} />
|
||||
<ActionCard title="General CV" body="Create and manage reusable CV documents from your profile." href="/career/builder" label="Create a general CV" icon={<DescriptionOutlinedIcon />} />
|
||||
<ActionCard title="Job-specific CV" body="Open a saved job to tailor and attach a CV for that application." href="/jobs" label="Choose a job" icon={<WorkOutlineIcon />} />
|
||||
</Box>
|
||||
|
||||
<Paper component="section" aria-labelledby="recent-cvs-title" sx={{ p: 2, borderRadius: 4 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<Typography id="recent-cvs-title" variant="h6" sx={{ fontWeight: 800 }}>Recent CVs</Typography>
|
||||
<Button href="/career/builder" size="small">View all CVs</Button>
|
||||
</Box>
|
||||
{recentLoading ? (
|
||||
<Stack spacing={1} sx={{ mt: 1.5 }}><Skeleton height={32} /><Skeleton height={32} /></Stack>
|
||||
) : recentError ? (
|
||||
<Alert severity="warning" sx={{ mt: 1.5 }}>Recent CVs could not be loaded. Open the CV Builder to try again.</Alert>
|
||||
) : recentCvs.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>No CV documents yet.</Typography>
|
||||
) : (
|
||||
<Stack spacing={1} sx={{ mt: 1 }}>
|
||||
{recentCvs.map((cv) => (
|
||||
<Box key={cv.id} sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>{cv.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Updated {new Date(cv.updatedAtUtc).toLocaleDateString()}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 0.75, alignItems: "center" }}>
|
||||
{cv.jobApplicationId ? <Chip label="Job-specific" size="small" variant="outlined" /> : null}
|
||||
<Button href={`/career/builder/${cv.id}`} size="small">Open</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -23,16 +23,20 @@ export default function ProfileCompleteness({
|
||||
versions,
|
||||
loading,
|
||||
onRestore,
|
||||
showSummary = true,
|
||||
}: {
|
||||
completeness: CareerCompletenessView | null;
|
||||
versions: CareerVersionView[];
|
||||
loading: boolean;
|
||||
onRestore: (version: number) => void;
|
||||
showSummary?: boolean;
|
||||
}) {
|
||||
if (!completeness) return null;
|
||||
if ((!showSummary || !completeness) && versions.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2.5, p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
{showSummary && completeness ? (
|
||||
<>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 2, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 900 }}>Profile completeness</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{completeness.percent}%</Typography>
|
||||
@@ -53,8 +57,10 @@ export default function ProfileCompleteness({
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "success.main", mt: 1.25, fontWeight: 700 }}>Your career profile is complete.</Typography>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{versions.length > 1 ? (
|
||||
<Accordion disableGutters elevation={0} sx={{ mt: 1.5, "&:before": { display: "none" }, backgroundColor: "transparent" }}>
|
||||
<Accordion disableGutters elevation={0} sx={{ mt: showSummary ? 1.5 : 0, "&:before": { display: "none" }, backgroundColor: "transparent" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0, minHeight: 0 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>Version history ({versions.length})</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
Reference in New Issue
Block a user