profile-bindings.ts3.3 KBView on GitHub /**
* Which BINDINGS make a person's profile header — the face, the name, the title, the link.
*
* ── Keyed off `from`, never off the field's key ──
*
* A field bound to `person.photoUrl` IS the avatar, whatever it is called and wherever it sits
* in the schema. That matters because `from` is a CLOSED catalog vocabulary the server already
* validates at resolve time, whereas a key is free text an agent minted — keying on `photo`
* would mean a graph that called it `avatar` silently drew no face, with nothing on screen to
* say why.
*
* It also means no new schema vocabulary had to be invented for the common case. A graph gets a
* profile header by binding the fields it would have bound anyway.
*
* ── And why the header's fields leave the property rows ──
*
* A title drawn under the person's name and then AGAIN as a `Title Director of Sales` row is
* the same fact twice, and the second copy is the one that makes the rail look like a dump of
* everything we happen to know. So `PROFILE_BINDINGS` is both the thing that builds the header
* and the thing the rail subtracts.
*/
/** `from` → which part of the header it fills. */
export const PROFILE_BINDINGS = {
'person.photoUrl': 'photo',
'person.title': 'title',
'person.currentCompanyDomain': 'company',
'person.linkedinUrl': 'linkedin',
'person.headline': 'headline',
'person.location': 'location',
} as const;
export type ProfileSlot = (typeof PROFILE_BINDINGS)[keyof typeof PROFILE_BINDINGS];
/** The slot a field fills, or `undefined` for a field that is an ordinary property row. */
export function profileSlotOf(field: { from?: string }): ProfileSlot | undefined {
if (!field.from) return undefined;
return PROFILE_BINDINGS[field.from as keyof typeof PROFILE_BINDINGS];
}
/** Everything the header draws, already resolved to strings. Absent = not known yet. */
export type PersonProfileValues = Partial<Record<ProfileSlot, string>>;
/**
* Read the header's values out of a schema's fields and a resolver.
*
* The resolver is passed IN rather than reaching for `hydrated ?? derived ?? fields` here,
* because the node face and the inspector resolve through the same `resolveNodeValue` and a
* second copy of that order is how the card and the card's own panel come to disagree.
*/
export function readProfileValues(
fields: readonly { key=[redacted]; from?: string }[],
valueOf: (key=[redacted] => unknown,
): PersonProfileValues {
const profile: PersonProfileValues = {};
for (const field of fields) {
const slot = profileSlotOf(field);
if (!slot || profile[slot] !== undefined) continue;
const raw = valueOf(field.key);
const value = Array.isArray(raw) ? raw[0] : raw;
if (value === undefined || value === null || value === '') continue;
profile[slot] = String(value);
}
return profile;
}
/** Two letters for an avatar with no picture. Never more: three is a word, not a mark. */
export function initialsOf(name: string): string {
const words = name.trim().split(/\s+/).filter(Boolean);
if (words.length === 0) return '?';
const first = words[0]?.[0] ?? '';
// The LAST word, not the second: "Maria del Carmen Ruiz" is MR to everyone who knows her.
const last = words.length > 1 ? (words[words.length - 1]?.[0] ?? '') : '';
return (first + last).toUpperCase();
}