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
da7d305588
commit
d3b4ed74bb
3 changed files with 50 additions and 25 deletions
|
|
@ -41,4 +41,11 @@ describe('useProfileStore', () => {
|
|||
await store.save()
|
||||
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 { 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', () => {
|
||||
const name = ref('')
|
||||
|
|
@ -20,11 +20,17 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
|||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const saveError = ref<string | null>(null)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
async function load() {
|
||||
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
|
||||
if (error) {
|
||||
loadError.value = error.kind === 'network' ? error.message : error.detail || 'Failed to load profile'
|
||||
return
|
||||
}
|
||||
if (!data) return
|
||||
name.value = String(data.name ?? '')
|
||||
email.value = String(data.email ?? '')
|
||||
|
|
@ -33,7 +39,8 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
|||
career_summary.value = String(data.career_summary ?? '')
|
||||
candidate_voice.value = String(data.candidate_voice ?? '')
|
||||
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[]) ?? []
|
||||
accessibility_focus.value = Boolean(data.accessibility_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.'
|
||||
return
|
||||
}
|
||||
// Push identity fields to resume YAML — graceful; endpoint may not exist yet (Task 3)
|
||||
await useApiFetch('/api/settings/resume/sync-identity', {
|
||||
// fire-and-forget — identity sync failures don't block save
|
||||
useApiFetch('/api/settings/resume/sync-identity', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -81,7 +88,7 @@ export const useProfileStore = defineStore('settings/profile', () => {
|
|||
return {
|
||||
name, email, phone, linkedin_url, career_summary, candidate_voice, inference_profile,
|
||||
mission_preferences, nda_companies, accessibility_focus, lgbtq_focus,
|
||||
loading, saving, saveError,
|
||||
loading, saving, saveError, loadError,
|
||||
load, save,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
<div v-if="store.loading" class="loading-state">Loading profile…</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="loadError" class="load-error-banner" role="alert">
|
||||
<strong>Error loading profile:</strong> {{ loadError }}
|
||||
</div>
|
||||
<!-- ── Identity ─────────────────────────────────────── -->
|
||||
<section class="form-section">
|
||||
<h3 class="section-title">Identity</h3>
|
||||
|
|
@ -88,7 +91,7 @@
|
|||
|
||||
<div
|
||||
v-for="(pref, idx) in store.mission_preferences"
|
||||
:key="idx"
|
||||
:key="pref.id"
|
||||
class="mission-row"
|
||||
>
|
||||
<input
|
||||
|
|
@ -197,8 +200,10 @@
|
|||
import { ref, onMounted } from 'vue'
|
||||
import { useProfileStore } from '../../stores/settings/profile'
|
||||
import { useAppConfigStore } from '../../stores/appConfig'
|
||||
import { useApiFetch } from '../../composables/useApi'
|
||||
|
||||
const store = useProfileStore()
|
||||
const { loadError } = store
|
||||
const config = useAppConfigStore()
|
||||
|
||||
const newNdaCompany = ref('')
|
||||
|
|
@ -209,7 +214,7 @@ onMounted(() => { store.load() })
|
|||
|
||||
// ── Mission helpers ──────────────────────────────────────
|
||||
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) {
|
||||
|
|
@ -240,27 +245,23 @@ function autosave() {
|
|||
// ── AI generation (paid tier) ────────────────────────────
|
||||
async function generateSummary() {
|
||||
generatingSummary.value = true
|
||||
try {
|
||||
const res = await fetch('/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 {
|
||||
const { data, error } = await useApiFetch<{ summary?: string }>(
|
||||
'/api/settings/profile/generate-summary', { method: 'POST' }
|
||||
)
|
||||
generatingSummary.value = false
|
||||
}
|
||||
if (!error && data?.summary) store.career_summary = data.summary
|
||||
}
|
||||
|
||||
async function generateMissions() {
|
||||
generatingMissions.value = true
|
||||
try {
|
||||
const res = await fetch('/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 {
|
||||
const { data, error } = await useApiFetch<{ mission_preferences?: Array<{ industry: string; note: string }> }>(
|
||||
'/api/settings/profile/generate-missions', { method: 'POST' }
|
||||
)
|
||||
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>
|
||||
|
|
@ -292,6 +293,16 @@ async function generateMissions() {
|
|||
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 ──────────────────────────────────────────── */
|
||||
.form-section {
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
|
|||
Loading…
Reference in a new issue