Files
inbuxa-admin/src/help/defaults.ts
T
jcoffey-dev 3d3dcb0227 Hover cards for domains and people, and defaults in option tooltips
- A domain's name in any list opens a card on hover: whether it's taking
  mail, how many people it has, which of its key records (mail routing,
  SPF, DKIM, DMARC) are live in public DNS, and whether DNS, signing keys
  and certificates are automatic.
- A person's shows their name, storage against their quota, role, groups
  and when they joined. The server keeps no last sign-in on the account,
  so the card doesn't claim one.
- Cards load on open and are cached for a minute.
- Option tooltips end with the option's default, and an option set away
  from its default gets a small "changed" mark.
2026-09-19 02:06:50 -07:00

75 lines
2.8 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* An option's default, as words, and whether the current value differs
* from it, so tooltips can say "Default: 5 min" and a form can mark what
* someone has changed.
*/
import type { Field, Schema } from '@/types/schema';
import { formatDuration, formatSize } from '@/lib/durationFormat';
function stable(v: unknown): string {
if (v && typeof v === 'object' && !Array.isArray(v)) {
const o = v as Record<string, unknown>;
return `{${Object.keys(o)
.sort()
.map((k) => `${JSON.stringify(k)}:${stable(o[k])}`)
.join(',')}}`;
}
return JSON.stringify(v ?? null);
}
/** Does the value differ from the default? An unset value counts as the default. */
export function differsFromDefault(value: unknown, def: unknown): boolean {
if (def === undefined) return false;
if (value === undefined || value === null) return false;
return stable(value) !== stable(def);
}
/** The default as someone would say it, or null when there's nothing short to say. */
export function describeDefault(
field: Field,
def: unknown,
schema: Schema,
words: { on: string; off: string; none: string },
): string | null {
if (def === undefined) return null;
if (def === null) return words.none;
const ft = field.type as { type: string; format?: string; enumName?: string; objectName?: string };
if (typeof def === 'boolean') return def ? words.on : words.off;
if (typeof def === 'number') {
if (ft.format === 'duration') return formatDuration(def);
if (ft.format === 'size') return formatSize(def);
return String(def);
}
if (typeof def === 'string') {
if (ft.type === 'enum' && ft.enumName) {
return schema.enums[ft.enumName]?.find((e) => e.name === def)?.label ?? def;
}
return def.length > 60 ? null : def;
}
if (typeof def === 'object' && !Array.isArray(def)) {
const variant = (def as Record<string, unknown>)['@type'];
if (typeof variant === 'string' && ft.objectName) {
const sch = schema.schemas[ft.objectName];
const label = sch?.type === 'multiple' ? sch.variants.find((v) => v.name === variant)?.label : undefined;
return label ?? variant;
}
const keys = Object.keys(def as Record<string, unknown>);
if (ft.type === 'set') {
if (keys.length === 0) return words.none;
if (ft.enumName || (field.type as { class?: { enumName?: string } }).class?.enumName) {
const en = (field.type as { class?: { enumName?: string } }).class?.enumName ?? ft.enumName!;
const labels = keys.map((k) => schema.enums[en]?.find((e) => e.name === k)?.label ?? k);
return labels.length > 4 ? `${labels.slice(0, 4).join(', ')}…` : labels.join(', ');
}
return keys.length > 4 ? `${keys.slice(0, 4).join(', ')}…` : keys.join(', ');
}
}
return null;
}