@rebasepro/admin-types 0.10.1-canary.3d09338 → 0.10.1-canary.60301c3

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.
@@ -21,6 +21,48 @@ import type { EntityCustomView, FormViewConfig } from "./types/entity_views";
21
21
  import type { EntityAction } from "./types/entity_actions";
22
22
  import type { ExportConfig } from "./types/export_import";
23
23
  import type { CollectionComponentOverrideMap } from "./types/component_overrides";
24
+ /**
25
+ * A key naming one of `M`'s fields, or a dotted path into a `map` field.
26
+ *
27
+ * Both forms are resolved with `getValueInPath`, so `"profile.displayName"` is
28
+ * as valid as `"title"`. Only the *root* is checked — the path below it is a
29
+ * nested `Properties` object this type has no view of — which is enough to
30
+ * reject the mistake that actually happens: a misspelled or removed field.
31
+ *
32
+ * When `M` is the default `Record<string, unknown>` — the plain
33
+ * `const x: PostgresCollectionConfig = { … }` annotation, which infers nothing —
34
+ * `Extract<keyof M, string>` is `string` and this accepts anything, exactly as
35
+ * before. `defineCollection` is what supplies a real `M` and turns the check on.
36
+ */
37
+ export type PropertyPath<M> = Extract<keyof M, string> | `${Extract<keyof M, string>}.${string}`;
38
+ /**
39
+ * A key naming a *column* in the list view: a property path, a child-collection
40
+ * column, or the `key` of one of this collection's `additionalFields`.
41
+ *
42
+ * `AdditionalFieldDelegate.key` is a plain `string`, and the block is not
43
+ * generic over its own `additionalFields`, so there is no type-level channel
44
+ * carrying those keys here. Accepting any string to cover them is what made this
45
+ * field unchecked in the first place; instead the two provable arms are closed
46
+ * and {@link AdditionalFieldKey} is the explicit, castable escape.
47
+ */
48
+ export type ColumnKey<M> = PropertyPath<M> | `subcollection:${string}` | AdditionalFieldKey;
49
+ /**
50
+ * Opt-out for a `propertiesOrder` / `listProperties` entry that names an
51
+ * `additionalFields` key rather than a property.
52
+ *
53
+ * The brand is **required**, which is the entire mechanism: a bare `"score"` is
54
+ * not assignable, so the entry has to be written `"score" as AdditionalFieldKey`
55
+ * — a visible admission that this key is not a property. An optional brand
56
+ * (`__additionalFieldKey?: never`) would be satisfied by every string and put us
57
+ * straight back to accepting typos.
58
+ *
59
+ * ```ts
60
+ * propertiesOrder: ["title", "score" as AdditionalFieldKey]
61
+ * ```
62
+ */
63
+ export type AdditionalFieldKey = string & {
64
+ readonly __additionalFieldKey: true;
65
+ };
24
66
  /**
25
67
  * Admin-panel presentation and behaviour for a collection.
26
68
  *
@@ -62,18 +104,18 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
62
104
  /**
63
105
  * Default preview properties displayed when this collection is referenced to.
64
106
  */
65
- previewProperties?: string[];
107
+ previewProperties?: Extract<keyof M, string>[];
66
108
  /**
67
109
  * Properties to display as columns in the list view.
68
110
  * If not specified, the list view uses a smart default (Title, Status, Date).
69
111
  */
70
- listProperties?: string[];
112
+ listProperties?: ColumnKey<M>[];
71
113
  /**
72
114
  * Title property of the entity. This is the property that will be used
73
115
  * as the title in entity related views and references.
74
116
  * If not specified, the first property simple text property will be used.
75
117
  */
76
- readonly titleProperty?: Extract<keyof M, string> | (string & {});
118
+ readonly titleProperty?: PropertyPath<M>;
77
119
  /**
78
120
  * When editing a entity, you can choose to open the entity in a side dialog
79
121
  * or in a full screen dialog. Defaults to `full_screen`.
@@ -116,7 +158,7 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
116
158
  * with `many` cardinality) each get a column with id
117
159
  * `subcollection:<slug>`, e.g. `subcollection:orders`.
118
160
  */
119
- propertiesOrder?: (Extract<keyof M, string> | (string & {}) | string | `subcollection:${string}`)[];
161
+ propertiesOrder?: ColumnKey<M>[];
120
162
  /**
121
163
  * If enabled, content is loaded in batches. If `false` all entities in the
122
164
  * collection are loaded. This means that when reaching the end of the
@@ -138,14 +180,14 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
138
180
  * e.g. `fixedFilter: { age: [">", 18] }`
139
181
  * e.g. `fixedFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
140
182
  */
141
- readonly fixedFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;
183
+ readonly fixedFilter?: FilterValues<PropertyPath<M>>;
142
184
  /**
143
185
  * Initial filters applied to the collection this collection is related to.
144
186
  * Defaults to none. Filters applied with this prop can be changed.
145
187
  * e.g. `defaultFilter: { age: [">", 18] }`
146
188
  * e.g. `defaultFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
147
189
  */
148
- readonly defaultFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;
190
+ readonly defaultFilter?: FilterValues<PropertyPath<M>>;
149
191
  /**
150
192
  * Pre-defined filter presets that appear as quick-access options in the
151
193
  * collection toolbar. Each preset applies a set of filters (and
@@ -163,14 +205,14 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
163
205
  * ]
164
206
  * ```
165
207
  */
166
- readonly filterPresets?: FilterPreset<Extract<keyof M, string> | (string & {})>[];
208
+ readonly filterPresets?: FilterPreset<PropertyPath<M>>[];
167
209
  /**
168
210
  * Default sort applied to this collection.
169
211
  * When setting this prop, entities will have a default order
170
212
  * applied in the collection.
171
213
  * e.g. `sort: ["order", "asc"]`
172
214
  */
173
- readonly sort?: OrderByTuple<Extract<keyof M, string> | (string & {})>;
215
+ readonly sort?: OrderByTuple<PropertyPath<M>>;
174
216
  /**
175
217
  * You can add additional fields to the collection view by implementing
176
218
  * an additional field delegate.
@@ -275,7 +317,7 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
275
317
  * Used by Kanban view for ordering within columns
276
318
  * and can be used for general ordering purposes.
277
319
  */
278
- readonly orderProperty?: Extract<keyof M, string> | (string & {});
320
+ readonly orderProperty?: Extract<keyof M, string>;
279
321
  /**
280
322
  * Actions that can be performed on the entities in this collection.
281
323
  */
package/dist/augment.d.ts CHANGED
@@ -25,10 +25,25 @@
25
25
  */
26
26
  import type { AdminArrayOptions, AdminDateOptions, AdminMapOptions, AdminNumberOptions, AdminPropertyOptions, AdminReferenceOptions, AdminRelationOptions, AdminStringOptions, AdminVectorOptions } from "./types/property_options";
27
27
  import type { AdminCollectionOptions } from "./admin_collection";
28
+ import type { User } from "@rebasepro/types";
28
29
  declare module "@rebasepro/types" {
29
- /** Presentation and behaviour for a collection in the admin panel. */
30
- interface BaseCollectionConfig {
31
- admin?: AdminCollectionOptions;
30
+ /**
31
+ * Presentation and behaviour for a collection in the admin panel.
32
+ *
33
+ * The type parameters are repeated here verbatim because declaration
34
+ * merging requires an identical parameter list — and they have to be
35
+ * *forwarded* to `AdminCollectionOptions`, which is the part that was
36
+ * missing. Written as a bare `admin?: AdminCollectionOptions`, `M` fell
37
+ * back to its default `Record<string, unknown>`, so `Extract<keyof M,
38
+ * string>` widened to `string` and every key-shaped field in the block —
39
+ * `titleProperty`, `sort`, `propertiesOrder`, `listProperties` — silently
40
+ * accepted any string. The completion `defineCollection` advertises is
41
+ * derived from `M`, so it never appeared: the inference was computed,
42
+ * carried to this seam, and dropped one line short of the field that
43
+ * needed it.
44
+ */
45
+ interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
46
+ admin?: AdminCollectionOptions<M, USER>;
32
47
  }
33
48
  interface BaseProperty<CustomProps = unknown> {
34
49
  admin?: AdminPropertyOptions<CustomProps>;
@@ -15,7 +15,7 @@
15
15
  import React from "react";
16
16
  import type { Entity, EntityStatus, FilterValues, User } from "@rebasepro/types";
17
17
  import type { RebaseContext } from "./rebase_context";
18
- import type { AdminCollection } from "@rebasepro/admin-types";
18
+ import type { AdminCollection, PropertyPath } from "@rebasepro/admin-types";
19
19
  /**
20
20
  * Configuration for Kanban board view mode.
21
21
  * @group Collections
@@ -26,6 +26,21 @@ export interface KanbanConfig<M extends Record<string, unknown> = Record<string,
26
26
  * Must reference a string property with `enum` values defined.
27
27
  * Entities will be grouped into columns based on this property's value.
28
28
  * The column order is determined by the order of `enum` values in the property.
29
+ *
30
+ * Left permissive on purpose, unlike the *optional* key fields on the admin
31
+ * block (`titleProperty`, `sort`, `propertiesOrder`, …), which are checked
32
+ * against `M`.
33
+ *
34
+ * This one is **required**, and that is the whole difference: a required
35
+ * generic-dependent property puts `Extract<keyof M, string>` in an invariant
36
+ * position, so `AdminCollection<M>` stops being assignable to
37
+ * `AdminCollection`. The admin package passes collections between those two
38
+ * forms in roughly fifteen places — `CollectionBoardViewBinding`, the
39
+ * collection editor, the view bindings — and every one of them breaks.
40
+ * Tightening this is a worthwhile change, but it is a refactor of those call
41
+ * sites rather than a type edit. Verified by trying it: `columnProperty:
42
+ * Extract<keyof M, string>` alone produced ~15 errors, and doing it alongside
43
+ * `entityViews`/`formView`/`Actions` produced 95.
29
44
  */
30
45
  columnProperty: Extract<keyof M, string> | (string & {});
31
46
  }
@@ -159,8 +174,15 @@ export interface AdditionalFieldDelegate<M extends Record<string, unknown> = Rec
159
174
  * e.g. ["name", "surname"]
160
175
  * This is a performance optimization, if you don't define dependencies
161
176
  * it will be updated in every render.
162
- */
163
- dependencies?: NoInfer<Extract<keyof M, string>> | NoInfer<Extract<keyof M, string>>[] | (string & {}) | (string & {})[];
177
+ *
178
+ * A key that is not a property of this collection can never change, so
179
+ * listing one is always a mistake — it silently pins the column to the
180
+ * "never re-render" path, which reads as a stale cell rather than a typo.
181
+ * The `NoInfer` wrappers keep these keys from participating in inference of
182
+ * `M`; the `(string & {})` arms they used to sit beside made the whole union
183
+ * `string`, so the wrappers had nothing to protect.
184
+ */
185
+ dependencies?: NoInfer<PropertyPath<M>> | NoInfer<PropertyPath<M>>[];
164
186
  /**
165
187
  * Use this prop to define the value of the column as a string or number.
166
188
  * This is the value that will be used for exporting the collection.
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../src/react_component_ref.ts","../src/admin_collection.ts"],"sourcesContent":["import type React from \"react\";\nimport type { ComponentLike, ComponentRef, LazyComponentRef } from \"@rebasepro/types\";\n\n/**\n * `ComponentRef`, narrowed to real React types.\n *\n * Core's {@link ComponentRef} describes a component structurally\n * ({@link ComponentLike}) so that `properties.ts` — and therefore the whole\n * property model the backend reads — can live without React. The trade is that\n * the return type is `unknown`, so a function returning something React cannot\n * render type-checks there.\n *\n * Use this type wherever React genuinely exists: authoring a collection's admin\n * options, and inside the admin packages. Assignments flow into core unchanged,\n * because every member of this union is a member of that one.\n */\nexport type ReactComponentRef<P = any> =\n | string\n | LazyComponentRef<P>\n | (() => Promise<{ default: React.ComponentType<P> }>)\n | React.ComponentType<P>;\n\n/**\n * The `ComponentLike` contract, as a signature the compiler has to keep true.\n *\n * The split rests on one claim: **every form a React component takes is\n * assignable to `ComponentLike`** — function components, class components,\n * `memo`, `forwardRef`. If that stopped holding, core's `ComponentRef` would\n * quietly begin rejecting real components, and the failure would surface far away\n * in whichever collection file happened to use the broken form.\n *\n * So the claim is not left to a test that someone has to run. This function's\n * parameter and return types state it, and `pnpm typecheck` enforces it on every\n * commit. It is also useful on its own: an explicit widening at the point where\n * an authored component enters a collection config.\n *\n * @example\n * import { MyField } from \"./MyField\";\n * admin: { Field: asComponentRef(MyField) }\n */\nexport function asComponentRef<P>(component: React.ComponentType<P>): ComponentRef<P> {\n return component;\n}\n\n/**\n * The same contract in the other direction: a `ComponentLike` is only renderable\n * once narrowed, and this is the single sanctioned place that narrowing is\n * spelled out. `resolveComponentRef` in `@rebasepro/app` does the runtime half.\n */\nexport function asReactComponent<P>(component: ComponentLike<P>): React.ComponentType<P> {\n return component as React.ComponentType<P>;\n}\n","/**\n * The typed admin block, and the type you author a collection against.\n *\n * A collection is one file. Schema, security rules and callbacks sit at the top\n * level, where the backend reads them; everything the admin panel renders sits\n * under `admin`. `@rebasepro/types` does not declare that field at all — naming a\n * kanban column definition would drag `React.ReactNode` back into the BaaS\n * contract, and a server has no use for one. `augment.ts` in this package declares\n * it, by declaration merging, onto core's `CollectionConfig`. So this is the other\n * side of that boundary: the 38 fields, fully typed, in the package where React\n * exists, and reachable only by a program that has opted in.\n *\n * Each field is declared exactly once, here. Core does not carry a React-free\n * skeleton of the same shape; two definitions that agree only by luck is the\n * `WhereFilterOp` mistake, and this block is far bigger than one union.\n */\nimport type React from \"react\";\nimport type {\n CollectionConfig,\n ComponentRef,\n FilterPreset,\n FilterValues,\n FirebaseCollectionConfig,\n FirebaseProperties,\n InferEntityType,\n MongoDBCollectionConfig,\n MongoProperties,\n OrderByTuple,\n PostgresCollectionConfig,\n PostgresProperties,\n User\n} from \"@rebasepro/types\";\n// A value, not a type: the runtime list core owns.\nimport { ADMIN_COLLECTION_KEYS as CORE_ADMIN_COLLECTION_KEYS } from \"@rebasepro/types\";\n\nimport type {\n AdditionalFieldDelegate,\n CollectionActionsProps,\n CollectionSize,\n DefaultSelectedViewBuilder,\n KanbanConfig,\n SelectionController,\n ViewMode\n} from \"./collections\";\nimport type { EntityCustomView, FormViewConfig } from \"./types/entity_views\";\nimport type { EntityAction } from \"./types/entity_actions\";\nimport type { ExportConfig } from \"./types/export_import\";\nimport type { CollectionComponentOverrideMap } from \"./types/component_overrides\";\n\n/**\n * Admin-panel presentation and behaviour for a collection.\n *\n * A `type` rather than an `interface`, and that is load-bearing: TypeScript gives\n * an implicit index signature to an object *type alias* but not to an interface.\n * `toAdminCollectionConfig` has to widen a collection carrying this block to\n * `Record<string, unknown>` in order to move the flattened keys back under\n * `admin`, and as an interface that conversion is an error (TS2352, \"index\n * signature for type 'string' is missing\"). Flipping it and running\n * `pnpm typecheck` reproduces that in one line.\n *\n * Declaration merging is not wanted here anyway; a plugin adding fields to the\n * block would have nothing reading them.\n *\n * @group Models\n */\nexport type AdminCollectionOptions<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n /**\n * Icon for the navigation sidebar or cards.\n *\n * Either a Lucide icon name (`\"FileText\"`, `\"ShoppingCart\"`) or a rendered\n * element. Prefer the name: it survives serialization, so the collection file\n * stays loadable by the backend and by `rebase generate-sdk`, and it is what\n * the schema editor writes back.\n */\n icon?: string | React.ReactNode;\n\n /**\n * Navigation group for this collection.\n * Collections sharing the same group name will be visually grouped\n * together in the drawer and home page. If not set, the collection\n * falls into the default \"Views\" group.\n */\n group?: string;\n\n /**\n * Array of entity views that this collection has.\n * Can be an array of `EntityCustomView` or a string representing the key of a global `EntityCustomView`.\n */\n entityViews?: (string | EntityCustomView<Record<string, unknown>>)[];\n\n /**\n * Default preview properties displayed when this collection is referenced to.\n */\n previewProperties?: string[];\n\n /**\n * Properties to display as columns in the list view.\n * If not specified, the list view uses a smart default (Title, Status, Date).\n */\n listProperties?: string[];\n\n /**\n * Title property of the entity. This is the property that will be used\n * as the title in entity related views and references.\n * If not specified, the first property simple text property will be used.\n */\n readonly titleProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * When editing a entity, you can choose to open the entity in a side dialog\n * or in a full screen dialog. Defaults to `full_screen`.\n */\n openEntityMode?: \"side_panel\" | \"full_screen\" | \"split\" | \"dialog\";\n\n /**\n * Controls what happens when a user clicks on a entity in the collection view.\n * - `\"edit\"` (default): Opens the entity in the edit form.\n * - `\"view\"`: Opens a read-only detail view with an \"Edit\" button.\n */\n defaultEntityAction?: \"view\" | \"edit\";\n\n /**\n * Replace the default entity form with a custom component.\n * The Builder receives the same props as entity view tabs\n * (entity, formContext, collection, etc.) and has full control over the UI.\n *\n * Works in both edit mode and read-only mode (when `defaultEntityAction`\n * is `\"view\"`). In read-only mode, `formContext.readOnly` will be `true`.\n */\n formView?: FormViewConfig;\n\n /**\n * Prevent default actions from being displayed or executed on this collection.\n */\n disableDefaultActions?: (\"edit\" | \"copy\" | \"delete\")[];\n\n /**\n * Order in which the properties are displayed.\n * If you are specifying your collection as code, the order is the same as the\n * one you define in `properties`. Additional columns are added at the\n * end of the list, if the order is not specified.\n *\n * You can use this prop to hide some properties from the table view.\n * Note that if you set this prop, other ways to hide fields, like\n * `hidden` in the property definition, will be ignored.\n * `propertiesOrder` has precedence over `hidden`.\n *\n * Supported entry formats:\n * - For properties, use the property key.\n * - For additional fields, use the field key.\n * - Child collections (Firestore subcollections, or Postgres relations\n * with `many` cardinality) each get a column with id\n * `subcollection:<slug>`, e.g. `subcollection:orders`.\n */\n propertiesOrder?: (Extract<keyof M, string> | (string & {}) | string | `subcollection:${string}`)[];\n\n /**\n * If enabled, content is loaded in batches. If `false` all entities in the\n * collection are loaded. This means that when reaching the end of the\n * collection, the CMS will load more entities.\n * You can specify a number to specify the pagination size (50 by default)\n * Defaults to `true`\n */\n pagination?: boolean | number;\n\n selectionEnabled?: boolean;\n\n /**\n * Pass your own selection controller if you want to control selected\n * entities externally.\n * @see useSelectionController\n */\n selectionController?: SelectionController<M>;\n\n /**\n * Force a filter in this view. If applied, the rest of the filters will\n * be disabled. Filters applied with this prop cannot be changed.\n * e.g. `fixedFilter: { age: [\">\", 18] }`\n * e.g. `fixedFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly fixedFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;\n\n /**\n * Initial filters applied to the collection this collection is related to.\n * Defaults to none. Filters applied with this prop can be changed.\n * e.g. `defaultFilter: { age: [\">\", 18] }`\n * e.g. `defaultFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly defaultFilter?: FilterValues<Extract<keyof M, string> | (string & {})>; // setting FilterValues<M> can break defining collections by code\n\n /**\n * Pre-defined filter presets that appear as quick-access options in the\n * collection toolbar. Each preset applies a set of filters (and\n * optionally a sort order) with a single click.\n *\n * ```ts\n * filterPresets: [\n * {\n * label: \"Shipped this month\",\n * filterValues: {\n * status: [\"==\", \"shipped\"],\n * order_date: [\">=\", new Date(Date.now() - 30 * 86400000)]\n * }\n * }\n * ]\n * ```\n */\n readonly filterPresets?: FilterPreset<Extract<keyof M, string> | (string & {})>[];\n\n /**\n * Default sort applied to this collection.\n * When setting this prop, entities will have a default order\n * applied in the collection.\n * e.g. `sort: [\"order\", \"asc\"]`\n */\n readonly sort?: OrderByTuple<Extract<keyof M, string> | (string & {})>;\n\n /**\n * You can add additional fields to the collection view by implementing\n * an additional field delegate.\n */\n readonly additionalFields?: AdditionalFieldDelegate<M, USER>[];\n\n /**\n * Default size of the rendered collection\n */\n defaultSize?: CollectionSize;\n\n /**\n * Can the elements in this collection be edited inline in the collection\n * view. Even when inline editing is disabled, entities can still be\n * edited in the side panel (subject to `securityRules`).\n */\n inlineEditing?: boolean;\n\n /**\n * Should this collection be hidden from the main navigation panel, if\n * it is at the root level, or in the entity side panel if it's a\n * subcollection.\n * It will still be accessible if you reach the specified path.\n * You can also use this collection as a reference target.\n */\n hideFromNavigation?: boolean;\n\n /**\n * If you want to open custom views or subcollections by default when opening the edit\n * view of a entity, you can specify the path to the view here.\n * The path is relative to the current collection. For example if you have a collection\n * that has a custom view as well as a subcollection that refers to another entity, you can\n * either specify the path to the custom view or the path to the subcollection.\n */\n defaultSelectedView?: string | DefaultSelectedViewBuilder;\n\n /**\n * Should the ID of this collection be hidden from the form view.\n */\n hideIdFromForm?: boolean;\n\n /**\n * Should the ID of this collection be hidden from the grid view.\n */\n hideIdFromCollection?: boolean;\n\n /**\n * If set to true, the form will be auto-saved when the user changes\n * the value of a field.\n * Defaults to false.\n * When a new entity is created, this property can be updated to generated a new ID\n */\n formAutoSave?: boolean;\n\n /**\n *\n */\n exportable?: boolean | ExportConfig<USER>;\n\n /**\n * Width of the side dialog (in pixels) when opening a entity in this collection.\n */\n sideDialogWidth?: number | string;\n\n /**\n * If set to true, the default values of the properties will be applied\n * to the entity every time the entity is updated (not only when created).\n * Defaults to false.\n */\n alwaysApplyDefaultValues?: boolean;\n\n /**\n * If set to true, a tab including the JSON representation of the entity will be included.\n */\n includeJsonView?: boolean;\n\n /**\n * Should local changes be backed up in local storage, to prevent data loss on\n * accidental navigations.\n * - `manual_apply`: When the user navigates back to a entity with local changes,\n * they will be prompted to restore the changes.\n * - `auto_apply`: When the user navigates back to a entity with local changes,\n * the changes will be automatically applied.\n * - `false`: Local changes will not be backed up.\n * Defaults to `manual_apply`.\n */\n localChangesBackup?: \"manual_apply\" | \"auto_apply\" | false;\n\n /**\n * Default view mode for displaying this collection.\n * - \"table\": Display entities in a table with inline editing (default)\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * Defaults to \"table\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"table\", \"cards\", \"kanban\".\n * Defaults to all three: [\"table\", \"cards\", \"kanban\"].\n * Note: \"kanban\" will only be available if the collection has at least\n * one string property with `enum` defined, regardless of this setting.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n * Must reference a string/text property. When items are reordered,\n * this property will be updated with lexicographic sort keys\n * (e.g. \"a0\", \"a1\", \"a0V\") using string-based fractional indexing.\n * Used by Kanban view for ordering within columns\n * and can be used for general ordering purposes.\n */\n readonly orderProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * Actions that can be performed on the entities in this collection.\n */\n entityActions?: EntityAction<M, USER>[];\n\n /**\n * Builder for the collection actions rendered in the toolbar\n */\n Actions?: ComponentRef<CollectionActionsProps>[];\n\n /**\n * Collection-scoped component overrides. These take precedence over\n * global overrides set on `<Rebase>`, but only within this collection's\n * views (entity form, detail view, table, empty state, etc.).\n *\n * Only collection-scoped components (like `Entity.Form`, `Collection.EmptyState`,\n * `Collection.Card`, etc.) can be overridden here. App-level components\n * (like `Shell.AppBar`, `HomePage`) can only be overridden at the `<Rebase>` level.\n *\n * @example\n * ```tsx\n * const productsCollection: PostgresCollectionConfig = {\n * name: \"Products\",\n * slug: \"products\",\n * table: \"products\",\n * components: {\n * \"Entity.Form\": { Component: ProductCustomForm },\n * \"Collection.Card\": { Component: ProductCard },\n * },\n * properties: { ... }\n * };\n * ```\n */\n components?: CollectionComponentOverrideMap;};\n\n/**\n * There is deliberately no `AdminCollectionConfig` here any more.\n *\n * It used to be `Omit<CollectionConfig, \"admin\"> & { admin?: AdminCollectionOptions }`,\n * a wrapper that existed because core typed the block opaquely. Now that `augment.ts`\n * declares `admin` directly on `BaseCollectionConfig`, `CollectionConfig` *is* the\n * authoring type — the wrapper would be an alias of it, and a second name for one thing\n * is what this whole refactor has been removing.\n *\n * A project opts its program in with one line, once:\n *\n * ```ts\n * /// <reference types=\"@rebasepro/admin-types\" />\n * ```\n *\n * after which `admin` is typed on every collection and every property. Without it,\n * writing one is an error — which is the guarantee a BaaS install depends on.\n */\n\n/**\n * Define a collection with the admin block type-checked.\n *\n * The same identity function as `defineCollection` in `@rebasepro/common` — which\n * is what a BaaS or headless project uses, and where `admin` does not exist at all\n * — with one difference: importing this one brings the augmentation with it, so\n * `admin: { icon, listProperties, kanban }` gets completion and a typo is an\n * error. See {@link AdminCollectionOptions}.\n *\n * Import it from the layer you are in. A project with an admin panel wants this\n * one; a project without one has no `admin` block to check.\n *\n * `const P` captures the literal property types, which is what gives\n * `admin.titleProperty`, `admin.sort` and `admin.propertiesOrder` completion over\n * the collection's own property keys rather than plain `string`.\n *\n * @example\n * export default defineCollection({\n * slug: \"posts\",\n * table: \"posts\",\n * properties: {\n * title: { name: \"Title\", type: \"string\" },\n * status: { name: \"Status\", type: \"string\" }\n * },\n * admin: {\n * icon: \"FileText\",\n * titleProperty: \"title\", // completion: \"title\" | \"status\"\n * listProperties: [\"title\", \"status\"]\n * }\n * });\n *\n * @group Builder\n */\nexport function defineCollection<\n const P extends PostgresProperties,\n USER extends User = User\n>(\n collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a Firestore-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends FirebaseProperties,\n USER extends User = User\n>(\n collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a MongoDB-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends MongoProperties,\n USER extends User = User\n>(\n collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Identity at runtime; the overloads above are the whole point. @group Builder */\nexport function defineCollection(collection: CollectionConfig): CollectionConfig {\n return collection;\n}\n\n/**\n * Re-exported from `@rebasepro/types`, where the list has to live: the ts-morph\n * schema editor in `@rebasepro/server` needs it to know which keys go inside the\n * block when it rewrites a collection file, and a core package may not import\n * this one. The list is plain data, so core is a fine home for it.\n *\n * What core *cannot* do is check the list against the type. That happens here.\n *\n * @group Models\n */\nexport type { AdminCollectionKey } from \"@rebasepro/types\";\n\n/**\n * Core's list, re-exported through a `satisfies` clause that is the agreement\n * check: a key core names that is not an option here fails to compile, and\n * `satisfies` keeps the literal tuple type rather than widening it to `string[]`.\n *\n * The reverse direction — an option missing from core's list — has no type-level\n * expression, since there is no exhaustiveness check over an optional-property\n * keyof. `test/admin_collection.test.ts` counts them instead.\n */\nexport const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];\n\n\n/**\n * A collection as the admin panel works with it: the contract with the `admin`\n * block flattened onto the top level.\n *\n * The panel reads presentation fields in a few hundred places, and threading\n * `collection.admin?.propertiesOrder` through all of them would be noise that\n * buys nothing — the panel has already resolved the collection by then, merging\n * the declared config with the user's per-collection overrides from local\n * storage. So the panel gets a flat *view model*, exactly as it already does for\n * entities (`Entity` is an admin view model over flat rows, not a wire type).\n *\n * The distinction that matters is direction:\n *\n * - **Reading** a resolved collection → `AdminCollection` (flat, convenient).\n * - **Authoring or persisting** one → core's `CollectionConfig`, with the `admin`\n * block this package augments onto it (nested, which is what the file on disk\n * and the wire both look like).\n *\n * `admin` is kept alongside the flattened fields so the collection editor can\n * still see the block it has to write back.\n *\n * @group Models\n */\nexport type AdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = WithFlatAdmin<CollectionConfig<M, USER>, M, USER>;\n\n/**\n * Flatten the admin block onto one member of the collection union at a time.\n *\n * `CollectionConfig` is a union discriminated on `engine`\n * (Postgres | Firestore | MongoDB), and a bare `Omit<Union, \"admin\">` collapses it\n * into a single object type with the discriminant widened. The result stops being\n * assignable back to `CollectionConfig`, so every call that hands a resolved\n * collection to a core function fails — which is exactly what happened. The\n * `C extends unknown` clause makes the mapping distributive, so each member keeps\n * its literal `engine` and stays assignable to its counterpart.\n */\ntype WithFlatAdmin<C, M extends Record<string, unknown>, USER extends User> =\n C extends unknown\n ? Omit<C, \"admin\"> & AdminCollectionOptions<M, USER> & { admin?: AdminCollectionOptions<M, USER> }\n : never;\n\n/** {@link AdminCollection} for a Postgres collection. @group Models */\nexport type AdminPostgresCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = Omit<PostgresCollectionConfig<M, USER>, \"admin\">\n & AdminCollectionOptions<M, USER>\n & { admin?: AdminCollectionOptions<M, USER> };\n\n/**\n * Flatten a collection's `admin` block onto it, producing the panel's view model.\n *\n * Shallow by design: the block's fields are independent, so a deep merge would\n * only create opportunities for a nested object to be half from one source and\n * half from the other. `admin` survives on the result.\n *\n * Idempotent — flattening an already-flat collection returns an equivalent one —\n * because the panel resolves collections at more than one entry point (the\n * registry, `<Rebase collections>`, a plugin's `modifyCollection`) and they must\n * not fight over which has run.\n */\nexport function resolveAdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n>(collection: CollectionConfig<M, USER> | AdminCollection<M, USER>): AdminCollection<M, USER> {\n const block = (collection as { admin?: AdminCollectionOptions<M, USER> }).admin;\n if (!block) return collection as AdminCollection<M, USER>;\n return { ...(collection as AdminCollection<M, USER>), ...block, admin: block };\n}\n\n/**\n * The inverse: lift flattened admin fields back into the block.\n *\n * Used on the way out — persisting from the collection editor, or handing a\n * collection to anything that expects the authoring shape. Any key in\n * {@link ADMIN_COLLECTION_KEYS} found at the top level is moved down, so a\n * round trip through the panel does not leave the file flat.\n */\nexport function toAdminCollectionConfig<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n>(collection: AdminCollection<M, USER> | CollectionConfig<M, USER>): CollectionConfig<M, USER> {\n const source = collection as Record<string, unknown>;\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...(source.admin as object ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if ((ADMIN_COLLECTION_KEYS as readonly string[]).includes(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top as unknown as CollectionConfig<M, USER>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,eAAkB,WAAoD;CAClF,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAoB,WAAqD;CACrF,OAAO;AACX;;;;ACqZA,SAAgB,iBAAiB,YAAgD;CAC7E,OAAO;AACX;;;;;;;;;;AAuBA,IAAa,wBAAwB;;;;;;;;;;;;;AAmErC,SAAgB,uBAGd,YAA4F;CAC1F,MAAM,QAAS,WAA2D;CAC1E,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EAAE,GAAI;EAAyC,GAAG;EAAO,OAAO;CAAM;AACjF;;;;;;;;;AAUA,SAAgB,wBAGd,YAA6F;CAC3F,MAAM,SAAS;CACf,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAI,OAAO,SAAmB,CAAC,EAAG;CAE3E,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAK,sBAA4C,SAAS,GAAG,GAAG,MAAM,OAAO;OACxE,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX"}
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../src/react_component_ref.ts","../src/admin_collection.ts"],"sourcesContent":["import type React from \"react\";\nimport type { ComponentLike, ComponentRef, LazyComponentRef } from \"@rebasepro/types\";\n\n/**\n * `ComponentRef`, narrowed to real React types.\n *\n * Core's {@link ComponentRef} describes a component structurally\n * ({@link ComponentLike}) so that `properties.ts` — and therefore the whole\n * property model the backend reads — can live without React. The trade is that\n * the return type is `unknown`, so a function returning something React cannot\n * render type-checks there.\n *\n * Use this type wherever React genuinely exists: authoring a collection's admin\n * options, and inside the admin packages. Assignments flow into core unchanged,\n * because every member of this union is a member of that one.\n */\nexport type ReactComponentRef<P = any> =\n | string\n | LazyComponentRef<P>\n | (() => Promise<{ default: React.ComponentType<P> }>)\n | React.ComponentType<P>;\n\n/**\n * The `ComponentLike` contract, as a signature the compiler has to keep true.\n *\n * The split rests on one claim: **every form a React component takes is\n * assignable to `ComponentLike`** — function components, class components,\n * `memo`, `forwardRef`. If that stopped holding, core's `ComponentRef` would\n * quietly begin rejecting real components, and the failure would surface far away\n * in whichever collection file happened to use the broken form.\n *\n * So the claim is not left to a test that someone has to run. This function's\n * parameter and return types state it, and `pnpm typecheck` enforces it on every\n * commit. It is also useful on its own: an explicit widening at the point where\n * an authored component enters a collection config.\n *\n * @example\n * import { MyField } from \"./MyField\";\n * admin: { Field: asComponentRef(MyField) }\n */\nexport function asComponentRef<P>(component: React.ComponentType<P>): ComponentRef<P> {\n return component;\n}\n\n/**\n * The same contract in the other direction: a `ComponentLike` is only renderable\n * once narrowed, and this is the single sanctioned place that narrowing is\n * spelled out. `resolveComponentRef` in `@rebasepro/app` does the runtime half.\n */\nexport function asReactComponent<P>(component: ComponentLike<P>): React.ComponentType<P> {\n return component as React.ComponentType<P>;\n}\n","/**\n * The typed admin block, and the type you author a collection against.\n *\n * A collection is one file. Schema, security rules and callbacks sit at the top\n * level, where the backend reads them; everything the admin panel renders sits\n * under `admin`. `@rebasepro/types` does not declare that field at all — naming a\n * kanban column definition would drag `React.ReactNode` back into the BaaS\n * contract, and a server has no use for one. `augment.ts` in this package declares\n * it, by declaration merging, onto core's `CollectionConfig`. So this is the other\n * side of that boundary: the 38 fields, fully typed, in the package where React\n * exists, and reachable only by a program that has opted in.\n *\n * Each field is declared exactly once, here. Core does not carry a React-free\n * skeleton of the same shape; two definitions that agree only by luck is the\n * `WhereFilterOp` mistake, and this block is far bigger than one union.\n */\nimport type React from \"react\";\nimport type {\n CollectionConfig,\n ComponentRef,\n FilterPreset,\n FilterValues,\n FirebaseCollectionConfig,\n FirebaseProperties,\n InferEntityType,\n MongoDBCollectionConfig,\n MongoProperties,\n OrderByTuple,\n PostgresCollectionConfig,\n PostgresProperties,\n User\n} from \"@rebasepro/types\";\n// A value, not a type: the runtime list core owns.\nimport { ADMIN_COLLECTION_KEYS as CORE_ADMIN_COLLECTION_KEYS } from \"@rebasepro/types\";\n\nimport type {\n AdditionalFieldDelegate,\n CollectionActionsProps,\n CollectionSize,\n DefaultSelectedViewBuilder,\n KanbanConfig,\n SelectionController,\n ViewMode\n} from \"./collections\";\nimport type { EntityCustomView, FormViewConfig } from \"./types/entity_views\";\nimport type { EntityAction } from \"./types/entity_actions\";\nimport type { ExportConfig } from \"./types/export_import\";\nimport type { CollectionComponentOverrideMap } from \"./types/component_overrides\";\n\n/**\n * A key naming one of `M`'s fields, or a dotted path into a `map` field.\n *\n * Both forms are resolved with `getValueInPath`, so `\"profile.displayName\"` is\n * as valid as `\"title\"`. Only the *root* is checked — the path below it is a\n * nested `Properties` object this type has no view of — which is enough to\n * reject the mistake that actually happens: a misspelled or removed field.\n *\n * When `M` is the default `Record<string, unknown>` — the plain\n * `const x: PostgresCollectionConfig = { … }` annotation, which infers nothing —\n * `Extract<keyof M, string>` is `string` and this accepts anything, exactly as\n * before. `defineCollection` is what supplies a real `M` and turns the check on.\n */\nexport type PropertyPath<M> =\n | Extract<keyof M, string>\n | `${Extract<keyof M, string>}.${string}`;\n\n/**\n * A key naming a *column* in the list view: a property path, a child-collection\n * column, or the `key` of one of this collection's `additionalFields`.\n *\n * `AdditionalFieldDelegate.key` is a plain `string`, and the block is not\n * generic over its own `additionalFields`, so there is no type-level channel\n * carrying those keys here. Accepting any string to cover them is what made this\n * field unchecked in the first place; instead the two provable arms are closed\n * and {@link AdditionalFieldKey} is the explicit, castable escape.\n */\nexport type ColumnKey<M> =\n | PropertyPath<M>\n | `subcollection:${string}`\n | AdditionalFieldKey;\n\n/**\n * Opt-out for a `propertiesOrder` / `listProperties` entry that names an\n * `additionalFields` key rather than a property.\n *\n * The brand is **required**, which is the entire mechanism: a bare `\"score\"` is\n * not assignable, so the entry has to be written `\"score\" as AdditionalFieldKey`\n * — a visible admission that this key is not a property. An optional brand\n * (`__additionalFieldKey?: never`) would be satisfied by every string and put us\n * straight back to accepting typos.\n *\n * ```ts\n * propertiesOrder: [\"title\", \"score\" as AdditionalFieldKey]\n * ```\n */\nexport type AdditionalFieldKey = string & { readonly __additionalFieldKey: true };\n\n/**\n * Admin-panel presentation and behaviour for a collection.\n *\n * A `type` rather than an `interface`, and that is load-bearing: TypeScript gives\n * an implicit index signature to an object *type alias* but not to an interface.\n * `toAdminCollectionConfig` has to widen a collection carrying this block to\n * `Record<string, unknown>` in order to move the flattened keys back under\n * `admin`, and as an interface that conversion is an error (TS2352, \"index\n * signature for type 'string' is missing\"). Flipping it and running\n * `pnpm typecheck` reproduces that in one line.\n *\n * Declaration merging is not wanted here anyway; a plugin adding fields to the\n * block would have nothing reading them.\n *\n * @group Models\n */\nexport type AdminCollectionOptions<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n /**\n * Icon for the navigation sidebar or cards.\n *\n * Either a Lucide icon name (`\"FileText\"`, `\"ShoppingCart\"`) or a rendered\n * element. Prefer the name: it survives serialization, so the collection file\n * stays loadable by the backend and by `rebase generate-sdk`, and it is what\n * the schema editor writes back.\n */\n icon?: string | React.ReactNode;\n\n /**\n * Navigation group for this collection.\n * Collections sharing the same group name will be visually grouped\n * together in the drawer and home page. If not set, the collection\n * falls into the default \"Views\" group.\n */\n group?: string;\n\n /**\n * Array of entity views that this collection has.\n * Can be an array of `EntityCustomView` or a string representing the key of a global `EntityCustomView`.\n */\n entityViews?: (string | EntityCustomView<Record<string, unknown>>)[];\n\n /**\n * Default preview properties displayed when this collection is referenced to.\n */\n previewProperties?: Extract<keyof M, string>[];\n\n /**\n * Properties to display as columns in the list view.\n * If not specified, the list view uses a smart default (Title, Status, Date).\n */\n listProperties?: ColumnKey<M>[];\n\n /**\n * Title property of the entity. This is the property that will be used\n * as the title in entity related views and references.\n * If not specified, the first property simple text property will be used.\n */\n readonly titleProperty?: PropertyPath<M>;\n\n /**\n * When editing a entity, you can choose to open the entity in a side dialog\n * or in a full screen dialog. Defaults to `full_screen`.\n */\n openEntityMode?: \"side_panel\" | \"full_screen\" | \"split\" | \"dialog\";\n\n /**\n * Controls what happens when a user clicks on a entity in the collection view.\n * - `\"edit\"` (default): Opens the entity in the edit form.\n * - `\"view\"`: Opens a read-only detail view with an \"Edit\" button.\n */\n defaultEntityAction?: \"view\" | \"edit\";\n\n /**\n * Replace the default entity form with a custom component.\n * The Builder receives the same props as entity view tabs\n * (entity, formContext, collection, etc.) and has full control over the UI.\n *\n * Works in both edit mode and read-only mode (when `defaultEntityAction`\n * is `\"view\"`). In read-only mode, `formContext.readOnly` will be `true`.\n */\n formView?: FormViewConfig;\n\n /**\n * Prevent default actions from being displayed or executed on this collection.\n */\n disableDefaultActions?: (\"edit\" | \"copy\" | \"delete\")[];\n\n /**\n * Order in which the properties are displayed.\n * If you are specifying your collection as code, the order is the same as the\n * one you define in `properties`. Additional columns are added at the\n * end of the list, if the order is not specified.\n *\n * You can use this prop to hide some properties from the table view.\n * Note that if you set this prop, other ways to hide fields, like\n * `hidden` in the property definition, will be ignored.\n * `propertiesOrder` has precedence over `hidden`.\n *\n * Supported entry formats:\n * - For properties, use the property key.\n * - For additional fields, use the field key.\n * - Child collections (Firestore subcollections, or Postgres relations\n * with `many` cardinality) each get a column with id\n * `subcollection:<slug>`, e.g. `subcollection:orders`.\n */\n propertiesOrder?: ColumnKey<M>[];\n\n /**\n * If enabled, content is loaded in batches. If `false` all entities in the\n * collection are loaded. This means that when reaching the end of the\n * collection, the CMS will load more entities.\n * You can specify a number to specify the pagination size (50 by default)\n * Defaults to `true`\n */\n pagination?: boolean | number;\n\n selectionEnabled?: boolean;\n\n /**\n * Pass your own selection controller if you want to control selected\n * entities externally.\n * @see useSelectionController\n */\n selectionController?: SelectionController<M>;\n\n /**\n * Force a filter in this view. If applied, the rest of the filters will\n * be disabled. Filters applied with this prop cannot be changed.\n * e.g. `fixedFilter: { age: [\">\", 18] }`\n * e.g. `fixedFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly fixedFilter?: FilterValues<PropertyPath<M>>;\n\n /**\n * Initial filters applied to the collection this collection is related to.\n * Defaults to none. Filters applied with this prop can be changed.\n * e.g. `defaultFilter: { age: [\">\", 18] }`\n * e.g. `defaultFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n // Keyed by property *path*, not by `FilterValues<M>` — the latter types each\n // value against that property's own type, which is what the old note here\n // warned breaks code-defined collections (an `EntityReference` filter on a\n // relation, a `Date` on a string column). Narrowing the key is independent\n // of that, and a dotted path still reaches into a `map`/jsonb column.\n readonly defaultFilter?: FilterValues<PropertyPath<M>>;\n\n /**\n * Pre-defined filter presets that appear as quick-access options in the\n * collection toolbar. Each preset applies a set of filters (and\n * optionally a sort order) with a single click.\n *\n * ```ts\n * filterPresets: [\n * {\n * label: \"Shipped this month\",\n * filterValues: {\n * status: [\"==\", \"shipped\"],\n * order_date: [\">=\", new Date(Date.now() - 30 * 86400000)]\n * }\n * }\n * ]\n * ```\n */\n readonly filterPresets?: FilterPreset<PropertyPath<M>>[];\n\n /**\n * Default sort applied to this collection.\n * When setting this prop, entities will have a default order\n * applied in the collection.\n * e.g. `sort: [\"order\", \"asc\"]`\n */\n readonly sort?: OrderByTuple<PropertyPath<M>>;\n\n /**\n * You can add additional fields to the collection view by implementing\n * an additional field delegate.\n */\n readonly additionalFields?: AdditionalFieldDelegate<M, USER>[];\n\n /**\n * Default size of the rendered collection\n */\n defaultSize?: CollectionSize;\n\n /**\n * Can the elements in this collection be edited inline in the collection\n * view. Even when inline editing is disabled, entities can still be\n * edited in the side panel (subject to `securityRules`).\n */\n inlineEditing?: boolean;\n\n /**\n * Should this collection be hidden from the main navigation panel, if\n * it is at the root level, or in the entity side panel if it's a\n * subcollection.\n * It will still be accessible if you reach the specified path.\n * You can also use this collection as a reference target.\n */\n hideFromNavigation?: boolean;\n\n /**\n * If you want to open custom views or subcollections by default when opening the edit\n * view of a entity, you can specify the path to the view here.\n * The path is relative to the current collection. For example if you have a collection\n * that has a custom view as well as a subcollection that refers to another entity, you can\n * either specify the path to the custom view or the path to the subcollection.\n */\n defaultSelectedView?: string | DefaultSelectedViewBuilder;\n\n /**\n * Should the ID of this collection be hidden from the form view.\n */\n hideIdFromForm?: boolean;\n\n /**\n * Should the ID of this collection be hidden from the grid view.\n */\n hideIdFromCollection?: boolean;\n\n /**\n * If set to true, the form will be auto-saved when the user changes\n * the value of a field.\n * Defaults to false.\n * When a new entity is created, this property can be updated to generated a new ID\n */\n formAutoSave?: boolean;\n\n /**\n *\n */\n exportable?: boolean | ExportConfig<USER>;\n\n /**\n * Width of the side dialog (in pixels) when opening a entity in this collection.\n */\n sideDialogWidth?: number | string;\n\n /**\n * If set to true, the default values of the properties will be applied\n * to the entity every time the entity is updated (not only when created).\n * Defaults to false.\n */\n alwaysApplyDefaultValues?: boolean;\n\n /**\n * If set to true, a tab including the JSON representation of the entity will be included.\n */\n includeJsonView?: boolean;\n\n /**\n * Should local changes be backed up in local storage, to prevent data loss on\n * accidental navigations.\n * - `manual_apply`: When the user navigates back to a entity with local changes,\n * they will be prompted to restore the changes.\n * - `auto_apply`: When the user navigates back to a entity with local changes,\n * the changes will be automatically applied.\n * - `false`: Local changes will not be backed up.\n * Defaults to `manual_apply`.\n */\n localChangesBackup?: \"manual_apply\" | \"auto_apply\" | false;\n\n /**\n * Default view mode for displaying this collection.\n * - \"table\": Display entities in a table with inline editing (default)\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * Defaults to \"table\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"table\", \"cards\", \"kanban\".\n * Defaults to all three: [\"table\", \"cards\", \"kanban\"].\n * Note: \"kanban\" will only be available if the collection has at least\n * one string property with `enum` defined, regardless of this setting.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n * Must reference a string/text property. When items are reordered,\n * this property will be updated with lexicographic sort keys\n * (e.g. \"a0\", \"a1\", \"a0V\") using string-based fractional indexing.\n * Used by Kanban view for ordering within columns\n * and can be used for general ordering purposes.\n */\n readonly orderProperty?: Extract<keyof M, string>;\n\n /**\n * Actions that can be performed on the entities in this collection.\n */\n entityActions?: EntityAction<M, USER>[];\n\n /**\n * Builder for the collection actions rendered in the toolbar\n */\n Actions?: ComponentRef<CollectionActionsProps>[];\n\n /**\n * Collection-scoped component overrides. These take precedence over\n * global overrides set on `<Rebase>`, but only within this collection's\n * views (entity form, detail view, table, empty state, etc.).\n *\n * Only collection-scoped components (like `Entity.Form`, `Collection.EmptyState`,\n * `Collection.Card`, etc.) can be overridden here. App-level components\n * (like `Shell.AppBar`, `HomePage`) can only be overridden at the `<Rebase>` level.\n *\n * @example\n * ```tsx\n * const productsCollection: PostgresCollectionConfig = {\n * name: \"Products\",\n * slug: \"products\",\n * table: \"products\",\n * components: {\n * \"Entity.Form\": { Component: ProductCustomForm },\n * \"Collection.Card\": { Component: ProductCard },\n * },\n * properties: { ... }\n * };\n * ```\n */\n components?: CollectionComponentOverrideMap;};\n\n/**\n * There is deliberately no `AdminCollectionConfig` here any more.\n *\n * It used to be `Omit<CollectionConfig, \"admin\"> & { admin?: AdminCollectionOptions }`,\n * a wrapper that existed because core typed the block opaquely. Now that `augment.ts`\n * declares `admin` directly on `BaseCollectionConfig`, `CollectionConfig` *is* the\n * authoring type — the wrapper would be an alias of it, and a second name for one thing\n * is what this whole refactor has been removing.\n *\n * A project opts its program in with one line, once:\n *\n * ```ts\n * /// <reference types=\"@rebasepro/admin-types\" />\n * ```\n *\n * after which `admin` is typed on every collection and every property. Without it,\n * writing one is an error — which is the guarantee a BaaS install depends on.\n */\n\n/**\n * Define a collection with the admin block type-checked.\n *\n * The same identity function as `defineCollection` in `@rebasepro/common` — which\n * is what a BaaS or headless project uses, and where `admin` does not exist at all\n * — with one difference: importing this one brings the augmentation with it, so\n * `admin: { icon, listProperties, kanban }` gets completion and a typo is an\n * error. See {@link AdminCollectionOptions}.\n *\n * Import it from the layer you are in. A project with an admin panel wants this\n * one; a project without one has no `admin` block to check.\n *\n * `const P` captures the literal property types, which is what gives\n * `admin.titleProperty`, `admin.sort` and `admin.propertiesOrder` completion over\n * the collection's own property keys rather than plain `string`.\n *\n * @example\n * export default defineCollection({\n * slug: \"posts\",\n * table: \"posts\",\n * properties: {\n * title: { name: \"Title\", type: \"string\" },\n * status: { name: \"Status\", type: \"string\" }\n * },\n * admin: {\n * icon: \"FileText\",\n * titleProperty: \"title\", // completion: \"title\" | \"status\"\n * listProperties: [\"title\", \"status\"]\n * }\n * });\n *\n * @group Builder\n */\nexport function defineCollection<\n const P extends PostgresProperties,\n USER extends User = User\n>(\n collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a Firestore-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends FirebaseProperties,\n USER extends User = User\n>(\n collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a MongoDB-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends MongoProperties,\n USER extends User = User\n>(\n collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Identity at runtime; the overloads above are the whole point. @group Builder */\nexport function defineCollection(collection: CollectionConfig): CollectionConfig {\n return collection;\n}\n\n/**\n * Re-exported from `@rebasepro/types`, where the list has to live: the ts-morph\n * schema editor in `@rebasepro/server` needs it to know which keys go inside the\n * block when it rewrites a collection file, and a core package may not import\n * this one. The list is plain data, so core is a fine home for it.\n *\n * What core *cannot* do is check the list against the type. That happens here.\n *\n * @group Models\n */\nexport type { AdminCollectionKey } from \"@rebasepro/types\";\n\n/**\n * Core's list, re-exported through a `satisfies` clause that is the agreement\n * check: a key core names that is not an option here fails to compile, and\n * `satisfies` keeps the literal tuple type rather than widening it to `string[]`.\n *\n * The reverse direction — an option missing from core's list — has no type-level\n * expression, since there is no exhaustiveness check over an optional-property\n * keyof. `test/admin_collection.test.ts` counts them instead.\n */\nexport const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];\n\n\n/**\n * A collection as the admin panel works with it: the contract with the `admin`\n * block flattened onto the top level.\n *\n * The panel reads presentation fields in a few hundred places, and threading\n * `collection.admin?.propertiesOrder` through all of them would be noise that\n * buys nothing — the panel has already resolved the collection by then, merging\n * the declared config with the user's per-collection overrides from local\n * storage. So the panel gets a flat *view model*, exactly as it already does for\n * entities (`Entity` is an admin view model over flat rows, not a wire type).\n *\n * The distinction that matters is direction:\n *\n * - **Reading** a resolved collection → `AdminCollection` (flat, convenient).\n * - **Authoring or persisting** one → core's `CollectionConfig`, with the `admin`\n * block this package augments onto it (nested, which is what the file on disk\n * and the wire both look like).\n *\n * `admin` is kept alongside the flattened fields so the collection editor can\n * still see the block it has to write back.\n *\n * @group Models\n */\nexport type AdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = WithFlatAdmin<CollectionConfig<M, USER>, M, USER>;\n\n/**\n * Flatten the admin block onto one member of the collection union at a time.\n *\n * `CollectionConfig` is a union discriminated on `engine`\n * (Postgres | Firestore | MongoDB), and a bare `Omit<Union, \"admin\">` collapses it\n * into a single object type with the discriminant widened. The result stops being\n * assignable back to `CollectionConfig`, so every call that hands a resolved\n * collection to a core function fails — which is exactly what happened. The\n * `C extends unknown` clause makes the mapping distributive, so each member keeps\n * its literal `engine` and stays assignable to its counterpart.\n */\ntype WithFlatAdmin<C, M extends Record<string, unknown>, USER extends User> =\n C extends unknown\n ? Omit<C, \"admin\"> & AdminCollectionOptions<M, USER> & { admin?: AdminCollectionOptions<M, USER> }\n : never;\n\n/** {@link AdminCollection} for a Postgres collection. @group Models */\nexport type AdminPostgresCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = Omit<PostgresCollectionConfig<M, USER>, \"admin\">\n & AdminCollectionOptions<M, USER>\n & { admin?: AdminCollectionOptions<M, USER> };\n\n/**\n * Flatten a collection's `admin` block onto it, producing the panel's view model.\n *\n * Shallow by design: the block's fields are independent, so a deep merge would\n * only create opportunities for a nested object to be half from one source and\n * half from the other. `admin` survives on the result.\n *\n * Idempotent — flattening an already-flat collection returns an equivalent one —\n * because the panel resolves collections at more than one entry point (the\n * registry, `<Rebase collections>`, a plugin's `modifyCollection`) and they must\n * not fight over which has run.\n */\nexport function resolveAdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n>(collection: CollectionConfig<M, USER> | AdminCollection<M, USER>): AdminCollection<M, USER> {\n const block = (collection as { admin?: AdminCollectionOptions<M, USER> }).admin;\n if (!block) return collection as AdminCollection<M, USER>;\n return { ...(collection as AdminCollection<M, USER>), ...block, admin: block };\n}\n\n/**\n * The inverse: lift flattened admin fields back into the block.\n *\n * Used on the way out — persisting from the collection editor, or handing a\n * collection to anything that expects the authoring shape. Any key in\n * {@link ADMIN_COLLECTION_KEYS} found at the top level is moved down, so a\n * round trip through the panel does not leave the file flat.\n */\nexport function toAdminCollectionConfig<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n>(collection: AdminCollection<M, USER> | CollectionConfig<M, USER>): CollectionConfig<M, USER> {\n const source = collection as Record<string, unknown>;\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...(source.admin as object ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if ((ADMIN_COLLECTION_KEYS as readonly string[]).includes(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top as unknown as CollectionConfig<M, USER>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,eAAkB,WAAoD;CAClF,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAoB,WAAqD;CACrF,OAAO;AACX;;;;AC0cA,SAAgB,iBAAiB,YAAgD;CAC7E,OAAO;AACX;;;;;;;;;;AAuBA,IAAa,wBAAwB;;;;;;;;;;;;;AAmErC,SAAgB,uBAGd,YAA4F;CAC1F,MAAM,QAAS,WAA2D;CAC1E,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EAAE,GAAI;EAAyC,GAAG;EAAO,OAAO;CAAM;AACjF;;;;;;;;;AAUA,SAAgB,wBAGd,YAA6F;CAC3F,MAAM,SAAS;CACf,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAI,OAAO,SAAmB,CAAC,EAAG;CAE3E,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAK,sBAA4C,SAAS,GAAG,GAAG,MAAM,OAAO;OACxE,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX"}
@@ -70,5 +70,5 @@ export type PropertyConfig = {
70
70
  */
71
71
  description?: string;
72
72
  };
73
- export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "user_select" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "multi_references" | "relation" | "switch" | "date_time" | "repeat" | "custom_array" | "block" | "vector_input";
73
+ export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "user_select" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "multi_references" | "relation" | "switch" | "date_time" | "repeat" | "custom_array" | "block" | "vector_input" | "geopoint" | "binary";
74
74
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin-types",
3
3
  "type": "module",
4
- "version": "0.10.1-canary.3d09338",
4
+ "version": "0.10.1-canary.60301c3",
5
5
  "description": "Rebase admin-panel type definitions — the React-flavoured half of the type surface",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -29,7 +29,7 @@
29
29
  "headless cms"
30
30
  ],
31
31
  "dependencies": {
32
- "@rebasepro/types": "0.10.1-canary.3d09338"
32
+ "@rebasepro/types": "0.10.1-canary.60301c3"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/jest": "^30.0.0",
@@ -47,6 +47,54 @@ import type { EntityAction } from "./types/entity_actions";
47
47
  import type { ExportConfig } from "./types/export_import";
48
48
  import type { CollectionComponentOverrideMap } from "./types/component_overrides";
49
49
 
50
+ /**
51
+ * A key naming one of `M`'s fields, or a dotted path into a `map` field.
52
+ *
53
+ * Both forms are resolved with `getValueInPath`, so `"profile.displayName"` is
54
+ * as valid as `"title"`. Only the *root* is checked — the path below it is a
55
+ * nested `Properties` object this type has no view of — which is enough to
56
+ * reject the mistake that actually happens: a misspelled or removed field.
57
+ *
58
+ * When `M` is the default `Record<string, unknown>` — the plain
59
+ * `const x: PostgresCollectionConfig = { … }` annotation, which infers nothing —
60
+ * `Extract<keyof M, string>` is `string` and this accepts anything, exactly as
61
+ * before. `defineCollection` is what supplies a real `M` and turns the check on.
62
+ */
63
+ export type PropertyPath<M> =
64
+ | Extract<keyof M, string>
65
+ | `${Extract<keyof M, string>}.${string}`;
66
+
67
+ /**
68
+ * A key naming a *column* in the list view: a property path, a child-collection
69
+ * column, or the `key` of one of this collection's `additionalFields`.
70
+ *
71
+ * `AdditionalFieldDelegate.key` is a plain `string`, and the block is not
72
+ * generic over its own `additionalFields`, so there is no type-level channel
73
+ * carrying those keys here. Accepting any string to cover them is what made this
74
+ * field unchecked in the first place; instead the two provable arms are closed
75
+ * and {@link AdditionalFieldKey} is the explicit, castable escape.
76
+ */
77
+ export type ColumnKey<M> =
78
+ | PropertyPath<M>
79
+ | `subcollection:${string}`
80
+ | AdditionalFieldKey;
81
+
82
+ /**
83
+ * Opt-out for a `propertiesOrder` / `listProperties` entry that names an
84
+ * `additionalFields` key rather than a property.
85
+ *
86
+ * The brand is **required**, which is the entire mechanism: a bare `"score"` is
87
+ * not assignable, so the entry has to be written `"score" as AdditionalFieldKey`
88
+ * — a visible admission that this key is not a property. An optional brand
89
+ * (`__additionalFieldKey?: never`) would be satisfied by every string and put us
90
+ * straight back to accepting typos.
91
+ *
92
+ * ```ts
93
+ * propertiesOrder: ["title", "score" as AdditionalFieldKey]
94
+ * ```
95
+ */
96
+ export type AdditionalFieldKey = string & { readonly __additionalFieldKey: true };
97
+
50
98
  /**
51
99
  * Admin-panel presentation and behaviour for a collection.
52
100
  *
@@ -94,20 +142,20 @@ export type AdminCollectionOptions<
94
142
  /**
95
143
  * Default preview properties displayed when this collection is referenced to.
96
144
  */
97
- previewProperties?: string[];
145
+ previewProperties?: Extract<keyof M, string>[];
98
146
 
99
147
  /**
100
148
  * Properties to display as columns in the list view.
101
149
  * If not specified, the list view uses a smart default (Title, Status, Date).
102
150
  */
103
- listProperties?: string[];
151
+ listProperties?: ColumnKey<M>[];
104
152
 
105
153
  /**
106
154
  * Title property of the entity. This is the property that will be used
107
155
  * as the title in entity related views and references.
108
156
  * If not specified, the first property simple text property will be used.
109
157
  */
110
- readonly titleProperty?: Extract<keyof M, string> | (string & {});
158
+ readonly titleProperty?: PropertyPath<M>;
111
159
 
112
160
  /**
113
161
  * When editing a entity, you can choose to open the entity in a side dialog
@@ -155,7 +203,7 @@ export type AdminCollectionOptions<
155
203
  * with `many` cardinality) each get a column with id
156
204
  * `subcollection:<slug>`, e.g. `subcollection:orders`.
157
205
  */
158
- propertiesOrder?: (Extract<keyof M, string> | (string & {}) | string | `subcollection:${string}`)[];
206
+ propertiesOrder?: ColumnKey<M>[];
159
207
 
160
208
  /**
161
209
  * If enabled, content is loaded in batches. If `false` all entities in the
@@ -181,7 +229,7 @@ export type AdminCollectionOptions<
181
229
  * e.g. `fixedFilter: { age: [">", 18] }`
182
230
  * e.g. `fixedFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
183
231
  */
184
- readonly fixedFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;
232
+ readonly fixedFilter?: FilterValues<PropertyPath<M>>;
185
233
 
186
234
  /**
187
235
  * Initial filters applied to the collection this collection is related to.
@@ -189,7 +237,12 @@ export type AdminCollectionOptions<
189
237
  * e.g. `defaultFilter: { age: [">", 18] }`
190
238
  * e.g. `defaultFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
191
239
  */
192
- readonly defaultFilter?: FilterValues<Extract<keyof M, string> | (string & {})>; // setting FilterValues<M> can break defining collections by code
240
+ // Keyed by property *path*, not by `FilterValues<M>` the latter types each
241
+ // value against that property's own type, which is what the old note here
242
+ // warned breaks code-defined collections (an `EntityReference` filter on a
243
+ // relation, a `Date` on a string column). Narrowing the key is independent
244
+ // of that, and a dotted path still reaches into a `map`/jsonb column.
245
+ readonly defaultFilter?: FilterValues<PropertyPath<M>>;
193
246
 
194
247
  /**
195
248
  * Pre-defined filter presets that appear as quick-access options in the
@@ -208,7 +261,7 @@ export type AdminCollectionOptions<
208
261
  * ]
209
262
  * ```
210
263
  */
211
- readonly filterPresets?: FilterPreset<Extract<keyof M, string> | (string & {})>[];
264
+ readonly filterPresets?: FilterPreset<PropertyPath<M>>[];
212
265
 
213
266
  /**
214
267
  * Default sort applied to this collection.
@@ -216,7 +269,7 @@ export type AdminCollectionOptions<
216
269
  * applied in the collection.
217
270
  * e.g. `sort: ["order", "asc"]`
218
271
  */
219
- readonly sort?: OrderByTuple<Extract<keyof M, string> | (string & {})>;
272
+ readonly sort?: OrderByTuple<PropertyPath<M>>;
220
273
 
221
274
  /**
222
275
  * You can add additional fields to the collection view by implementing
@@ -338,7 +391,7 @@ export type AdminCollectionOptions<
338
391
  * Used by Kanban view for ordering within columns
339
392
  * and can be used for general ordering purposes.
340
393
  */
341
- readonly orderProperty?: Extract<keyof M, string> | (string & {});
394
+ readonly orderProperty?: Extract<keyof M, string>;
342
395
 
343
396
  /**
344
397
  * Actions that can be performed on the entities in this collection.
package/src/augment.ts CHANGED
@@ -35,12 +35,31 @@ import type {
35
35
  AdminVectorOptions
36
36
  } from "./types/property_options";
37
37
  import type { AdminCollectionOptions } from "./admin_collection";
38
+ // Named in the merged declaration's parameter list, which must match core's.
39
+ import type { User } from "@rebasepro/types";
38
40
 
39
41
  declare module "@rebasepro/types" {
40
42
 
41
- /** Presentation and behaviour for a collection in the admin panel. */
42
- interface BaseCollectionConfig {
43
- admin?: AdminCollectionOptions;
43
+ /**
44
+ * Presentation and behaviour for a collection in the admin panel.
45
+ *
46
+ * The type parameters are repeated here verbatim because declaration
47
+ * merging requires an identical parameter list — and they have to be
48
+ * *forwarded* to `AdminCollectionOptions`, which is the part that was
49
+ * missing. Written as a bare `admin?: AdminCollectionOptions`, `M` fell
50
+ * back to its default `Record<string, unknown>`, so `Extract<keyof M,
51
+ * string>` widened to `string` and every key-shaped field in the block —
52
+ * `titleProperty`, `sort`, `propertiesOrder`, `listProperties` — silently
53
+ * accepted any string. The completion `defineCollection` advertises is
54
+ * derived from `M`, so it never appeared: the inference was computed,
55
+ * carried to this seam, and dropped one line short of the field that
56
+ * needed it.
57
+ */
58
+ interface BaseCollectionConfig<
59
+ M extends Record<string, unknown> = Record<string, unknown>,
60
+ USER extends User = User
61
+ > {
62
+ admin?: AdminCollectionOptions<M, USER>;
44
63
  }
45
64
 
46
65
  interface BaseProperty<CustomProps = unknown> {
@@ -16,7 +16,7 @@ import React, { Dispatch, SetStateAction } from "react";
16
16
  import type { Entity, EntityStatus, FilterValues, Property, User } from "@rebasepro/types";
17
17
 
18
18
  import type { RebaseContext } from "./rebase_context";
19
- import type { AdminCollection } from "@rebasepro/admin-types";
19
+ import type { AdminCollection, PropertyPath } from "@rebasepro/admin-types";
20
20
 
21
21
  /**
22
22
  * Configuration for Kanban board view mode.
@@ -28,6 +28,21 @@ export interface KanbanConfig<M extends Record<string, unknown> = Record<string,
28
28
  * Must reference a string property with `enum` values defined.
29
29
  * Entities will be grouped into columns based on this property's value.
30
30
  * The column order is determined by the order of `enum` values in the property.
31
+ *
32
+ * Left permissive on purpose, unlike the *optional* key fields on the admin
33
+ * block (`titleProperty`, `sort`, `propertiesOrder`, …), which are checked
34
+ * against `M`.
35
+ *
36
+ * This one is **required**, and that is the whole difference: a required
37
+ * generic-dependent property puts `Extract<keyof M, string>` in an invariant
38
+ * position, so `AdminCollection<M>` stops being assignable to
39
+ * `AdminCollection`. The admin package passes collections between those two
40
+ * forms in roughly fifteen places — `CollectionBoardViewBinding`, the
41
+ * collection editor, the view bindings — and every one of them breaks.
42
+ * Tightening this is a worthwhile change, but it is a refactor of those call
43
+ * sites rather than a type edit. Verified by trying it: `columnProperty:
44
+ * Extract<keyof M, string>` alone produced ~15 errors, and doing it alongside
45
+ * `entityViews`/`formView`/`Actions` produced 95.
31
46
  */
32
47
  columnProperty: Extract<keyof M, string> | (string & {});
33
48
  }
@@ -182,8 +197,15 @@ export interface AdditionalFieldDelegate<M extends Record<string, unknown> = Rec
182
197
  * e.g. ["name", "surname"]
183
198
  * This is a performance optimization, if you don't define dependencies
184
199
  * it will be updated in every render.
200
+ *
201
+ * A key that is not a property of this collection can never change, so
202
+ * listing one is always a mistake — it silently pins the column to the
203
+ * "never re-render" path, which reads as a stale cell rather than a typo.
204
+ * The `NoInfer` wrappers keep these keys from participating in inference of
205
+ * `M`; the `(string & {})` arms they used to sit beside made the whole union
206
+ * `string`, so the wrappers had nothing to protect.
185
207
  */
186
- dependencies?: NoInfer<Extract<keyof M, string>> | NoInfer<Extract<keyof M, string>>[] | (string & {}) | (string & {})[];
208
+ dependencies?: NoInfer<PropertyPath<M>> | NoInfer<PropertyPath<M>>[];
187
209
 
188
210
  /**
189
211
  * Use this prop to define the value of the column as a string or number.
@@ -92,4 +92,6 @@ export type PropertyConfigId =
92
92
  "repeat" |
93
93
  "custom_array" |
94
94
  "block" |
95
- "vector_input";
95
+ "vector_input" |
96
+ "geopoint" |
97
+ "binary";