ultisuite-client/lib/contacts/discovery-grouping.ts
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- 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.
2026-06-06 20:26:51 +02:00

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
}