-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathmanagedByEmptyState.ts
More file actions
117 lines (114 loc) · 5.77 KB
/
Copy pathmanagedByEmptyState.ts
File metadata and controls
117 lines (114 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* Derive a context-aware empty-state config for object lists whose
* `managedBy` bucket means the user can't create rows directly from
* this list view.
*
* Background: previously the entire affordance story was carried by a
* full-width banner pinned to the top of every list/detail/form page.
* That violated the principle most enterprise platforms (Salesforce,
* ServiceNow, SAP Fiori, Notion, Linear) settled on long ago: hide the
* affordance you don't want users to take, and use the *empty state* as
* the only place to explain why the list is empty and where new entries
* come from.
*
* This helper returns an `emptyState` payload compatible with the
* `ListView` schema (`{ title, message, icon }`). It only fires for the
* buckets where the default empty state ("No records yet") would be
* misleading; for `platform`/`config` it returns `undefined` so the
* caller falls back to the user-defined view-level empty state.
*
* The bucket → message mapping mirrors `ManagedByBadge` so that the badge
* (in the header) and the empty state (in the body) tell a consistent
* story without repeating themselves verbatim.
*/
import { resolveCrudAffordances, type UserActionsOverride } from '@object-ui/core';
export interface ManagedByEmptyState {
title: string;
message: string;
icon: string;
}
/**
* Translator function, structurally compatible with the `t` returned by
* `useObjectTranslation()`. Accepts a key and optional options (including a
* `defaultValue` used as the English fallback when a locale lacks the key).
*/
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
export function resolveManagedByEmptyState(
managedBy: string | undefined | null,
t: TranslateFn,
objectName?: string | null,
userActions?: UserActionsOverride | null,
): ManagedByEmptyState | undefined {
switch (managedBy) {
case 'system':
// ADR-0103 — a `system` object that opened creation is admin/user-writable
// data (e.g. Notification Preferences). The "entries appear automatically"
// copy would be wrong; fall back to the generic empty state (which surfaces
// the New button) by returning undefined. The resolved `create` affordance
// (shared @object-ui/core policy) is the one place that reads the override.
if (resolveCrudAffordances({ managedBy, userActions }).create) return undefined;
return {
icon: 'Lock',
title: t('list.managedBy.system.title', { defaultValue: 'Nothing here yet' }),
message: t('list.managedBy.system.message', {
defaultValue:
'Entries appear automatically when their source action runs (e.g. Submit for Approval, Share, Invite). Trigger one of those on a source record to create a row.',
}),
};
case 'append-only':
return {
icon: 'Archive',
title: t('list.managedBy.appendOnly.title', { defaultValue: 'No events recorded' }),
message: t('list.managedBy.appendOnly.message', {
defaultValue:
'This is an immutable audit log. Rows are written by the platform when events occur — you can export the history but cannot create entries from here.',
}),
};
case 'better-auth':
// `sys_user` is the one identity table with a concrete onboarding
// answer, so give it actionable guidance: teammates arrive via an
// org-level invite + SSO just-in-time provisioning (ADR-0024 D9), and
// app end-users self-register. We deliberately do NOT name the env-level
// "Invite User" action — it is multi-org-gated and hidden in single-org —
// nor a "Reset Password" toolbar action, which does not exist (cloud#580).
// Every other identity table (sessions, accounts, tokens, jwks,
// verifications, …) is written purely by auth flows, so keep the generic
// copy — naming an invite/signup CTA on a token list would be wrong.
if (objectName === 'sys_user') {
return {
icon: 'ShieldAlert',
title: t('list.managedBy.betterAuthUser.title', { defaultValue: 'No users yet' }),
message: t('list.managedBy.betterAuthUser.message', {
defaultValue:
'User accounts are provisioned by the authentication provider, not created here. Invite teammates to your organization and they appear automatically on first sign-in (SSO just-in-time provisioning). App end-users arrive when they sign up through your app.',
}),
};
}
// `sys_team` is the other identity table that CAN be created by hand —
// the `create_team` toolbar action (multi-org gated) hits better-auth's
// `organization/create-team`. The generic "not added by hand here" copy
// below would flatly contradict that visible Create Team button
// (objectui review — the reported empty-state/CTA mismatch). Give teams
// their own accurate copy instead of the identity-record disclaimer.
if (objectName === 'sys_team') {
return {
icon: 'Users',
title: t('list.managedBy.betterAuthTeam.title', { defaultValue: 'No teams yet' }),
message: t('list.managedBy.betterAuthTeam.message', {
defaultValue:
'Teams group members within an organization. Create one with “Create Team”, or they arrive through your auth provider’s organization and SSO provisioning flows.',
}),
};
}
return {
icon: 'ShieldAlert',
title: t('list.managedBy.betterAuth.title', { defaultValue: 'No identity records' }),
message: t('list.managedBy.betterAuth.message', {
defaultValue:
'These records are created by the authentication provider — through sign-in, provisioning, and security flows — not added by hand here.',
}),
};
default:
return undefined;
}
}