@startsimpli/ui 0.4.135 → 0.4.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @startsimpli/ui/history — the shapes the shared record history panel renders
3
+ * (bd startsim-g4rwo; trail bd startsim-o1qib).
4
+ *
5
+ * Structural on purpose, exactly like `activity/types.ts`: packages/ui imports
6
+ * no API client, so these mirror one `/api/v1/entities/<id>/revisions/` row
7
+ * without depending on @startsimpli/api. The consuming app injects its own
8
+ * authed client (see {@link RevisionClient}).
9
+ *
10
+ * ── THE SPELLING TRAP, AND WHY EVERY KEY HERE IS OPTIONAL TWICE ─────────────
11
+ *
12
+ * The wire is snake_case. The shared API client
13
+ * (`packages/api/src/utils/case-transform.ts`) camelCases every response key
14
+ * RECURSIVELY and has no preserve list, so an app that fetches this route
15
+ * through `FetchWrapper` receives `actorKind`, `createdAt`, `redactedFields`
16
+ * and `valuesOmitted`. Both spellings are therefore declared and
17
+ * {@link normalizeRevision} reads either, because a panel that renders blank
18
+ * actors against a camelising client — and correctly against a raw one — is a
19
+ * bug nobody would find twice.
20
+ *
21
+ * WHAT THAT TRANSFORM BREAKS AND THIS MODULE CANNOT FIX: the keys INSIDE
22
+ * `metadata.changed` are the tenant's own declared attribute names, so a
23
+ * camelising client renames `team_verdict` to `teamVerdict` and there is no
24
+ * safe inverse — `snake_to_camel` is not injective, and guessing one would
25
+ * print a field name the tenant never declared. So field names are rendered
26
+ * EXACTLY as received and never re-cased. A consumer that wants the declared
27
+ * spellings must fetch this route without the case transform; one that cannot
28
+ * should pass `fieldLabel` to map them. Stated here rather than discovered
29
+ * later, because the same transform already corrupted declared names once.
30
+ */
31
+
32
+ /**
33
+ * The actor vocabulary lives on `ActorIdentity`, which is the component that
34
+ * RENDERS it, and is re-exported here so a consumer of this module needs one
35
+ * import rather than two. Declared once (rule #1): a second union would be a
36
+ * second place for "person | machine | unknown" to drift, and the three-state
37
+ * shape is the one thing in this epic that must not.
38
+ */
39
+ import type { ActorKind, ActorKindSource } from '../ActorIdentity';
40
+
41
+ export type { ActorKind, ActorKindSource };
42
+
43
+ /** `deleted` is declared server-side and not yet written — bd startsim-jkkn7.6. */
44
+ export type RevisionAction = 'created' | 'updated' | 'deleted';
45
+
46
+ /** Which endpoint wrote. `server` is a management command or a worker — a real
47
+ * write that must not claim to be an endpoint. */
48
+ export type RevisionSource = 'create' | 'patch' | 'put' | 'upsert' | 'server';
49
+
50
+ /** `none` always, today; `matched` arrives with the conditional write
51
+ * (bd startsim-3c2wc). */
52
+ export type RevisionPrecondition = 'none' | 'matched';
53
+
54
+ /**
55
+ * One `metadata.changed` entry, straight off the wire.
56
+ *
57
+ * THE ABSENCE OF A SIDE IS THE PAYLOAD, which is why neither key is nullable
58
+ * rather than optional-and-nullable:
59
+ *
60
+ * - a field the write CREATED has `after` and NO `before` key;
61
+ * - a field the write DELETED has `before` and NO `after` key — the server
62
+ * never writes `after: null`, because a key a person deleted is a different
63
+ * fact from a key set to `null`, and `null` IS a legal stored value;
64
+ * - a field the read gate WITHHELD is `{}` and the row's `redacted_fields`
65
+ * names it;
66
+ * - a row over the byte cap has `{}` for EVERY field and
67
+ * `metadata.values_omitted` set.
68
+ *
69
+ * So `'after' in entry` is the only correct test and `entry.after === undefined`
70
+ * is wrong. {@link normalizeFieldChange} is the one place that does it.
71
+ */
72
+ export interface RevisionFieldChange {
73
+ before?: unknown;
74
+ after?: unknown;
75
+ /** The value was too large to store. WHEN THIS IS TRUE A MISSING SIDE SAYS
76
+ * NOTHING — it was dropped, not deleted — so the delete inference above is
77
+ * invalid and a diff over this entry would show a deletion that never
78
+ * happened. */
79
+ truncated?: boolean;
80
+ }
81
+
82
+ export interface RevisionMetadata {
83
+ /** ALWAYS PRESENT, possibly `{}`. Keyed by the tenant's declared attribute
84
+ * names — see the spelling trap in this module's header. */
85
+ changed?: Record<string, RevisionFieldChange>;
86
+ /** Present only when the name COLUMN moved. Namespaced away from `changed`
87
+ * because a tenant may declare a data key literally called "name". */
88
+ name?: RevisionFieldChange;
89
+ /** Fields `human_edits.guard_machine_write` HELD BACK. A row carrying these
90
+ * is a machine ATTEMPT, not a change, and its `changed` is `{}` — which is
91
+ * correct, not a bug. */
92
+ preserved?: string[];
93
+ /** What an upsert CLAIMED it was re-authoring. The one override that
94
+ * legitimately destroys a person's text, so it is shown. */
95
+ regenerate?: string[];
96
+ /** The whole row exceeded the row cap: every field is still NAMED in
97
+ * `changed`, every value is `{}`. */
98
+ values_omitted?: boolean;
99
+ /** Camelised alias — see the header. */
100
+ valuesOmitted?: boolean;
101
+ }
102
+
103
+ /** One row of the append-only revision trail. Only `id` and `version` are
104
+ * relied on; everything else is treated as optional so a row from a tenant
105
+ * running an older build still renders. */
106
+ export interface EntityRevision {
107
+ id: string;
108
+ /** 1-based, GAPLESS 1..N per entity, DB-unique. A hole is a provably lost
109
+ * revision — which is what the unique constraint exists to make visible. */
110
+ version: number;
111
+ action?: RevisionAction | string;
112
+ source?: RevisionSource | string;
113
+ precondition?: RevisionPrecondition | string;
114
+ created_at?: string | null;
115
+ createdAt?: string | null;
116
+ /** May be `""`. */
117
+ actor_sub?: string | null;
118
+ actorSub?: string | null;
119
+ /** The actor's email, denormalised at write time. For a machine this is
120
+ * often not an address at all (`svc:n8n-ogmc`). */
121
+ actor_label?: string | null;
122
+ actorLabel?: string | null;
123
+ actor_kind?: ActorKind | string | null;
124
+ actorKind?: ActorKind | string | null;
125
+ actor_kind_source?: ActorKindSource | string | null;
126
+ actorKindSource?: ActorKindSource | string | null;
127
+ metadata?: RevisionMetadata | null;
128
+ /** PRESENT ONLY when the per-field gate withheld something on this row. */
129
+ redacted_fields?: string[] | null;
130
+ redactedFields?: string[] | null;
131
+ }
132
+
133
+ /** The DRF envelope. `redacted_fields` is the server's roll-up of every row on
134
+ * this page (`apps/api/pagination.py`), present only when something really
135
+ * was held back. */
136
+ export interface RevisionPage {
137
+ count?: number;
138
+ next?: string | null;
139
+ previous?: string | null;
140
+ results?: EntityRevision[];
141
+ redacted_fields?: string[] | null;
142
+ redactedFields?: string[] | null;
143
+ }
144
+
145
+ /** Narrowing the panel asks for. Server-side paginated (repo rule 8) — the
146
+ * route caps `page_size` at 200 and 400s a parameter it does not recognise,
147
+ * so this stays deliberately minimal. */
148
+ export interface RevisionQuery {
149
+ page?: number;
150
+ pageSize?: number;
151
+ // Index signature so this satisfies an API client's query-params type; the
152
+ // injected client is the app's own.
153
+ [key: string]: unknown;
154
+ }
155
+
156
+ /**
157
+ * The one call the panel needs. The consuming app injects its authed client
158
+ * (`{ list: (p) => api.entities.revisions(id, p) }`), which is what keeps
159
+ * @startsimpli/api out of packages/ui and the app wrapper under ten lines.
160
+ *
161
+ * A 404 from this route means the caller may not read the PARENT record — the
162
+ * route is nested and an unreadable row is simply absent. The panel surfaces
163
+ * that as "you cannot see this record's history", never as an empty trail.
164
+ */
165
+ export interface RevisionClient {
166
+ list: (query: RevisionQuery) => Promise<RevisionPage>;
167
+ }
@@ -155,7 +155,13 @@ export * from './calendar'
155
155
  // display name is never printed alone as if it were an identity
156
156
  // (startsim-768w.19.8.1).
157
157
  export { ActorIdentity, actorInitials, actorLabel, actorHandle } from './ActorIdentity'
158
- export type { ActorIdentityProps, ActorIdentityValue } from './ActorIdentity'
158
+ export { actorKindTitle } from './ActorIdentity'
159
+ export type {
160
+ ActorIdentityProps,
161
+ ActorIdentityValue,
162
+ ActorKind,
163
+ ActorKindSource,
164
+ } from './ActorIdentity'
159
165
 
160
166
  // Activity components (timeline, quick log, log dialog)
161
167
  export { ActivityTimeline, dayHeading } from './ActivityTimeline'
@@ -168,6 +174,10 @@ export type {
168
174
  // The shared, filterable activity feed: filter bar + page-level composer over
169
175
  // any append-only event stream (startsim-768w.19.5).
170
176
  export * from './activity'
177
+
178
+ // The shared record history panel: who changed which field, from what, to
179
+ // what, and whether a machine did it (bd startsim-g4rwo).
180
+ export * from './history'
171
181
  export { QuickLogButtons } from './QuickLogButtons'
172
182
  export type { QuickLogButtonsProps, QuickLogAction } from './QuickLogButtons'
173
183
  export { LogActivityDialog } from './LogActivityDialog'