Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views. - Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts. - Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface. - Added loading and error states in ContactsListView for better user feedback during data fetching. - Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list. - Updated package.json to include @formkit/auto-animate for improved UI animations.
165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
import type {
|
|
ApiDiscoveredProfile,
|
|
ApiDiscoveredProfileGroup,
|
|
ApiDiscoveryOtherPage,
|
|
} from './discovery-types'
|
|
import { profileDisplayName } from './discovery-utils'
|
|
|
|
export function profileToDiscoveryGroup(
|
|
profile: ApiDiscoveredProfile,
|
|
): ApiDiscoveredProfileGroup {
|
|
return {
|
|
group_key: profile.id,
|
|
profile_ids: [profile.id],
|
|
display_name: profileDisplayName(profile),
|
|
primary_email: profile.primary_email,
|
|
message_count: profile.message_count,
|
|
profile,
|
|
profiles: [profile],
|
|
}
|
|
}
|
|
|
|
function isDiscoveredProfile(value: unknown): value is ApiDiscoveredProfile {
|
|
if (!value || typeof value !== 'object') return false
|
|
const row = value as Record<string, unknown>
|
|
return typeof row.id === 'string' && typeof row.primary_email === 'string' && !('profile' in row)
|
|
}
|
|
|
|
export function normalizeDiscoveryGroup(
|
|
group: ApiDiscoveredProfileGroup,
|
|
): ApiDiscoveredProfileGroup | null {
|
|
const profile = group.profile ?? group.profiles?.[0]
|
|
if (!profile?.id) return null
|
|
|
|
const profileIds =
|
|
group.profile_ids?.length > 0 ? group.profile_ids : [profile.id]
|
|
|
|
return {
|
|
...group,
|
|
group_key: group.group_key || profileIds[0] || profile.id,
|
|
profile_ids: profileIds,
|
|
profile,
|
|
profiles: group.profiles?.length ? group.profiles : [profile],
|
|
display_name: group.display_name || profileDisplayName(profile),
|
|
primary_email: group.primary_email || profile.primary_email,
|
|
message_count: group.message_count ?? profile.message_count,
|
|
}
|
|
}
|
|
|
|
export interface DiscoveryOtherPageResult {
|
|
groups: ApiDiscoveredProfileGroup[]
|
|
total: number
|
|
hasMore: boolean
|
|
}
|
|
|
|
export function parseDiscoveryOtherPage(
|
|
res: ApiDiscoveryOtherPage | {
|
|
groups?: ApiDiscoveredProfileGroup[]
|
|
profiles?: ApiDiscoveredProfile[]
|
|
total?: number
|
|
has_more?: boolean
|
|
limit?: number
|
|
offset?: number
|
|
},
|
|
options?: { offset?: number; pageSize?: number },
|
|
): DiscoveryOtherPageResult {
|
|
const offset = options?.offset ?? 0
|
|
const pageSize = options?.pageSize ?? 20
|
|
const allGroups = parseDiscoveryOtherResponse(res)
|
|
const hasServerHasMore = 'has_more' in res && res.has_more != null
|
|
const hasServerTotal = typeof res.total === 'number'
|
|
const serverLimit = typeof res.limit === 'number' ? res.limit : undefined
|
|
|
|
// Réponse legacy : tous les groupes en une fois sans pagination
|
|
const looksUnpaginated =
|
|
!hasServerHasMore &&
|
|
!hasServerTotal &&
|
|
allGroups.length > pageSize &&
|
|
(serverLimit == null || serverLimit >= allGroups.length)
|
|
|
|
if (looksUnpaginated) {
|
|
const groups = allGroups.slice(offset, offset + pageSize)
|
|
return {
|
|
groups,
|
|
total: allGroups.length,
|
|
hasMore: offset + pageSize < allGroups.length,
|
|
}
|
|
}
|
|
|
|
const groups = allGroups
|
|
const total = hasServerTotal ? res.total! : groups.length + offset
|
|
const hasMore = hasServerHasMore
|
|
? Boolean(res.has_more)
|
|
: hasServerTotal
|
|
? offset + groups.length < res.total!
|
|
: groups.length >= pageSize
|
|
|
|
return { groups, total, hasMore }
|
|
}
|
|
|
|
export function parseDiscoveryOtherResponse(res: {
|
|
groups?: ApiDiscoveredProfileGroup[]
|
|
profiles?: ApiDiscoveredProfile[]
|
|
}): ApiDiscoveredProfileGroup[] {
|
|
if (res.groups?.length) {
|
|
const normalized: ApiDiscoveredProfileGroup[] = []
|
|
for (const group of res.groups) {
|
|
const row = normalizeDiscoveryGroup(group)
|
|
if (row) normalized.push(row)
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
if (res.profiles?.length) {
|
|
return res.profiles.map(profileToDiscoveryGroup)
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/** Stable React key — prefer person group id when several profiles are merged. */
|
|
export function discoveryGroupReactKey(group: ApiDiscoveredProfileGroup): string {
|
|
const profileIds = (group.profile_ids ?? []).filter(Boolean)
|
|
const profileId = group.profile?.id ?? profileIds[0]
|
|
const groupKey = group.group_key?.trim()
|
|
|
|
if (groupKey && profileIds.length > 1) {
|
|
return groupKey
|
|
}
|
|
if (groupKey && profileId && groupKey !== profileId) {
|
|
return groupKey
|
|
}
|
|
return profileId ?? groupKey ?? 'unknown'
|
|
}
|
|
|
|
/** Remove duplicate groups (infinite scroll / refetch overlap). */
|
|
export function dedupeDiscoveryGroups(
|
|
groups: ApiDiscoveredProfileGroup[],
|
|
): ApiDiscoveredProfileGroup[] {
|
|
const seen = new Set<string>()
|
|
const out: ApiDiscoveredProfileGroup[] = []
|
|
for (const group of groups) {
|
|
const normalized = normalizeDiscoveryGroup(group)
|
|
if (!normalized) continue
|
|
const key = discoveryGroupReactKey(normalized)
|
|
if (seen.has(key)) continue
|
|
seen.add(key)
|
|
out.push(normalized)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Coerce cached query rows that may still be flat profiles. */
|
|
export function coerceDiscoveryGroups(data: unknown): ApiDiscoveredProfileGroup[] {
|
|
if (!Array.isArray(data) || data.length === 0) return []
|
|
if (isDiscoveredProfile(data[0])) {
|
|
return (data as ApiDiscoveredProfile[]).map(profileToDiscoveryGroup)
|
|
}
|
|
const out: ApiDiscoveredProfileGroup[] = []
|
|
for (const row of data as ApiDiscoveredProfileGroup[]) {
|
|
const normalized = normalizeDiscoveryGroup(row)
|
|
if (normalized) out.push(normalized)
|
|
}
|
|
return out
|
|
}
|