fix(settings): address profile tab code quality issues
- add loadError ref to useProfileStore, rendered in MyProfileView - replace raw fetch with useApiFetch in generateSummary/generateMissions - remove await from sync-identity call (fire-and-forget) - add stable id field to MissionPref, use as v-for key - add test for load() error path
This commit is contained in:
parent
a8b16d616c
commit
86454a97be
3 changed files with 50 additions and 25 deletions
|
|
@ -41,4 +41,11 @@ describe('useProfileStore', () => {
|
||||||
await store.save()
|
await store.save()
|
||||||
expect(store.saveError).toBeTruthy()
|
expect(store.saveError).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sets loadError when load fails', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({ data: null, error: { kind: 'network', message: 'Network error' } })
|
||||||
|
const store = useProfileStore()
|
||||||
|
await store.load()
|
||||||
|
expect(store.loadError).toBe('Network error')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useApiFetch } from '../../composables/useApi'
|
import { useApiFetch } from '../../composables/useApi'
|
||||||
|
|
||||||
export interface MissionPref { industry: string; note: string }
|
export interface MissionPref { id: string; industry: string; note: string }
|
||||||
|
|
||||||
export const useProfileStore = defineStore('settings/profile', () => {
|
export const useProfileStore = defineStore('settings/profile', () => {
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
|
|
@ -20,11 +20,17 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const saveError = ref<string | null>(null)
|
const saveError = ref<string | null>(null)
|
||||||
|
const loadError = ref<string | null>(null)
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const { data } = await useApiFetch<Record<string, unknown>>('/api/settings/profile')
|
loadError.value = null
|
||||||
|
const { data, error } = await useApiFetch<Record<string, unknown>>('/api/settings/profile')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
if (error) {
|
||||||
|
loadError.value = error.kind === 'network' ? error.message : error.detail || 'Failed to load profile'
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!data) return
|
if (!data) return
|
||||||
name.value = String(data.name ?? '')
|
name.value = String(data.name ?? '')
|
||||||
email.value = String(data.email ?? '')
|
email.value = String(data.email ?? '')
|
||||||
|
|
@ -33,7 +39,8 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
||||||
career_summary.value = String(data.career_summary ?? '')
|
career_summary.value = String(data.career_summary ?? '')
|
||||||
candidate_voice.value = String(data.candidate_voice ?? '')
|
candidate_voice.value = String(data.candidate_voice ?? '')
|
||||||
inference_profile.value = String(data.inference_profile ?? 'cpu')
|
inference_profile.value = String(data.inference_profile ?? 'cpu')
|
||||||
mission_preferences.value = (data.mission_preferences as MissionPref[]) ?? []
|
mission_preferences.value = ((data.mission_preferences as Array<{ industry: string; note: string }>) ?? [])
|
||||||
|
.map((m) => ({ id: crypto.randomUUID(), industry: m.industry ?? '', note: m.note ?? '' }))
|
||||||
nda_companies.value = (data.nda_companies as string[]) ?? []
|
nda_companies.value = (data.nda_companies as string[]) ?? []
|
||||||
accessibility_focus.value = Boolean(data.accessibility_focus)
|
accessibility_focus.value = Boolean(data.accessibility_focus)
|
||||||
lgbtq_focus.value = Boolean(data.lgbtq_focus)
|
lgbtq_focus.value = Boolean(data.lgbtq_focus)
|
||||||
|
|
@ -65,8 +72,8 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
||||||
saveError.value = 'Save failed — please try again.'
|
saveError.value = 'Save failed — please try again.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Push identity fields to resume YAML — graceful; endpoint may not exist yet (Task 3)
|
// fire-and-forget — identity sync failures don't block save
|
||||||
await useApiFetch('/api/settings/resume/sync-identity', {
|
useApiFetch('/api/settings/resume/sync-identity', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -81,7 +88,7 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
||||||
return {
|
return {
|
||||||
name, email, phone, linkedin_url, career_summary, candidate_voice, inference_profile,
|
name, email, phone, linkedin_url, career_summary, candidate_voice, inference_profile,
|
||||||
mission_preferences, nda_companies, accessibility_focus, lgbtq_focus,
|
mission_preferences, nda_companies, accessibility_focus, lgbtq_focus,
|
||||||
loading, saving, saveError,
|
loading, saving, saveError, loadError,
|
||||||
load, save,
|
load, save,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
<div v-if="store.loading" class="loading-state">Loading profile…</div>
|
<div v-if="store.loading" class="loading-state">Loading profile…</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div v-if="loadError" class="load-error-banner" role="alert">
|
||||||
|
<strong>Error loading profile:</strong> {{ loadError }}
|
||||||
|
</div>
|
||||||
<!-- ── Identity ─────────────────────────────────────── -->
|
<!-- ── Identity ─────────────────────────────────────── -->
|
||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<h3 class="section-title">Identity</h3>
|
<h3 class="section-title">Identity</h3>
|
||||||
|
|
@ -88,7 +91,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(pref, idx) in store.mission_preferences"
|
v-for="(pref, idx) in store.mission_preferences"
|
||||||
:key="idx"
|
:key="pref.id"
|
||||||
class="mission-row"
|
class="mission-row"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -197,8 +200,10 @@
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useProfileStore } from '../../stores/settings/profile'
|
import { useProfileStore } from '../../stores/settings/profile'
|
||||||
import { useAppConfigStore } from '../../stores/appConfig'
|
import { useAppConfigStore } from '../../stores/appConfig'
|
||||||
|
import { useApiFetch } from '../../composables/useApi'
|
||||||
|
|
||||||
const store = useProfileStore()
|
const store = useProfileStore()
|
||||||
|
const { loadError } = store
|
||||||
const config = useAppConfigStore()
|
const config = useAppConfigStore()
|
||||||
|
|
||||||
const newNdaCompany = ref('')
|
const newNdaCompany = ref('')
|
||||||
|
|
@ -209,7 +214,7 @@ onMounted(() => { store.load() })
|
||||||
|
|
||||||
// ── Mission helpers ──────────────────────────────────────
|
// ── Mission helpers ──────────────────────────────────────
|
||||||
function addMission() {
|
function addMission() {
|
||||||
store.mission_preferences = [...store.mission_preferences, { industry: '', note: '' }]
|
store.mission_preferences = [...store.mission_preferences, { id: crypto.randomUUID(), industry: '', note: '' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMission(idx: number) {
|
function removeMission(idx: number) {
|
||||||
|
|
@ -240,27 +245,23 @@ function autosave() {
|
||||||
// ── AI generation (paid tier) ────────────────────────────
|
// ── AI generation (paid tier) ────────────────────────────
|
||||||
async function generateSummary() {
|
async function generateSummary() {
|
||||||
generatingSummary.value = true
|
generatingSummary.value = true
|
||||||
try {
|
const { data, error } = await useApiFetch<{ summary?: string }>(
|
||||||
const res = await fetch('/api/settings/profile/generate-summary', { method: 'POST' })
|
'/api/settings/profile/generate-summary', { method: 'POST' }
|
||||||
if (res.ok) {
|
)
|
||||||
const data = await res.json() as { summary?: string }
|
|
||||||
if (data.summary) store.career_summary = data.summary
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
generatingSummary.value = false
|
generatingSummary.value = false
|
||||||
}
|
if (!error && data?.summary) store.career_summary = data.summary
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateMissions() {
|
async function generateMissions() {
|
||||||
generatingMissions.value = true
|
generatingMissions.value = true
|
||||||
try {
|
const { data, error } = await useApiFetch<{ mission_preferences?: Array<{ industry: string; note: string }> }>(
|
||||||
const res = await fetch('/api/settings/profile/generate-missions', { method: 'POST' })
|
'/api/settings/profile/generate-missions', { method: 'POST' }
|
||||||
if (res.ok) {
|
)
|
||||||
const data = await res.json() as { mission_preferences?: Array<{ industry: string; note: string }> }
|
|
||||||
if (data.mission_preferences) store.mission_preferences = data.mission_preferences
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
generatingMissions.value = false
|
generatingMissions.value = false
|
||||||
|
if (!error && data?.mission_preferences) {
|
||||||
|
store.mission_preferences = data.mission_preferences.map((m) => ({
|
||||||
|
id: crypto.randomUUID(), industry: m.industry ?? '', note: m.note ?? '',
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -292,6 +293,16 @@ async function generateMissions() {
|
||||||
padding: var(--space-4) 0;
|
padding: var(--space-4) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-error-banner {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
background: color-mix(in srgb, var(--color-danger, #c0392b) 10%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-danger, #c0392b) 40%, transparent);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--color-danger, #c0392b);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Sections ──────────────────────────────────────────── */
|
/* ── Sections ──────────────────────────────────────────── */
|
||||||
.form-section {
|
.form-section {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue