cc-haha/desktop/src/api/desktopUiPreferences.ts
程序员阿江(Relakkes) 587922ba75 fix(desktop): refine activity profile editing
The token usage profile header had too much top spacing, the edit action competed with the content from the top-right corner, and the profile subtitle was fixed to the default project link. This keeps the profile controls close to the identity block, moves the modal scrim to the document body so it covers the whole desktop shell, and persists a user-editable second line with URL auto-linking.

Constraint: Preserve existing profile preferences and backfill older records that do not have a subtitle field.
Rejected: Keep the top-right edit button | it kept the action visually detached from the profile content.
Rejected: Store the second line only in desktop local state | the preference already has a server-backed profile shape.
Confidence: high
Scope-risk: narrow
Directive: Keep profile preference schema changes backward-compatible with older desktop-ui preference files.
Tested: cd desktop && bun run test --run src/pages/ActivitySettings.test.tsx src/api/desktopUiPreferences.test.ts
Tested: bun test src/server/__tests__/desktop-ui-preferences.test.ts
Tested: bun run check:persistence-upgrade
Tested: bun run check:server
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Full Tauri native shell smoke; renderer flow was smoke-tested in browser only.
2026-05-31 17:42:30 +08:00

86 lines
2.3 KiB
TypeScript

import { ApiError, api, getApiUrl, getAuthToken } from './client'
export type SidebarProjectPreferences = {
projectOrder: string[]
pinnedProjects: string[]
hiddenProjects: string[]
projectOrganization: 'project' | 'recentProject' | 'time'
projectSortBy: 'createdAt' | 'updatedAt'
}
export type DesktopProfilePreferences = {
displayName: string
subtitle: string
avatarFile: string | null
avatarUpdatedAt: string | null
}
export type DesktopUiPreferences = {
schemaVersion: number
sidebar: SidebarProjectPreferences
profile: DesktopProfilePreferences
}
export type DesktopUiPreferencesResponse = {
preferences: DesktopUiPreferences
exists: boolean
}
export const desktopUiPreferencesApi = {
getPreferences() {
return api.get<DesktopUiPreferencesResponse>('/api/desktop-ui/preferences')
},
updateSidebarPreferences(sidebar: SidebarProjectPreferences) {
return api.put<{ ok: true; preferences: DesktopUiPreferences }>(
'/api/desktop-ui/preferences/sidebar',
sidebar,
)
},
updateProfilePreferences(profile: Pick<DesktopProfilePreferences, 'displayName' | 'subtitle'>) {
return api.put<{ ok: true; preferences: DesktopUiPreferences }>(
'/api/desktop-ui/preferences/profile',
profile,
)
},
async uploadProfileAvatar(file: File) {
return uploadProfileAvatar(file)
},
deleteProfileAvatar() {
return api.delete<{ ok: true; preferences: DesktopUiPreferences }>(
'/api/desktop-ui/preferences/profile/avatar',
)
},
}
export function getProfileAvatarUrl(updatedAt: string | null | undefined) {
const suffix = updatedAt ? `?v=${encodeURIComponent(updatedAt)}` : ''
return getApiUrl(`/api/desktop-ui/preferences/profile/avatar${suffix}`)
}
async function uploadProfileAvatar(file: File): Promise<{ ok: true; preferences: DesktopUiPreferences }> {
const headers: Record<string, string> = {
'Content-Type': file.type || 'application/octet-stream',
}
const token = getAuthToken()
if (token) {
headers.Authorization = `Bearer ${token}`
}
const res = await fetch(getApiUrl('/api/desktop-ui/preferences/profile/avatar'), {
method: 'PUT',
headers,
body: file,
})
if (!res.ok) {
const body = await res.json().catch(() => res.text())
throw new ApiError(res.status, body)
}
return res.json() as Promise<{ ok: true; preferences: DesktopUiPreferences }>
}