feat(uat-batch): Groups J + K — activity feed + onboarding resolver-chain
J38, J39, K40 (core) from the 2026-05-21 plan.
Shipped:
J38 EntityActivityFeed sentence rendering surfaces the new value
inline. Was "<actor> updated the X"; now "<actor> set X to
<value>" when the audit row carries `newValue`. Field-level
diff line underneath keeps showing the old → new strikethrough
for context. Truncates inline value at 60 chars to keep long
notes / descriptions from blowing out the row.
J39 Client → Companies tab CTA. Empty state gains a "Link to a
company" action; populated state grows a top-right "Link to
company" button. New <LinkCompanyDialog> wraps the existing
<CompanyPicker> + a membership-role select + an "is primary"
checkbox, then POSTs to /api/v1/companies/[id]/members.
Empty-state copy dropped "Add a membership from a company's
detail page" — the rep can act inline now.
K40 OnboardingChecklist resolver-chain. The auto-check no longer
reads raw `/admin/settings` rows (which miss env fallbacks).
Resolved endpoint widened to accept `?keys=k1,k2,...` so the
checklist can batch-resolve any heterogenous set of registry
keys through port → global → env → default in one round-trip.
Checklist captures the dominant source per step ("env fallback",
"global default", "built-in default") and surfaces it inline
under the green tick so super-admins see when a step is
relying on env rather than a per-port override. Compound-key
gates report the weakest sub-key's source so a partially-env
config still flags clearly.
Topbar banner / dashboard tile / weekly nudge / celebration
sub-items remain queued — the core resolver-chain gap was
the actual cause of the "step never ticks" UAT complaint.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,15 +65,36 @@ function formatValueForField(field: string | null, value: unknown): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
/** Natural-sentence rendering of one audit row. Falls back to terse when
|
||||
* there's no field to talk about. */
|
||||
/** Natural-sentence rendering of one audit row.
|
||||
*
|
||||
* Behaviour ladder:
|
||||
* 1. Field changed with both old + new values → "set <field> to <new>"
|
||||
* (the inline diff below already shows the old-value strikethrough,
|
||||
* so we don't restate it here; result reads like a sentence rather
|
||||
* than a diff label).
|
||||
* 2. Field changed with only a new value → "set <field> to <new>"
|
||||
* 3. Field changed with neither value (rare; legacy rows) → "<verb>
|
||||
* the <field>"
|
||||
* 4. No field → "<verb> this record"
|
||||
*
|
||||
* Truncation at 60 chars on the inline value keeps long body fields
|
||||
* (notes, descriptions) from blowing out the row — the diff line
|
||||
* below still renders the full value if the rep clicks through.
|
||||
*/
|
||||
function sentence(row: AuditRow, actor: string): string {
|
||||
const verb = actionVerb(row.action);
|
||||
const field = formatField(row.fieldChanged);
|
||||
if (field) {
|
||||
return `${actor} ${verb} the ${field}`;
|
||||
if (!field) return `${actor} ${verb} this record`;
|
||||
|
||||
const newFormatted =
|
||||
row.newValue !== null && row.newValue !== undefined
|
||||
? formatValueForField(row.fieldChanged, row.newValue)
|
||||
: null;
|
||||
if (newFormatted) {
|
||||
const truncated = newFormatted.length > 60 ? newFormatted.slice(0, 60) + '…' : newFormatted;
|
||||
return `${actor} set ${field} to "${truncated}"`;
|
||||
}
|
||||
return `${actor} ${verb} this record`;
|
||||
return `${actor} ${verb} the ${field}`;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
||||
Reference in New Issue
Block a user