aop-columns.ts3.7 KBView on GitHub /**
* aop-columns.ts
*
* The AOP-derived half of a CRM column set, shared by every surface that
* offers filtering over conversations: the canvas (`useCanvasConfiguration`),
* the conversations sidebar (`useSidebarAllColumns`), and the inbox editor
* (`useAopFilterColumns`).
*
* Each surface layers its own concerns on top — canvas adds width/order/
* visibility, the sidebar synthesizes them — but the mapping from a set of
* AOPs to `wm_<fieldId>` columns and their merged field definitions is the
* same everywhere, and drifting copies of it are how a custom field ends up
* filterable on one surface and invisible on another.
*/
import { getVisibleFieldDefinitions } from './background-fields';
import type { CRMColumnMetadata } from '../store/crmSlice';
import type { CustomFieldDefinitions } from '../types';
/** Field types with no meaningful value ordering (free text). */
const NON_VALUE_SORTABLE = new Set(['text', 'url', 'email', 'phone']);
/** The subset of an AOP these builders read. */
export type AopWithCustomFields = {
customFieldDefinitions?: CustomFieldDefinitions | null;
};
/**
* Union of every selected AOP's visible custom field definitions, first
* definition wins on id collisions. Consumed by `computeFiltersFromConfig` /
* `computeBackendSortFromConfig` to resolve the type of a `wm_*` column.
*/
export function mergeCustomFieldDefinitions(
aops: AopWithCustomFields[],
): CustomFieldDefinitions | undefined {
if (aops.length === 0) return undefined;
const merged: CustomFieldDefinitions = {};
for (const aop of aops) {
if (!aop.customFieldDefinitions) continue;
// Skip background (sub-event taxonomy) fields — never surfaced as columns/filters.
for (const [fieldId, field] of Object.entries(
getVisibleFieldDefinitions(aop.customFieldDefinitions),
)) {
if (!(fieldId in merged)) merged[fieldId] = field;
}
}
return Object.keys(merged).length > 0 ? merged : undefined;
}
/**
* `wm_<fieldId>` column metadata for every selected AOP's custom fields,
* deduped by column id and ordered by the field's `displayOrder`. A field with
* signal scoring emits a trailing `_signal` sub-column.
*/
export function buildAopCustomFieldColumns(aops: AopWithCustomFields[]): CRMColumnMetadata[] {
if (aops.length === 0) return [];
const fieldMap = new Map<string, { meta: CRMColumnMetadata; displayOrder: number }>();
for (const aop of aops) {
if (!aop.customFieldDefinitions) continue;
Object.entries(getVisibleFieldDefinitions(aop.customFieldDefinitions)).forEach(
([fieldId, field]) => {
const colId = `wm_${fieldId}`;
if (fieldMap.has(colId)) return;
fieldMap.set(colId, {
meta: {
id: colId,
name: field.label,
type: field.type as CRMColumnMetadata['type'],
description: field.description || '',
enumOptions: field.options,
valueSortable: !NON_VALUE_SORTABLE.has(field.type),
hasSignal: field.signal?.enabled === true,
filterable: true,
filterDimensions: [
'value' as const,
...(field.signal?.enabled ? (['signal'] as const) : []),
],
},
displayOrder: field.displayOrder ?? 0,
});
},
);
}
const sorted = Array.from(fieldMap.values()).sort((a, b) => a.displayOrder - b.displayOrder);
const result: CRMColumnMetadata[] = [];
for (const { meta } of sorted) {
result.push(meta);
if (meta.hasSignal) {
result.push({
id: `${meta.id}_signal`,
name: 'Signal',
type: 'score',
description: `Signal for ${meta.name}`,
parentColumnId: meta.id,
signalDimension: 'signal',
});
}
}
return result;
}