import type { ConditionalRule, FilterTextMask, } from "./table-properties-types" /** * Conditional-formatting matchers shared by `DataTable` cells, * `data-row-list` rows, and board card backgrounds. Keeping the logic * here ensures the table grid and the list / board derivatives all * paint the same row with the same rule (no UI drift between views). * * `columns` is optional and only used when the consumer needs to: * - read from a different row key than the rule's `fieldKey` * (`sortKey`-aliased columns), or * - apply a `textMask` (phone / zip) so "contains 555" matches the * raw digits of "(415) 555-0100". */ export type ConditionalColumnHint = { key: string sortKey?: string filter?: { type?: string; textMask?: FilterTextMask } } function rowValueForRule>( row: T, rule: ConditionalRule, columns?: ConditionalColumnHint[], ): string { const col = columns?.find((c) => c.key === rule.fieldKey) const dataKey = (col?.sortKey ?? rule.fieldKey) as keyof T return String(row[dataKey] ?? "") } /** * Whether a rule carries enough input to paint anything. Half-built rules (a * field picked, no value typed) must not light up the column-header indicator * either, so the header and the cells agree on what "applied" means. */ export function ruleHasActiveValues( rule: ConditionalRule, columns?: ConditionalColumnHint[], ): boolean { if (rule.values.length === 0) return false const col = columns?.find((c) => c.key === rule.fieldKey) if (col?.filter?.type === "text") return (rule.values[0] ?? "").trim().length > 0 return true } function conditionalTextMatches( cellVal: string, needle: string, op: "contains" | "not_contains", textMask: FilterTextMask | undefined, ) { const v = cellVal.trim() const n = needle.trim() if (!n) return op === "not_contains" if (textMask === "phone" || textMask === "zip") { const nd = n.replace(/\D/g, "") const hay = v.replace(/\D/g, "") if (!nd) return op === "not_contains" const hit = hay.includes(nd) return op === "contains" ? hit : !hit } const hit = v.toLowerCase().includes(n.toLowerCase()) return op === "contains" ? hit : !hit } /** Whether a conditional rule matches a row (same logic as DataTable cells). */ export function conditionalRuleMatchesRow>( row: T, rule: ConditionalRule, columns?: ConditionalColumnHint[], ): boolean { if (!ruleHasActiveValues(rule, columns)) return false const v = rowValueForRule(row, rule, columns).trim() const col = columns?.find((c) => c.key === rule.fieldKey) const textMask = col?.filter?.type === "text" ? col.filter.textMask : undefined switch (rule.operator) { case "is": return rule.values.includes(v) case "is_not": return !rule.values.includes(v) case "contains": return rule.values.some((val) => conditionalTextMatches(v, val, "contains", textMask), ) case "not_contains": return !rule.values.some((val) => conditionalTextMatches(v, val, "contains", textMask), ) default: return false } } /** First matching conditional rule background for a row (list/board row tint). */ export function getConditionalRowBackground>( row: T, rules: ConditionalRule[] | undefined, columns?: ConditionalColumnHint[], ): string | undefined { if (!rules?.length) return undefined for (const rule of rules) { if (conditionalRuleMatchesRow(row, rule, columns)) return rule.bgColor } return undefined } /** Background for one table cell from conditional rules on that column. */ export function getConditionalCellBackground>( row: T, colKey: string, rules: ConditionalRule[] | undefined, columns?: ConditionalColumnHint[], ): string | undefined { if (!rules?.length) return undefined const rule = rules.find((r) => r.fieldKey === colKey) if (!rule || !conditionalRuleMatchesRow(row, rule, columns)) return undefined return rule.bgColor }