feat(phase-2): separate /profile (identity) from /career (master profile)
Phase 2 — Career/Profile separation. The master career profile is the source of
truth; identity and career data are now saved independently so neither wipes the
other. CV Builder deliberately not built yet.
Backend — PUT /auth/profile is now a partial update:
- null/omitted field -> unchanged; "" -> cleared; value -> set (trimmed).
- Email/UserName never cleared to empty (login identifiers).
This lets /profile save identity fields and /career save the master-profile
fields through the same endpoint without one nulling the other. 4 new tests
cover the data-integrity guarantees (identity save keeps the CV, career save
keeps identity, empty clears, null leaves).
Frontend:
- ProfilePage save payload is now scoped by careerOnly: /career sends only
{ profileCvText, profileCvStructureJson }, /profile sends only identity.
- CareerWorkspacePage: removed the inert "CV Builder" tab (careerView) — Phase 2
establishes the master profile only; the builder is Phase 4.
- Dropped the dead careerView prop.
- Updated the CV-save test to render career mode and assert identity is excluded.
Source-of-truth flip (CareerProfileService authoritative) stays deferred to F5
per the branch design; CareerProfileService keeps mirroring via its dual-write.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -226,13 +226,16 @@ function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata })
|
||||
);
|
||||
}
|
||||
|
||||
// careerOnly splits the two Phase 2 surfaces this component still backs:
|
||||
// false -> /profile: account identity + security + preferences
|
||||
// true -> /career: the master career profile (source of truth)
|
||||
// Fully separating this into two components is roadmap 2.2; for now the fork keeps each route's
|
||||
// content — and its Save payload — cleanly scoped.
|
||||
export default function ProfilePage({
|
||||
careerOnly = false,
|
||||
careerView = "master",
|
||||
onMasterCvAvailabilityChange,
|
||||
}: {
|
||||
careerOnly?: boolean;
|
||||
careerView?: "master" | "builder";
|
||||
onMasterCvAvailabilityChange?: (hasMasterCv: boolean) => void;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
@@ -1310,8 +1313,14 @@ export default function ProfilePage({
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.put("/auth/profile", { email, userName, firstName, lastName, displayName, profileCvText, profileCvStructureJson: JSON.stringify(structuredCv) });
|
||||
window.localStorage.setItem("profileHeadline", headline.trim());
|
||||
// Scoped save: /career persists only the master profile, /profile only identity.
|
||||
// The backend (PUT /auth/profile) does partial updates — omitted fields are left
|
||||
// unchanged — so neither surface wipes the other's data.
|
||||
const payload = careerOnly
|
||||
? { profileCvText, profileCvStructureJson: JSON.stringify(structuredCv) }
|
||||
: { email, userName, firstName, lastName, displayName };
|
||||
await api.put("/auth/profile", payload);
|
||||
if (!careerOnly) window.localStorage.setItem("profileHeadline", headline.trim());
|
||||
await loadProfile();
|
||||
toast(t("profileUpdated"), "success");
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user