@rebasepro/cms-types 0.17.0-canary.0

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/dist/admin_collection.d.ts +650 -0
  3. package/dist/augment.d.ts +81 -0
  4. package/dist/collections.d.ts +282 -0
  5. package/dist/controllers/analytics_controller.d.ts +7 -0
  6. package/dist/controllers/auth.d.ts +111 -0
  7. package/dist/controllers/customization_controller.d.ts +69 -0
  8. package/dist/controllers/dialogs_controller.d.ts +36 -0
  9. package/dist/controllers/index.d.ts +10 -0
  10. package/dist/controllers/local_config_persistence.d.ts +20 -0
  11. package/dist/controllers/navigation.d.ts +248 -0
  12. package/dist/controllers/registry.d.ts +96 -0
  13. package/dist/controllers/side_dialogs_controller.d.ts +67 -0
  14. package/dist/controllers/side_panel_controller.d.ts +97 -0
  15. package/dist/controllers/snackbar.d.ts +45 -0
  16. package/dist/index.d.ts +18 -0
  17. package/dist/index.es.js +154 -0
  18. package/dist/index.es.js.map +1 -0
  19. package/dist/react_component_ref.d.ts +43 -0
  20. package/dist/rebase_context.d.ts +68 -0
  21. package/dist/types/breadcrumbs.d.ts +15 -0
  22. package/dist/types/builders.d.ts +15 -0
  23. package/dist/types/collection_views.d.ts +105 -0
  24. package/dist/types/component_overrides.d.ts +196 -0
  25. package/dist/types/entity_actions.d.ts +112 -0
  26. package/dist/types/entity_display.d.ts +148 -0
  27. package/dist/types/entity_link_builder.d.ts +7 -0
  28. package/dist/types/entity_views.d.ts +115 -0
  29. package/dist/types/export_import.d.ts +21 -0
  30. package/dist/types/form_layout.d.ts +126 -0
  31. package/dist/types/formex.d.ts +40 -0
  32. package/dist/types/index.d.ts +18 -0
  33. package/dist/types/locales.d.ts +4 -0
  34. package/dist/types/modify_collections.d.ts +5 -0
  35. package/dist/types/plugins.d.ts +277 -0
  36. package/dist/types/property_config.d.ts +74 -0
  37. package/dist/types/property_options.d.ts +255 -0
  38. package/dist/types/slots.d.ts +279 -0
  39. package/dist/types/translations.d.ts +989 -0
  40. package/dist/types/user_management_delegate.d.ts +22 -0
  41. package/package.json +103 -0
  42. package/src/admin_collection.ts +775 -0
  43. package/src/augment.ts +79 -0
  44. package/src/collections.ts +312 -0
  45. package/src/controllers/analytics_controller.tsx +57 -0
  46. package/src/controllers/auth.ts +122 -0
  47. package/src/controllers/customization_controller.tsx +81 -0
  48. package/src/controllers/dialogs_controller.tsx +37 -0
  49. package/src/controllers/index.ts +10 -0
  50. package/src/controllers/local_config_persistence.tsx +22 -0
  51. package/src/controllers/navigation.ts +288 -0
  52. package/src/controllers/registry.ts +114 -0
  53. package/src/controllers/side_dialogs_controller.tsx +82 -0
  54. package/src/controllers/side_panel_controller.tsx +112 -0
  55. package/src/controllers/snackbar.ts +51 -0
  56. package/src/index.ts +20 -0
  57. package/src/react_component_ref.ts +52 -0
  58. package/src/rebase_context.ts +81 -0
  59. package/src/types/breadcrumbs.ts +16 -0
  60. package/src/types/builders.ts +18 -0
  61. package/src/types/collection_views.tsx +125 -0
  62. package/src/types/component_overrides.ts +244 -0
  63. package/src/types/entity_actions.tsx +134 -0
  64. package/src/types/entity_display.ts +182 -0
  65. package/src/types/entity_link_builder.ts +8 -0
  66. package/src/types/entity_views.tsx +135 -0
  67. package/src/types/export_import.ts +26 -0
  68. package/src/types/form_layout.ts +137 -0
  69. package/src/types/formex.ts +45 -0
  70. package/src/types/index.ts +18 -0
  71. package/src/types/locales.ts +81 -0
  72. package/src/types/modify_collections.tsx +6 -0
  73. package/src/types/plugins.tsx +346 -0
  74. package/src/types/property_config.tsx +97 -0
  75. package/src/types/property_options.ts +300 -0
  76. package/src/types/slots.tsx +334 -0
  77. package/src/types/translations.ts +1104 -0
  78. package/src/types/user_management_delegate.ts +23 -0
@@ -0,0 +1,775 @@
1
+ /**
2
+ * The typed admin block, and the type you author a collection against.
3
+ *
4
+ * A collection is one file. Schema, security rules and callbacks sit at the top
5
+ * level, where the backend reads them; everything the admin panel renders sits
6
+ * under `admin`. `@rebasepro/types` does not declare that field at all — naming a
7
+ * kanban column definition would drag `React.ReactNode` back into the BaaS
8
+ * contract, and a server has no use for one. `augment.ts` in this package declares
9
+ * it, by declaration merging, onto core's `CollectionConfig`. So this is the other
10
+ * side of that boundary: the 38 fields, fully typed, in the package where React
11
+ * exists, and reachable only by a program that has opted in.
12
+ *
13
+ * Each field is declared exactly once, here. Core does not carry a React-free
14
+ * skeleton of the same shape; two definitions that agree only by luck is the
15
+ * `WhereFilterOp` mistake, and this block is far bigger than one union.
16
+ */
17
+ import type React from "react";
18
+ import type {
19
+ CollectionConfig,
20
+ ComponentRef,
21
+ FilterPreset,
22
+ FilterValues,
23
+ FirebaseCollectionConfig,
24
+ FirebaseProperties,
25
+ InferEntityType,
26
+ MongoDBCollectionConfig,
27
+ MongoProperties,
28
+ OrderBySpec,
29
+ PostgresCollectionConfig,
30
+ PostgresProperties,
31
+ User
32
+ } from "@rebasepro/types";
33
+ // A value, not a type: the runtime list core owns.
34
+ import { ADMIN_COLLECTION_KEYS as CORE_ADMIN_COLLECTION_KEYS, nestAdminCollectionKeys } from "@rebasepro/types";
35
+
36
+ import type {
37
+ AdditionalFieldDelegate,
38
+ CollectionActionsProps,
39
+ CollectionSize,
40
+ DefaultSelectedViewBuilder,
41
+ KanbanConfig,
42
+ SelectionController,
43
+ ViewMode
44
+ } from "./collections";
45
+ import type { EntityCustomView, FormViewConfig } from "./types/entity_views";
46
+ import type { CollectionCustomView } from "./types/collection_views";
47
+ import type { EntityDisplay } from "./types/entity_display";
48
+ import type { FormLayoutConfig } from "./types/form_layout";
49
+ import type { EntityAction } from "./types/entity_actions";
50
+ import type { ExportConfig } from "./types/export_import";
51
+ import type { CollectionComponentOverrideMap } from "./types/component_overrides";
52
+
53
+ /**
54
+ * A key naming one of `M`'s fields, or a dotted path into a `map` field.
55
+ *
56
+ * Both forms are resolved with `getValueInPath`, so `"profile.displayName"` is
57
+ * as valid as `"title"`. Only the *root* is checked — the path below it is a
58
+ * nested `Properties` object this type has no view of — which is enough to
59
+ * reject the mistake that actually happens: a misspelled or removed field.
60
+ *
61
+ * When `M` is the default `Record<string, unknown>` — the plain
62
+ * `const x: PostgresCollectionConfig = { … }` annotation, which infers nothing —
63
+ * `Extract<keyof M, string>` is `string` and this accepts anything, exactly as
64
+ * before. `defineCollection` is what supplies a real `M` and turns the check on.
65
+ */
66
+ export type PropertyPath<M> =
67
+ | Extract<keyof M, string>
68
+ | `${Extract<keyof M, string>}.${string}`;
69
+
70
+ /**
71
+ * The `display` block for a collection, with its property paths checked
72
+ * against `M`.
73
+ *
74
+ * `EntityDisplay` is generic over the path type so that
75
+ * `@rebasepro/cms-types`' two halves do not import each other in a cycle;
76
+ * this alias is what an authoring site actually names.
77
+ */
78
+ export type CollectionDisplay<
79
+ M extends Record<string, unknown> = Record<string, unknown>,
80
+ USER extends User = User
81
+ > = EntityDisplay<PropertyPath<M>, M, USER>;
82
+
83
+ /**
84
+ * A key naming a *column* in the list view: a property path, a child-collection
85
+ * column, or the `key` of one of this collection's `additionalFields`.
86
+ *
87
+ * `AdditionalFieldDelegate.key` is a plain `string`, and the block is not
88
+ * generic over its own `additionalFields`, so there is no type-level channel
89
+ * carrying those keys here. Accepting any string to cover them is what made this
90
+ * field unchecked in the first place; instead the two provable arms are closed
91
+ * and {@link AdditionalFieldKey} is the explicit, castable escape.
92
+ */
93
+ export type ColumnKey<M> =
94
+ | PropertyPath<M>
95
+ | `subcollection:${string}`
96
+ | AdditionalFieldKey;
97
+
98
+ /**
99
+ * Opt-out for a `propertiesOrder` / `listProperties` entry that names an
100
+ * `additionalFields` key rather than a property.
101
+ *
102
+ * The brand is **required**, which is the entire mechanism: a bare `"score"` is
103
+ * not assignable, so the entry has to be written `"score" as AdditionalFieldKey`
104
+ * — a visible admission that this key is not a property. An optional brand
105
+ * (`__additionalFieldKey?: never`) would be satisfied by every string and put us
106
+ * straight back to accepting typos.
107
+ *
108
+ * ```ts
109
+ * propertiesOrder: ["title", "score" as AdditionalFieldKey]
110
+ * ```
111
+ */
112
+ export type AdditionalFieldKey = string & { readonly __additionalFieldKey: true };
113
+
114
+ /**
115
+ * Admin-panel presentation and behaviour for a collection.
116
+ *
117
+ * A `type` rather than an `interface`, and that is load-bearing: TypeScript gives
118
+ * an implicit index signature to an object *type alias* but not to an interface.
119
+ * `toAdminCollectionConfig` has to widen a collection carrying this block to
120
+ * `Record<string, unknown>` in order to move the flattened keys back under
121
+ * `admin`, and as an interface that conversion is an error (TS2352, "index
122
+ * signature for type 'string' is missing"). Flipping it and running
123
+ * `pnpm typecheck` reproduces that in one line.
124
+ *
125
+ * Declaration merging is not wanted here anyway; a plugin adding fields to the
126
+ * block would have nothing reading them.
127
+ *
128
+ * @group Models
129
+ */
130
+ export type AdminCollectionOptions<
131
+ M extends Record<string, unknown> = Record<string, unknown>,
132
+ USER extends User = User
133
+ > = {
134
+ /**
135
+ * Icon for the navigation sidebar or cards.
136
+ *
137
+ * Either a Lucide icon name (`"FileText"`, `"ShoppingCart"`) or a rendered
138
+ * element. Prefer the name: it survives serialization, so the collection file
139
+ * stays loadable by the backend and by `rebase generate-sdk`, and it is what
140
+ * the schema editor writes back.
141
+ */
142
+ icon?: string | React.ReactNode;
143
+
144
+ /**
145
+ * Navigation group for this collection.
146
+ * Collections sharing the same group name will be visually grouped
147
+ * together in the drawer and home page. If not set, the collection
148
+ * falls into the default "Views" group.
149
+ */
150
+ group?: string;
151
+
152
+ /**
153
+ * Array of entity views that this collection has.
154
+ * Can be an array of `EntityCustomView` or a string representing the key of a global `EntityCustomView`.
155
+ */
156
+ entityViews?: (string | EntityCustomView<Record<string, unknown>>)[];
157
+
158
+ /**
159
+ * Default preview properties displayed when this collection is referenced to.
160
+ */
161
+ previewProperties?: Extract<keyof M, string>[];
162
+
163
+ /**
164
+ * Properties to display as columns in the list view.
165
+ * If not specified, the list view uses a smart default (Title, Status, Date).
166
+ */
167
+ listProperties?: ColumnKey<M>[];
168
+
169
+ /**
170
+ * How a record of this collection shows up — its title, subtitle, image,
171
+ * status, date and tags.
172
+ *
173
+ * Each role takes a property path or a resolver, and a resolver may be
174
+ * async:
175
+ *
176
+ * ```ts
177
+ * display: {
178
+ * title: "name",
179
+ * image: "cover.url",
180
+ * subtitle: ({ entity }) => `${entity.values.city}, ${entity.values.country}`,
181
+ * status: async ({ entity, context }) =>
182
+ * (await context.data.audits.get(`${entity.id}/latest`))?.state
183
+ * }
184
+ * ```
185
+ *
186
+ * Every role left out is derived from the property schema exactly as before,
187
+ * so a collection that says nothing renders as it always did. See
188
+ * {@link EntityDisplay} for what each role means and
189
+ * {@link EntityDisplayResolver} for what a resolver is handed.
190
+ */
191
+ readonly display?: EntityDisplay<PropertyPath<M>, M, USER>;
192
+
193
+ /**
194
+ * When editing a entity, you can choose to open the entity in a side dialog
195
+ * or in a full screen dialog. Defaults to `full_screen`.
196
+ */
197
+ openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
198
+
199
+ /**
200
+ * Controls what happens when a user clicks on a entity in the collection view.
201
+ * - `"edit"` (default): Opens the entity in the edit form.
202
+ * - `"view"`: Opens a read-only detail view with an "Edit" button.
203
+ */
204
+ defaultEntityAction?: "view" | "edit";
205
+
206
+ /**
207
+ * Replace the default entity form with a custom component.
208
+ * The Builder receives the same props as entity view tabs
209
+ * (entity, formContext, collection, etc.) and has full control over the UI.
210
+ *
211
+ * Works in both edit mode and read-only mode (when `defaultEntityAction`
212
+ * is `"view"`). In read-only mode, `formContext.readOnly` will be `true`.
213
+ */
214
+ formView?: FormViewConfig;
215
+
216
+ /**
217
+ * How the generated form is laid out: which properties are grouped into
218
+ * sections in the main column, and which are pulled out into the metadata
219
+ * rail beside it.
220
+ *
221
+ * Entirely optional. With no `form` block the layout is derived from the
222
+ * properties themselves — see {@link FormLayoutConfig} — which is what most
223
+ * collections should rely on. Reach for this when the derived grouping is
224
+ * wrong for your domain, not to restate it.
225
+ *
226
+ * Unlike {@link FormViewConfig}, this does not replace the generated form:
227
+ * every field keeps its validation, error focus, local-changes restore and
228
+ * autosave wiring.
229
+ */
230
+ form?: FormLayoutConfig<M>;
231
+
232
+ /**
233
+ * Prevent default actions from being displayed or executed on this collection.
234
+ */
235
+ disableDefaultActions?: ("edit" | "copy" | "delete")[];
236
+
237
+ /**
238
+ * Order in which the properties are displayed.
239
+ * If you are specifying your collection as code, the order is the same as the
240
+ * one you define in `properties`. Additional columns are added at the
241
+ * end of the list, if the order is not specified.
242
+ *
243
+ * You can use this prop to hide some properties from the table view.
244
+ * Note that if you set this prop, other ways to hide fields, like
245
+ * `hidden` in the property definition, will be ignored.
246
+ * `propertiesOrder` has precedence over `hidden`.
247
+ *
248
+ * Supported entry formats:
249
+ * - For properties, use the property key.
250
+ * - For additional fields, use the field key.
251
+ * - Child collections (Firestore subcollections, or Postgres relations
252
+ * with `many` cardinality) each get a column with id
253
+ * `subcollection:<slug>`, e.g. `subcollection:orders`.
254
+ */
255
+ propertiesOrder?: ColumnKey<M>[];
256
+
257
+ /**
258
+ * If enabled, content is loaded in batches. If `false` all entities in the
259
+ * collection are loaded. This means that when reaching the end of the
260
+ * collection, the admin will load more entities.
261
+ * You can specify a number to specify the pagination size (50 by default)
262
+ * Defaults to `true`
263
+ */
264
+ pagination?: boolean | number;
265
+
266
+ selectionEnabled?: boolean;
267
+
268
+ /**
269
+ * Pass your own selection controller if you want to control selected
270
+ * entities externally.
271
+ * @see useSelectionController
272
+ */
273
+ selectionController?: SelectionController<M>;
274
+
275
+ /**
276
+ * Force a filter in this view. If applied, the rest of the filters will
277
+ * be disabled. Filters applied with this prop cannot be changed.
278
+ * e.g. `fixedFilter: { age: [">", 18] }`
279
+ * e.g. `fixedFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
280
+ */
281
+ readonly fixedFilter?: FilterValues<PropertyPath<M>>;
282
+
283
+ /**
284
+ * Initial filters applied to the collection this collection is related to.
285
+ * Defaults to none. Filters applied with this prop can be changed.
286
+ * e.g. `defaultFilter: { age: [">", 18] }`
287
+ * e.g. `defaultFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
288
+ */
289
+ // Keyed by property *path*, not by `FilterValues<M>` — the latter types each
290
+ // value against that property's own type, which is what the old note here
291
+ // warned breaks code-defined collections (an `EntityReference` filter on a
292
+ // relation, a `Date` on a string column). Narrowing the key is independent
293
+ // of that, and a dotted path still reaches into a `map`/jsonb column.
294
+ readonly defaultFilter?: FilterValues<PropertyPath<M>>;
295
+
296
+ /**
297
+ * Pre-defined filter presets that appear as quick-access options in the
298
+ * collection toolbar. Each preset applies a set of filters (and
299
+ * optionally a sort order) with a single click.
300
+ *
301
+ * ```ts
302
+ * filterPresets: [
303
+ * {
304
+ * label: "Shipped this month",
305
+ * filterValues: {
306
+ * status: ["==", "shipped"],
307
+ * order_date: [">=", new Date(Date.now() - 30 * 86400000)]
308
+ * }
309
+ * }
310
+ * ]
311
+ * ```
312
+ */
313
+ readonly filterPresets?: FilterPreset<PropertyPath<M>>[];
314
+
315
+ /**
316
+ * Default sort applied to this collection.
317
+ * When setting this prop, entities will have a default order
318
+ * applied in the collection.
319
+ *
320
+ * One key, or several applied in order of significance — the second breaks
321
+ * ties on the first, and so on. The row id breaks the last tie, so the
322
+ * order is total and paging over it neither repeats nor skips rows.
323
+ *
324
+ * @example sort: ["order", "asc"]
325
+ * @example sort: [["roles", "asc"], ["createdAt", "desc"]]
326
+ */
327
+ readonly sort?: OrderBySpec<PropertyPath<M>>;
328
+
329
+ /**
330
+ * You can add additional fields to the collection view by implementing
331
+ * an additional field delegate.
332
+ */
333
+ readonly additionalFields?: AdditionalFieldDelegate<M, USER>[];
334
+
335
+ /**
336
+ * Default size of the rendered collection
337
+ */
338
+ defaultSize?: CollectionSize;
339
+
340
+ /**
341
+ * Can the elements in this collection be edited inline in the collection
342
+ * view. Even when inline editing is disabled, entities can still be
343
+ * edited in the side panel (subject to `securityRules`).
344
+ */
345
+ inlineEditing?: boolean;
346
+
347
+ /**
348
+ * Should this collection be hidden from the main navigation panel, if
349
+ * it is at the root level, or in the entity side panel if it's a
350
+ * subcollection.
351
+ * It will still be accessible if you reach the specified path.
352
+ * You can also use this collection as a reference target.
353
+ *
354
+ * Note that this covers *both* roles at once. A collection that is a root
355
+ * collection **and** the target of a many-relation is hidden in both places,
356
+ * which is rarely what you want for a join or audit table: it should not be
357
+ * a destination in the drawer, but it is exactly what you want to see on its
358
+ * parent. Use {@link hideFromEntityViews} to separate the two.
359
+ */
360
+ hideFromNavigation?: boolean;
361
+
362
+ /**
363
+ * Should this collection be hidden from the tab strip of a parent entity,
364
+ * when it is reached as a child view (a Firestore subcollection, or the
365
+ * target of a `many`-cardinality relation).
366
+ *
367
+ * Independent of {@link hideFromNavigation}, which governs the drawer. The
368
+ * two exist separately because a collection commonly plays both roles and
369
+ * wants a different answer for each:
370
+ *
371
+ * - a join table (`company_members`) is not a destination but *is* a
372
+ * meaningful tab → `hideFromNavigation: true`, this left unset;
373
+ * - a table with a dedicated workspace (`scraped_jobs`) may want the
374
+ * opposite, so the workspace stays the only way in.
375
+ *
376
+ * Defaults to `false`. Setting {@link hideFromNavigation} does not imply it.
377
+ */
378
+ hideFromEntityViews?: boolean;
379
+
380
+ /**
381
+ * If you want to open custom views or subcollections by default when opening the edit
382
+ * view of a entity, you can specify the path to the view here.
383
+ * The path is relative to the current collection. For example if you have a collection
384
+ * that has a custom view as well as a subcollection that refers to another entity, you can
385
+ * either specify the path to the custom view or the path to the subcollection.
386
+ */
387
+ defaultSelectedView?: string | DefaultSelectedViewBuilder;
388
+
389
+ /**
390
+ * Should the ID of this collection be hidden from the form view.
391
+ */
392
+ hideIdFromForm?: boolean;
393
+
394
+ /**
395
+ * Should the ID of this collection be hidden from the grid view.
396
+ */
397
+ hideIdFromCollection?: boolean;
398
+
399
+ /**
400
+ * If set to true, the form will be auto-saved when the user changes
401
+ * the value of a field.
402
+ * Defaults to false.
403
+ * When a new entity is created, this property can be updated to generated a new ID
404
+ */
405
+ formAutoSave?: boolean;
406
+
407
+ /**
408
+ *
409
+ */
410
+ exportable?: boolean | ExportConfig<USER>;
411
+
412
+ /**
413
+ * Width of the side dialog (in pixels) when opening a entity in this collection.
414
+ */
415
+ sideDialogWidth?: number | string;
416
+
417
+ /**
418
+ * If set to true, the default values of the properties will be applied
419
+ * to the entity every time the entity is updated (not only when created).
420
+ * Defaults to false.
421
+ */
422
+ alwaysApplyDefaultValues?: boolean;
423
+
424
+ /**
425
+ * If set to true, a tab including the JSON representation of the entity will be included.
426
+ */
427
+ includeJsonView?: boolean;
428
+
429
+ /**
430
+ * Should local changes be backed up in local storage, to prevent data loss on
431
+ * accidental navigations.
432
+ * - `manual_apply`: When the user navigates back to a entity with local changes,
433
+ * they will be prompted to restore the changes.
434
+ * - `auto_apply`: When the user navigates back to a entity with local changes,
435
+ * the changes will be automatically applied.
436
+ * - `false`: Local changes will not be backed up.
437
+ * Defaults to `manual_apply`.
438
+ */
439
+ localChangesBackup?: "manual_apply" | "auto_apply" | false;
440
+
441
+ /**
442
+ * Default view mode for displaying this collection.
443
+ * - "list": Display entities as a list (default)
444
+ * - "table": Display entities in a table with inline editing
445
+ * - "cards": Display entities as a grid of cards with thumbnails
446
+ * - "kanban": Display entities in a Kanban board grouped by a property
447
+ * - any `key` from {@link customViews}
448
+ * Defaults to "list".
449
+ */
450
+ defaultViewMode?: ViewMode;
451
+
452
+ /**
453
+ * Which view modes are available for this collection.
454
+ * Possible values: "list", "table", "cards", "kanban", and any `key` from
455
+ * {@link customViews}.
456
+ * Defaults to all four built-ins plus every declared custom view.
457
+ * Note: "kanban" will only be available if the collection has at least
458
+ * one string property with `enum` defined, regardless of this setting.
459
+ * With a single entry the view switcher is hidden.
460
+ */
461
+ enabledViews?: ViewMode[];
462
+
463
+ /**
464
+ * Additional ways to render this collection's rows, offered in the view
465
+ * switcher beside list / table / cards / kanban.
466
+ *
467
+ * Can be an array of `CollectionCustomView` or a string naming the `key` of
468
+ * one registered globally on `<RebaseCMS collectionViews={…}>`. The
469
+ * string form is what lets a React-free config package reference React UI,
470
+ * and it is what the collection editor stores.
471
+ *
472
+ * A custom view is another rendering of the *same query* — it is handed the
473
+ * live table controller and inherits filters, search and the entity side
474
+ * panel. Use an `AppView` instead for a workflow spanning collections.
475
+ *
476
+ * @example
477
+ * ```ts
478
+ * admin: {
479
+ * customViews: [
480
+ * { key: "map", name: "Map", icon: "Map", Builder: MapView }
481
+ * ],
482
+ * enabledViews: ["table", "map"],
483
+ * defaultViewMode: "map"
484
+ * }
485
+ * ```
486
+ */
487
+ customViews?: (string | CollectionCustomView<Record<string, unknown>>)[];
488
+
489
+ /**
490
+ * Configuration for Kanban board view mode.
491
+ * When set, the Kanban view mode becomes available.
492
+ *
493
+ * A board is only half-configured without {@link orderProperty}. Cards
494
+ * still drag between columns — that writes `columnProperty` — but their
495
+ * order *within* a column has nowhere to be stored, so it resets on the
496
+ * next read and the board renders a warning bar saying so. Declare both,
497
+ * always.
498
+ */
499
+ kanban?: KanbanConfig<M>;
500
+
501
+ /**
502
+ * Property key to use for ordering items.
503
+ *
504
+ * Must reference a **string** property — a `number` can never hold one of
505
+ * these keys, so a numeric `sortOrder` leaves the board permanently asking
506
+ * to be initialised. The convention across the collections here is a
507
+ * hidden `__order: { type: "string", admin: { disabled: true,
508
+ * hideFromCollection: true } }`.
509
+ *
510
+ * Reordering writes a `fractional-indexing` key built from the base36,
511
+ * lower-case alphabet `0123456789abcdefghijklmnopqrstuvwxyz` — `"i0"`,
512
+ * `"i1"`, `"i0i"`. Single case because *Postgres* does the sorting and its
513
+ * default collation is not byte ordering; base36 rather than the library's
514
+ * default base62 for the same reason. Generating a key without passing
515
+ * that alphabet yields base62 keys (`"a0"`), which this board rejects.
516
+ *
517
+ * Nothing assigns a key on insert. A row created by a cron, a seed, a
518
+ * migration or the REST API lands with this property null, and the board
519
+ * shows an **Initialize** bar until someone clicks it. Backends that
520
+ * create rows for a board should append a key themselves — see the
521
+ * "Kanban boards" section of the `rebase-collections` skill.
522
+ *
523
+ * Used by Kanban view for ordering within columns and can be used for
524
+ * general ordering purposes.
525
+ */
526
+ readonly orderProperty?: Extract<keyof M, string>;
527
+
528
+ /**
529
+ * Actions that can be performed on the entities in this collection.
530
+ *
531
+ * An entry may be the action itself, or the `key` of one registered app-level
532
+ * on `<RebaseCMS entityActions={…}>` — `resolveEntityAction` looks a string
533
+ * up against that list.
534
+ *
535
+ * The key form is what lets a collection declared in a React-free config
536
+ * package use an action whose UI is React: an action carries an `onClick` and
537
+ * usually renders a dialog, so importing one into a collection file pulls the
538
+ * admin bundle into any backend that loads it for its schema. Naming it costs
539
+ * nothing there.
540
+ *
541
+ * `string` was accepted at runtime and by the collection editor — which stores
542
+ * exactly these keys — long before the type said so, which meant the documented
543
+ * approach needed a cast. Mirrors `entityViews`, typed this way already.
544
+ */
545
+ entityActions?: (string | EntityAction<M, USER>)[];
546
+
547
+ /**
548
+ * Builder for the collection actions rendered in the toolbar
549
+ */
550
+ Actions?: ComponentRef<CollectionActionsProps>[];
551
+
552
+ /**
553
+ * Collection-scoped component overrides. These take precedence over
554
+ * global overrides set on `<Rebase>`, but only within this collection's
555
+ * views (entity form, detail view, table, empty state, etc.).
556
+ *
557
+ * Only collection-scoped components (like `Entity.Form`, `Collection.EmptyState`,
558
+ * `Collection.Card`, etc.) can be overridden here. App-level components
559
+ * (like `Shell.AppBar`, `HomePage`) can only be overridden at the `<Rebase>` level.
560
+ *
561
+ * @example
562
+ * ```tsx
563
+ * const productsCollection: PostgresCollectionConfig = {
564
+ * name: "Products",
565
+ * slug: "products",
566
+ * table: "products",
567
+ * components: {
568
+ * "Entity.Form": { Component: ProductCustomForm },
569
+ * "Collection.Card": { Component: ProductCard },
570
+ * },
571
+ * properties: { ... }
572
+ * };
573
+ * ```
574
+ */
575
+ components?: CollectionComponentOverrideMap;};
576
+
577
+ /**
578
+ * There is deliberately no `AdminCollectionConfig` here any more.
579
+ *
580
+ * It used to be `Omit<CollectionConfig, "admin"> & { admin?: AdminCollectionOptions }`,
581
+ * a wrapper that existed because core typed the block opaquely. Now that `augment.ts`
582
+ * declares `admin` directly on `BaseCollectionConfig`, `CollectionConfig` *is* the
583
+ * authoring type — the wrapper would be an alias of it, and a second name for one thing
584
+ * is what this whole refactor has been removing.
585
+ *
586
+ * A project opts its program in with one line, once:
587
+ *
588
+ * ```ts
589
+ * /// <reference types="@rebasepro/cms-types" />
590
+ * ```
591
+ *
592
+ * after which `admin` is typed on every collection and every property. Without it,
593
+ * writing one is an error — which is the guarantee a BaaS install depends on.
594
+ */
595
+
596
+ /**
597
+ * Define a collection with the admin block type-checked.
598
+ *
599
+ * The same identity function as `defineCollection` in `@rebasepro/common` — which
600
+ * is what a BaaS or headless project uses, and where `admin` does not exist at all
601
+ * — with one difference: importing this one brings the augmentation with it, so
602
+ * `admin: { icon, listProperties, kanban }` gets completion and a typo is an
603
+ * error. See {@link AdminCollectionOptions}.
604
+ *
605
+ * Import it from the layer you are in. A project with an admin panel wants this
606
+ * one; a project without one has no `admin` block to check.
607
+ *
608
+ * `const P` captures the literal property types, which is what gives
609
+ * `admin.display`, `admin.sort` and `admin.propertiesOrder` completion over
610
+ * the collection's own property keys rather than plain `string`.
611
+ *
612
+ * @example
613
+ * export default defineCollection({
614
+ * slug: "posts",
615
+ * table: "posts",
616
+ * properties: {
617
+ * title: { name: "Title", type: "string" },
618
+ * status: { name: "Status", type: "string" }
619
+ * },
620
+ * admin: {
621
+ * icon: "FileText",
622
+ * display: { title: "title" }, // completion: "title" | "status"
623
+ * listProperties: ["title", "status"]
624
+ * }
625
+ * });
626
+ *
627
+ * @group Builder
628
+ */
629
+ export function defineCollection<
630
+ const P extends PostgresProperties,
631
+ USER extends User = User
632
+ >(
633
+ collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, "properties">
634
+ & { properties: P }
635
+ ): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };
636
+
637
+ /** Define a Firestore-backed collection with the admin block checked. @group Builder */
638
+ export function defineCollection<
639
+ const P extends FirebaseProperties,
640
+ USER extends User = User
641
+ >(
642
+ collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, "properties">
643
+ & { properties: P }
644
+ ): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };
645
+
646
+ /** Define a MongoDB-backed collection with the admin block checked. @group Builder */
647
+ export function defineCollection<
648
+ const P extends MongoProperties,
649
+ USER extends User = User
650
+ >(
651
+ collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, "properties">
652
+ & { properties: P }
653
+ ): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };
654
+
655
+ /** Identity at runtime; the overloads above are the whole point. @group Builder */
656
+ export function defineCollection(collection: CollectionConfig): CollectionConfig {
657
+ return collection;
658
+ }
659
+
660
+ /**
661
+ * Re-exported from `@rebasepro/types`, where the list has to live: the ts-morph
662
+ * schema editor in `@rebasepro/server` needs it to know which keys go inside the
663
+ * block when it rewrites a collection file, and a core package may not import
664
+ * this one. The list is plain data, so core is a fine home for it.
665
+ *
666
+ * What core *cannot* do is check the list against the type. That happens here.
667
+ *
668
+ * @group Models
669
+ */
670
+ export type { AdminCollectionKey } from "@rebasepro/types";
671
+
672
+ /**
673
+ * Core's list, re-exported through a `satisfies` clause that is the agreement
674
+ * check: a key core names that is not an option here fails to compile, and
675
+ * `satisfies` keeps the literal tuple type rather than widening it to `string[]`.
676
+ *
677
+ * The reverse direction — an option missing from core's list — has no type-level
678
+ * expression, since there is no exhaustiveness check over an optional-property
679
+ * keyof. `test/admin_collection.test.ts` counts them instead.
680
+ */
681
+ export const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];
682
+
683
+
684
+ /**
685
+ * A collection as the admin panel works with it: the contract with the `admin`
686
+ * block flattened onto the top level.
687
+ *
688
+ * The panel reads presentation fields in a few hundred places, and threading
689
+ * `collection.admin?.propertiesOrder` through all of them would be noise that
690
+ * buys nothing — the panel has already resolved the collection by then, merging
691
+ * the declared config with the user's per-collection overrides from local
692
+ * storage. So the panel gets a flat *view model*, exactly as it already does for
693
+ * entities (`Entity` is an admin view model over flat rows, not a wire type).
694
+ *
695
+ * The distinction that matters is direction:
696
+ *
697
+ * - **Reading** a resolved collection → `AdminCollection` (flat, convenient).
698
+ * - **Authoring or persisting** one → core's `CollectionConfig`, with the `admin`
699
+ * block this package augments onto it (nested, which is what the file on disk
700
+ * and the wire both look like).
701
+ *
702
+ * `admin` is kept alongside the flattened fields so the collection editor can
703
+ * still see the block it has to write back.
704
+ *
705
+ * @group Models
706
+ */
707
+ export type AdminCollection<
708
+ M extends Record<string, unknown> = Record<string, unknown>,
709
+ USER extends User = User
710
+ > = WithFlatAdmin<CollectionConfig<M, USER>, M, USER>;
711
+
712
+ /**
713
+ * Flatten the admin block onto one member of the collection union at a time.
714
+ *
715
+ * `CollectionConfig` is a union discriminated on `engine`
716
+ * (Postgres | Firestore | MongoDB), and a bare `Omit<Union, "admin">` collapses it
717
+ * into a single object type with the discriminant widened. The result stops being
718
+ * assignable back to `CollectionConfig`, so every call that hands a resolved
719
+ * collection to a core function fails — which is exactly what happened. The
720
+ * `C extends unknown` clause makes the mapping distributive, so each member keeps
721
+ * its literal `engine` and stays assignable to its counterpart.
722
+ */
723
+ type WithFlatAdmin<C, M extends Record<string, unknown>, USER extends User> =
724
+ C extends unknown
725
+ ? Omit<C, "admin"> & AdminCollectionOptions<M, USER> & { admin?: AdminCollectionOptions<M, USER> }
726
+ : never;
727
+
728
+ /** {@link AdminCollection} for a Postgres collection. @group Models */
729
+ export type AdminPostgresCollection<
730
+ M extends Record<string, unknown> = Record<string, unknown>,
731
+ USER extends User = User
732
+ > = Omit<PostgresCollectionConfig<M, USER>, "admin">
733
+ & AdminCollectionOptions<M, USER>
734
+ & { admin?: AdminCollectionOptions<M, USER> };
735
+
736
+ /**
737
+ * Flatten a collection's `admin` block onto it, producing the panel's view model.
738
+ *
739
+ * Shallow by design: the block's fields are independent, so a deep merge would
740
+ * only create opportunities for a nested object to be half from one source and
741
+ * half from the other. `admin` survives on the result.
742
+ *
743
+ * Idempotent — flattening an already-flat collection returns an equivalent one —
744
+ * because the panel resolves collections at more than one entry point (the
745
+ * registry, `<Rebase collections>`, a plugin's `modifyCollection`) and they must
746
+ * not fight over which has run.
747
+ */
748
+ export function resolveAdminCollection<
749
+ M extends Record<string, unknown> = Record<string, unknown>,
750
+ USER extends User = User
751
+ >(collection: CollectionConfig<M, USER> | AdminCollection<M, USER>): AdminCollection<M, USER> {
752
+ const block = (collection as { admin?: AdminCollectionOptions<M, USER> }).admin;
753
+ if (!block) return collection as AdminCollection<M, USER>;
754
+ return { ...(collection as AdminCollection<M, USER>), ...block, admin: block };
755
+ }
756
+
757
+ /**
758
+ * The inverse: lift flattened admin fields back into the block.
759
+ *
760
+ * Used on the way out — persisting from the collection editor, or handing a
761
+ * collection to anything that expects the authoring shape. Any key in
762
+ * {@link ADMIN_COLLECTION_KEYS} found at the top level is moved down, so a
763
+ * round trip through the panel does not leave the file flat.
764
+ *
765
+ * The nesting itself lives in `@rebasepro/types` because the schema editor in
766
+ * `@rebasepro/server` — which cannot import this package — has to do exactly the
767
+ * same thing when it writes a collection file back to disk. Two copies of the
768
+ * rule disagreed on which side wins, and the disagreement was invisible.
769
+ */
770
+ export function toAdminCollectionConfig<
771
+ M extends Record<string, unknown> = Record<string, unknown>,
772
+ USER extends User = User
773
+ >(collection: AdminCollection<M, USER> | CollectionConfig<M, USER>): CollectionConfig<M, USER> {
774
+ return nestAdminCollectionKeys(collection as Record<string, unknown>) as unknown as CollectionConfig<M, USER>;
775
+ }