@rebasepro/cms-types 0.20.0 → 0.21.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.
- package/dist/admin_collection.d.ts +3 -1
- package/dist/collections.d.ts +95 -4
- package/dist/index.es.js.map +1 -1
- package/dist/rebase_context.d.ts +8 -1
- package/dist/types/entity_views.d.ts +19 -5
- package/dist/types/property_options.d.ts +11 -4
- package/dist/types/translations.d.ts +20 -0
- package/package.json +2 -2
|
@@ -204,7 +204,9 @@ export type AdminCollectionOptions<M extends Record<string, unknown> = Record<st
|
|
|
204
204
|
* (entity, formContext, collection, etc.) and has full control over the UI.
|
|
205
205
|
*
|
|
206
206
|
* Works in both edit mode and read-only mode (when `defaultEntityAction`
|
|
207
|
-
* is `"view"
|
|
207
|
+
* is `"view"`, or for a user who may not edit the record). In edit mode
|
|
208
|
+
* `formContext` is the record form's live context; in read-only mode
|
|
209
|
+
* `formContext.disabled` and `formContext.readOnly` are both `true`.
|
|
208
210
|
*/
|
|
209
211
|
formView?: FormViewConfig;
|
|
210
212
|
/**
|
package/dist/collections.d.ts
CHANGED
|
@@ -121,14 +121,105 @@ export interface CollectionActionsProps<M extends Record<string, unknown> = Reco
|
|
|
121
121
|
openNewDocument: (defaultValues?: Record<string, unknown>) => void;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* The query a selection stands for: the view's filter, search and sort at the
|
|
125
|
+
* moment "select all matching" was clicked.
|
|
126
|
+
*
|
|
127
|
+
* It is captured rather than read live, because the filter is editable while a
|
|
128
|
+
* selection is held. A selection that silently followed the filter bar would
|
|
129
|
+
* mean the rows you are about to delete are not the rows you counted.
|
|
130
|
+
*
|
|
131
|
+
* @group Models
|
|
132
|
+
*/
|
|
133
|
+
export interface SelectionQuery<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
134
|
+
path: string;
|
|
135
|
+
filterValues?: FilterValues<Extract<keyof M, string> | (string & {})>;
|
|
136
|
+
searchString?: string;
|
|
137
|
+
sortBy?: OrderByTuple<Extract<keyof M, string> | (string & {})>[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a selection *is*.
|
|
141
|
+
*
|
|
142
|
+
* Ticking rows gives you `"entities"` — the rows themselves, all of them
|
|
143
|
+
* loaded. Clicking "select all 12,480 matching" gives you `"query"`, which
|
|
144
|
+
* stands for rows that mostly have not been read and, past a few pages, never
|
|
145
|
+
* will be all at once.
|
|
146
|
+
*
|
|
147
|
+
* This is a union rather than an `Entity[]` with a flag beside it because the
|
|
148
|
+
* difference has to be unmissable at the point of use. The array it replaced
|
|
149
|
+
* held, in query mode, whatever the view happened to have scrolled — so
|
|
150
|
+
* `selectedEntities.length` read 50, a bulk delete deleted 50, and it reported
|
|
151
|
+
* success. There is no way to write that against a union: the `"query"` branch
|
|
152
|
+
* has no `entities` to reach for, and `resolveSelection` is the only thing that
|
|
153
|
+
* can produce them.
|
|
154
|
+
*
|
|
155
|
+
* @group Models
|
|
156
|
+
*/
|
|
157
|
+
export type EntitySelection<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
158
|
+
type: "entities";
|
|
159
|
+
/** The rows that were picked, in the order they were picked. */
|
|
160
|
+
entities: Entity<M>[];
|
|
161
|
+
} | {
|
|
162
|
+
type: "query";
|
|
163
|
+
/** Every row matching this is selected, bar the exclusions. */
|
|
164
|
+
query: SelectionQuery<M>;
|
|
165
|
+
/**
|
|
166
|
+
* Rows ticked back off after selecting the query.
|
|
167
|
+
*
|
|
168
|
+
* Gmail drops to a plain list when you untick one; keeping the query and
|
|
169
|
+
* carrying the exclusions is both closer to what was meant ("all of them
|
|
170
|
+
* except that one") and cheaper — dropping to a list would have to read
|
|
171
|
+
* every row first.
|
|
172
|
+
*/
|
|
173
|
+
excluded: Entity<M>[];
|
|
174
|
+
/**
|
|
175
|
+
* The server's count for `query` when the selection was made.
|
|
176
|
+
*
|
|
177
|
+
* `undefined` where the accessor has no `count` — a collection can be
|
|
178
|
+
* selected in full without anyone knowing how large it is, and the UI
|
|
179
|
+
* says so rather than inventing a number.
|
|
180
|
+
*/
|
|
181
|
+
count?: number;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Use this controller to retrieve the selection or modify it in an
|
|
185
|
+
* {@link AdminCollection}.
|
|
186
|
+
*
|
|
187
|
+
* Read {@link SelectionController.selection} to find out what is selected. It
|
|
188
|
+
* is a {@link EntitySelection} union, so a consumer that wants rows has to say
|
|
189
|
+
* what it does when the selection is a query — {@link resolveSelection} reads
|
|
190
|
+
* them, page by page, with a ceiling and a progress callback.
|
|
191
|
+
*
|
|
192
|
+
* {@link SelectionController.isEntitySelected} and
|
|
193
|
+
* {@link SelectionController.toggleEntitySelection} work the same in both
|
|
194
|
+
* modes and are what a per-row checkbox should use.
|
|
195
|
+
*
|
|
126
196
|
* @group Models
|
|
127
197
|
*/
|
|
128
198
|
export interface SelectionController<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
129
|
-
|
|
199
|
+
/** What is selected: the rows, or the query they stand for. */
|
|
200
|
+
selection: EntitySelection<M>;
|
|
201
|
+
setSelection(selection: EntitySelection<M>): void;
|
|
202
|
+
setSelection(action: (prev: EntitySelection<M>) => EntitySelection<M>): void;
|
|
203
|
+
/**
|
|
204
|
+
* How many rows are selected.
|
|
205
|
+
*
|
|
206
|
+
* `undefined` only in query mode against an accessor with no `count`:
|
|
207
|
+
* "every matching row, we do not know how many". Callers must render that
|
|
208
|
+
* case rather than defaulting it to zero.
|
|
209
|
+
*/
|
|
210
|
+
selectedCount: number | undefined;
|
|
211
|
+
/** Whether anything at all is selected. Cheap in both modes. */
|
|
212
|
+
hasSelection: boolean;
|
|
213
|
+
/** Replace the selection with exactly these rows. */
|
|
130
214
|
setSelectedEntities(entities: Entity<M>[]): void;
|
|
131
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Select every row matching `query`, minus whatever gets unticked later.
|
|
217
|
+
*
|
|
218
|
+
* `count` is what the UI reports and what {@link resolveSelection} checks
|
|
219
|
+
* its ceiling against; pass the view's own filtered count so the two agree.
|
|
220
|
+
*/
|
|
221
|
+
selectAllMatching(query: SelectionQuery<M>, count?: number): void;
|
|
222
|
+
clearSelection(): void;
|
|
132
223
|
isEntitySelected(entity: Entity<M>): boolean;
|
|
133
224
|
toggleEntitySelection(entity: Entity<M>, newSelectedState?: boolean): void;
|
|
134
225
|
}
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/types/property_options.ts","../src/react_component_ref.ts","../src/define_collection.ts","../src/admin_collection.ts","../src/types/entity_display.ts","../src/types/form_layout.ts","../src/types/slots.tsx"],"sourcesContent":["/**\n * Per-property presentation options.\n *\n * These lived in `@rebasepro/types` next to the property types they belong to, which\n * meant a BaaS install shipped `Field`, `Preview`, `columnWidth` and `hideFromCollection`\n * in its type surface with nothing to render them. They are attached to the property\n * types by `augment.ts` instead.\n */\nimport type { ComponentRef, FilterValues, WhereFilterOp } from \"@rebasepro/types\";\nimport { ADMIN_PROPERTY_KEYS as CORE_ADMIN_PROPERTY_KEYS } from \"@rebasepro/types\";\nimport type { PropertySpan } from \"./form_layout\";\n\n/**\n * Interface including all common properties of an admin property.\n * @group Entity properties\n */\nexport interface AdminPropertyOptions<CustomProps = unknown> {\n /**\n * Width of this property's column in the table view, in pixels. Omit and the\n * table derives one from the property type.\n *\n * A person can drag a column wider, and that is remembered per user; this is\n * the width everyone starts from.\n */\n columnWidth?: number;\n /**\n * Keep this property out of the table and card views. It is still on the\n * entity form, and still read and written by the API.\n *\n * For a field that should not leave the server at all, use\n * `excludeFromApi` on the property itself — this one is presentation, and\n * hiding a secret with it hides it from exactly one screen.\n */\n hideFromCollection?: boolean;\n /**\n * Render the **value**, not a control. Defaults to `false`.\n *\n * The distinction from {@link disabled} is what the field looks like and\n * what it can do. `readOnly` shows the value as text: nothing to focus,\n * nothing to clear, and no explanation owed, because there is no control to\n * wonder about. Use it for something the server owns — a computed total, an\n * `autoValue` timestamp.\n *\n * A property that is read-only *for some people* is not this: that is a\n * security rule, and writing it here leaves the column writable through the\n * API by anyone who skips the panel.\n */\n readOnly?: boolean;\n /**\n * Render the control, greyed out. Defaults to `false`.\n *\n * The counterpart to {@link readOnly}: this one is a field that *could* be\n * edited but is not, right now — usually because of a condition, which is\n * why it takes a config with a `disabledMessage` saying why and a\n * `clearOnDisabled` for the value that no longer applies. The control stays\n * visible so the reader can see the shape of what they are not allowed to\n * fill in.\n */\n disabled?: boolean | PropertyDisabledConfig;\n\n /**\n * How many of the form grid's {@link FORM_GRID_COLUMNS} columns this field\n * occupies. Omit to let the layout derive one from the property type.\n *\n * Spans snap to a shared grid, so two fields line up whatever order they\n * are declared in.\n */\n span?: PropertySpan;\n /**\n * Anything your own {@link Field} or {@link Preview} needs, passed straight\n * through untouched.\n *\n * Typed by the property's own `CustomProps` parameter, so a custom field\n * declares what it expects and a collection that supplies the wrong shape is\n * a compile error rather than an `undefined` at render time.\n */\n customProps?: CustomProps;\n /**\n * Replace the form control for this property.\n *\n * The component receives `FieldProps` — the value, `setValue`, the resolved\n * property, the whole entity's values, and any {@link customProps}. It owns\n * the input; validation, the label and the error line stay with the form.\n *\n * A `ComponentRef` rather than a component so a collection stays\n * serializable: the reference is a registered key, which survives being sent\n * to the schema editor and written back to the file.\n */\n Field?: ComponentRef<any>;\n /**\n * Replace how this property renders when it is *not* being edited — a table\n * cell, a card line, a reference chip.\n *\n * Separate from {@link Field} because the two are read in different places\n * and at different sizes; overriding one and not the other is normal.\n */\n Preview?: ComponentRef<any>;\n\n /**\n * Narrow the filter operators offered for this property in collection\n * filter UIs (table header filters and the Filters dialog).\n *\n * The final offered set is the **intersection** of the engine's\n * capabilities, the property-type defaults, and this list — you can only\n * *restrict*, never enable an operator the underlying engine cannot run.\n *\n * Pass an empty array to disable filtering on this property entirely.\n *\n * @example\n * // Email column: exact match, contains, and null check only\n * admin: { filterOperators: [\"==\", \"ilike\", \"is-null\"] }\n */\n filterOperators?: readonly WhereFilterOp[];\n\n /**\n * Replace the filter field rendered for this property in collection\n * filter UIs. The component receives `FilterFieldBindingProps`\n * (property, resolved `operators`, `value`, `setValue`, …).\n *\n * Takes precedence over the collection-level\n * `components[\"Collection.FilterField\"]` override and the built-in\n * per-type filter fields.\n */\n Filter?: ComponentRef<any>;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminStringOptions extends AdminPropertyOptions {\n /**\n * Is this string property long enough so it should be displayed in\n * a multiple line field. Defaults to false. If set to true,\n * the number of lines adapts to the content\n */\n multiline?: boolean;\n /**\n * Should this string property be displayed as a markdown field. If true,\n * the field is rendered as a text editor that supports markdown highlight\n * syntax. It also includes a preview of the result.\n */\n markdown?: boolean;\n /**\n * Should this string be rendered as a tag instead of just text.\n */\n previewAsTag?: boolean;\n /**\n * Add an icon that sets the value to `null`. Defaults to `false`.\n *\n * Worth setting where empty and empty-string are different answers — an\n * unset middle name is not the same as one somebody deleted.\n */\n clearable?: boolean;\n /**\n * How to render a string that holds a URL: a link, or one of the supported\n * media types for an inline preview.\n *\n * Only presentation. Whether the string *is* a URL is `url` on the property\n * itself, which is what the OpenAPI contract is generated from.\n */\n urlPreview?: PreviewType;\n}\n\n/**\n * How a number is written out for reading.\n *\n * A thin, explicit subset of `Intl.NumberFormatOptions`. Explicit is the whole\n * point: nothing here is inferred. A collection that happens to carry a\n * `currency` column alongside a `total` column has not told us that one formats\n * the other — that is a relationship only the author knows, and guessing it\n * would put a euro sign on the one number that was never money.\n *\n * Presentation only. It changes what {@link PropertyPreview} renders — the\n * detail view, the table cell, a reference card — and never what the number\n * input holds, because a formatted string is not a number you can type into.\n */\nexport interface NumberFormatOptions {\n /** Defaults to `\"decimal\"`. `\"currency\"` requires {@link currency}. */\n style?: \"decimal\" | \"currency\" | \"percent\";\n /**\n * ISO 4217 code — `\"EUR\"`, `\"USD\"`. Setting it implies `style: \"currency\"`,\n * so the common case is one key.\n */\n currency?: string;\n /**\n * BCP 47 tag. Defaults to the panel's locale, which is what makes the same\n * amount read `1,234.50` for one user and `1.234,50` for another.\n */\n locale?: string;\n /** Pad to at least this many decimals — `2` writes `5` as `5.00`. */\n minimumFractionDigits?: number;\n /** Round to at most this many decimals. Does not change the stored value. */\n maximumFractionDigits?: number;\n /** `\"compact\"` renders `12000` as `12K`. Useful in narrow table columns. */\n notation?: \"standard\" | \"compact\";\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminNumberOptions extends AdminPropertyOptions {\n /**\n * Add an icon that sets the value to `null`. Defaults to `false`.\n *\n * Numbers are where this matters most: without it, clearing the input\n * leaves `0`, and \"no price\" and \"free\" become the same row.\n */\n clearable?: boolean;\n /**\n * Write this number out as currency, a percentage, or with fixed decimals.\n * Omit and the raw value renders, which stays the default: a number in the\n * database is shown as the number in the database.\n */\n format?: NumberFormatOptions;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminVectorOptions extends AdminPropertyOptions {\n /**\n * Add an icon that sets the embedding to `null`. Defaults to `false`.\n *\n * A vector is normally written by whatever generates it, so this is for the\n * case where a human needs to say \"this one is stale\" and let it be\n * recomputed.\n */\n clearable?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminDateOptions extends AdminPropertyOptions {\n /**\n * Add an icon to clear the value and set it to `null`. Defaults to `false`\n */\n clearable?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminReferenceOptions extends AdminPropertyOptions {\n /**\n * Which of the *target's* properties are shown in the chip that stands in\n * for the referenced entity. At most three fit; the rest are ignored.\n *\n * Defaults to the target collection's own `admin.previewProperties`, then to\n * a derived guess. Name them here when the referring context wants different\n * ones — an order line wants the product's SKU, the catalogue wants its\n * name.\n */\n previewProperties?: string[];\n\n /**\n * Offer only entities that pass this filter in the selection dialog.\n * e.g. `fixedFilter: { age: [\">=\", 18] }`\n */\n fixedFilter?: FilterValues<string>;\n\n /** Show the referenced entity's id in previews. Defaults to `true`. */\n includeId?: boolean;\n\n /** Show a link that opens the referenced entity. Defaults to `true`. */\n includeEntityLink?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminRelationOptions extends AdminPropertyOptions {\n /**\n * Which of the *target's* properties are shown in the chip that stands in\n * for the related row. At most three fit; the rest are ignored.\n *\n * Defaults to the target collection's own `admin.previewProperties`, then to\n * a derived guess. Name them here when this side wants different ones.\n */\n previewProperties?: string[];\n\n /**\n * Which widget selects the related entity. Defaults to `select`.\n */\n widget?: \"select\" | \"dialog\";\n\n /**\n * Offer only entities that pass this filter in the selection widget.\n * e.g. `fixedFilter: { age: [\">=\", 18] }`\n */\n fixedFilter?: FilterValues<string>;\n\n /** Show the related entity's id in previews. Defaults to `true`. */\n includeId?: boolean;\n\n /** Show a link that opens the related entity. Defaults to `true`. */\n includeEntityLink?: boolean;\n\n /**\n * Render a **many**-relation as a picker inside the entity form as well as\n * the tab it already gets. Defaults to `false`.\n *\n * The entity view lists a many-relation's rows as a tab, which is the whole\n * treatment: the child rows are a list, not a value the form holds. This\n * flag exists for the project that wants the inline picker anyway — it is\n * off by default because the two surfaces are redundant by construction.\n *\n * No effect on a to-one relation: a foreign key gets no tab, so its picker\n * is always rendered.\n */\n renderInForm?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminArrayOptions extends AdminPropertyOptions {\n /**\n * Open every element on load instead of collapsing them to one line each.\n * Defaults to `false`.\n *\n * Expanding is right for a short list of small elements and wrong for a long\n * one: twenty open cards is a form nobody can find the bottom of.\n */\n expanded?: boolean;\n /**\n * Drop the per-element chrome — the frame, the header, the index — and\n * render the children alone. Defaults to `false`.\n *\n * For an array of one simple field, where the chrome is most of the pixels.\n */\n minimalistView?: boolean;\n\n /**\n * Can the elements in this array be reordered by dragging. Defaults to\n * `true`. No effect when the property is disabled.\n */\n sortable?: boolean;\n\n /**\n * Can elements be added to this array. Defaults to `true`. No effect when\n * the property is disabled.\n */\n canAddElements?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminMapOptions extends AdminPropertyOptions {\n /**\n * Open the map's fields on load instead of collapsing them behind its\n * header. Defaults to `false`.\n */\n expanded?: boolean;\n /**\n * Drop the map's frame and header and render its fields alone. Defaults to\n * `false`.\n */\n minimalistView?: boolean;\n /**\n * Lay the map's fields out as if they were the parent's own, rather than\n * grouped inside it. Defaults to `false`.\n *\n * Presentation only — the values still nest under this property's key in the\n * row, and in every read the API serves. It is for a group that is a\n * grouping in the schema and not in the form: an address, a set of\n * dimensions.\n */\n spreadChildren?: boolean;\n\n /**\n * Which of the map's own properties are shown when it is rendered as a\n * preview. Defaults to all of them, in `propertiesOrder`.\n */\n previewProperties?: string[];\n}\n\n/**\n * @group Entity properties\n */\nexport interface PropertyDisabledConfig {\n /**\n * Enable this flag if you would like to clear the value of the field\n * when the corresponding property gets disabled.\n *\n * This is useful for keeping data consistency when you have conditional\n * properties.\n */\n clearOnDisabled?: boolean;\n\n /**\n * Explanation of why this property is disabled (e.g. a different field\n * needs to be enabled)\n */\n disabledMessage?: string;\n\n /**\n * Set this flag to true if you want to hide this field when disabled\n */\n hidden?: boolean;\n}\n\n/**\n * Used for previewing urls if the download file is known\n * @group Entity properties\n */\nexport type PreviewType = \"image\" | \"video\" | \"audio\" | \"file\";\n\n/**\n * Every key any property `admin` block accepts, across the base options and the\n * per-type extensions.\n */\ntype AnyAdminPropertyOptionKey =\n | keyof AdminPropertyOptions\n | keyof AdminStringOptions\n | keyof AdminNumberOptions\n | keyof AdminVectorOptions\n | keyof AdminDateOptions\n | keyof AdminReferenceOptions\n | keyof AdminRelationOptions\n | keyof AdminArrayOptions\n | keyof AdminMapOptions;\n\n/**\n * Core's list, re-exported through the same `satisfies` agreement check that\n * {@link ADMIN_COLLECTION_KEYS} gets: core owns the data because the boot-time\n * collection validator in `@rebasepro/server` needs it and may not import this\n * package, and this clause is what stops the data from drifting off the types.\n */\nexport const ADMIN_PROPERTY_KEYS = CORE_ADMIN_PROPERTY_KEYS satisfies readonly AnyAdminPropertyOptionKey[];\n\n/**\n * And the reverse direction: an option key these types declare that core's list\n * does not name.\n *\n * The `satisfies` above only closes one side. This one matters since the boot\n * validator started warning about unrecognised keys inside a property's `admin`\n * block: an option missing from the list would make the server call a correct\n * config a typo, and a check that cries wolf is a check people turn off.\n */\ntype _EveryAdminPropertyOptionIsListed =\n AssertNeverPropertyKey<Exclude<AnyAdminPropertyOptionKey, typeof CORE_ADMIN_PROPERTY_KEYS[number]>>;\n\n/** Compiles only when `T` is `never`. */\ntype AssertNeverPropertyKey<T extends never = never> = T;\n","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 * `defineCollection` — the admin-aware builder, in a module a backend can load.\n *\n * This is the function every scaffolded collection file imports, and it must be\n * reachable from a Node process that has no React and no DOM. So it lives here,\n * apart from `admin_collection.ts` (which describes the panel's option types and\n * names `React` throughout) and well away from `collections.ts` (the panel's\n * view models, which import React as a value).\n *\n * The side-effect import below is the other half of what the import buys you:\n * `augment.ts` is what declares `admin` on `BaseCollectionConfig` and on every\n * property type, so importing this builder brings the block's type-checking with\n * it. It is types only, and compiles to nothing.\n */\n// Side-effect import: this is what adds `admin` back onto the core types.\nimport \"./augment\";\n\nimport type {\n FirebaseCollectionConfig,\n FirebaseProperties,\n FirebaseProperty,\n InferEntityType,\n MongoDBCollectionConfig,\n MongoProperties,\n MongoProperty,\n PostgresCollectionConfig,\n PostgresProperties,\n PostgresProperty,\n Properties,\n Property,\n StrictProperties,\n User,\n CollectionConfig\n} from \"@rebasepro/types\";\nimport { resolveResourceRefs, type ResourceRef } from \"@rebasepro/types\";\n\n/**\n * The engines a collection can name. Absent means Postgres.\n *\n * The discriminant that replaced three overloads of `defineCollection`. See\n * {@link CollectionConfigForEngine} for why that mattered.\n */\ntype CollectionEngine = \"postgres\" | \"firestore\" | \"mongodb\";\n\n/**\n * The concrete collection type an `engine` selects.\n *\n * `defineCollection` used to be three overloads — one per engine — and overload\n * resolution is what made its errors unreadable. When no overload matches,\n * TypeScript emits **one** diagnostic at the call site listing each overload's\n * *first* failure, so:\n *\n * - a bad `defaultValue` **and** a misspelled `admin.display.title` in the same\n * collection reported only the first. Fixing it revealed the second on the\n * next run, one per edit-compile cycle;\n * - the error landed on `defineCollection(`, not on the key that was wrong;\n * - and every Postgres collection's error dragged `FirebaseCollectionConfig`\n * and `MongoDBCollectionConfig` through the message, naming two engines the\n * author had not mentioned and does not use.\n *\n * With one signature there is no resolution to fail: each error is reported\n * where it is, all of them at once, against the one config type the `engine`\n * selects.\n */\ntype CollectionConfigForEngine<E, P, USER extends User> =\n E extends \"firestore\" ? FirebaseCollectionConfig<EntityShapeOf<P>, USER>\n : E extends \"mongodb\" ? MongoDBCollectionConfig<EntityShapeOf<P>, USER>\n : PostgresCollectionConfig<EntityShapeOf<P>, USER>;\n\n/**\n * `InferEntityType`, tolerant of a property map that has an error in it.\n *\n * `P` is deliberately **unconstrained** on the builder, and this is why. A\n * constraint TypeScript cannot satisfy is a constraint it silently falls back\n * from: one property with a bad `defaultValue` made `P extends PostgresProperties`\n * fail, `P` became `PostgresProperties`, `M` became `Record<string, unknown>`,\n * and every `admin` key — `display.title`, `listProperties`, `propertiesOrder` —\n * widened to `string` and stopped being checked. So a collection with two\n * mistakes reported one, and reported the second only after the first was fixed.\n *\n * With no constraint, `keyof P` survives a bad property and the `admin` block is\n * still checked against the real key set. Exactness and the engine gate move\n * into `StrictProperties`, which reports them on the property itself.\n */\ntype EntityShapeOf<P> = InferEntityType<{\n [K in keyof P]: P[K] extends Property ? P[K] : Property;\n}>;\n\n/** The property union an engine admits — the engine gate, as a type. */\ntype PropertyForEngine<E> =\n E extends \"firestore\" ? FirebaseProperty\n : E extends \"mongodb\" ? MongoProperty\n : PostgresProperty;\n\n/** {@link PropertyForEngine} as a property map, for the `P` constraint. */\ntype PropertiesForEngine<E> =\n E extends \"firestore\" ? FirebaseProperties\n : E extends \"mongodb\" ? MongoProperties\n : PostgresProperties;\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.display`, `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 * display: { title: \"title\" }, // completion: \"title\" | \"status\"\n * listProperties: [\"title\", \"status\"]\n * }\n * });\n *\n * @group Builder\n */\nexport function defineCollection<\n const E extends CollectionEngine = \"postgres\",\n /**\n * The properties, **constrained**. This is what checks them, and — just as\n * importantly — what supplies the contextual type inside them: without a\n * constraint the parameter of an inline\n * `callbacks: { beforeSave: ({ value }) => … }` has nothing to be typed\n * from, and TypeScript reports an implicit `any` on a callback the author\n * wrote correctly.\n */\n const P extends PropertiesForEngine<E> & Properties = PropertiesForEngine<E> & Properties,\n /**\n * The properties again, **unconstrained**, and this is why there are two.\n *\n * A constraint TypeScript cannot satisfy is one it silently falls back\n * from: one property with a bad `defaultValue` made `P` become\n * `PostgresProperties`, the entity shape become `Record<string, unknown>`,\n * and every `admin` key — `display.title`, `listProperties`,\n * `propertiesOrder` — widen to `string` and stop being checked. A\n * collection with two mistakes reported one, and revealed the second only\n * after the first was fixed.\n *\n * `KEYS` has no constraint to fall back from, so `keyof KEYS` survives a bad\n * property and the `admin` block is still checked against the real key set.\n */\n const KEYS = Properties,\n USER extends User = User\n>(\n collection: Omit<CollectionConfigForEngine<E, KEYS, USER>, \"properties\" | \"engine\" | \"dataSource\">\n & {\n engine?: E;\n properties: StrictProperties<P, PropertyForEngine<E>> & KEYS;\n dataSource?: ResourceRef;\n }\n): CollectionConfigForEngine<E, KEYS, USER> & { properties: KEYS };\n\n/**\n * At runtime this records the collection as data: a resource handle written\n * where a key belongs — `dataSource: analytics`, `storageSource: media` — is\n * replaced by its key, so what leaves here serialises and compares like the\n * string it always was. The signature above is the rest of the point.\n * @group Builder\n */\nexport function defineCollection(\n collection: Omit<CollectionConfig, \"dataSource\"> & { dataSource?: ResourceRef }\n): CollectionConfig {\n return resolveResourceRefs(collection) as CollectionConfig;\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 CollectionCallbacks,\n CollectionConfig,\n ComponentRef,\n FilterPreset,\n FilterValues,\n OrderBySpec,\n PostgresCollectionConfig,\n Property,\n Properties,\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, nestAdminCollectionKeys } 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 { CollectionCustomView } from \"./types/collection_views\";\nimport type { EntityDisplay } from \"./types/entity_display\";\nimport type { FormLayoutConfig } from \"./types/form_layout\";\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 * The `display` block for a collection, with its property paths checked\n * against `M`.\n *\n * `EntityDisplay` is generic over the path type so that\n * `@rebasepro/cms-types`' two halves do not import each other in a cycle;\n * this alias is what an authoring site actually names.\n */\nexport type CollectionDisplay<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = EntityDisplay<PropertyPath<M>, M, USER>;\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 * Lifecycle callbacks that run **in the browser**, in the admin panel.\n *\n * The twin of the collection's top-level `callbacks`, and the distinction is\n * only where the code runs — the shape is identical:\n *\n * - `callbacks` runs on the server, on every path that reaches it (REST,\n * realtime, `dataAsAdmin`). Its bodies are stripped from the admin bundle,\n * so a secret read there never leaves the server.\n * - `browserCallbacks` runs in the panel, and nowhere else. It ships to\n * every visitor.\n *\n * This exists for collections on a `direct` or `custom` transport — a\n * Firestore collection the panel talks to itself, with no Rebase server in\n * the request path. Nothing server-side sees those writes, so `callbacks`\n * can never fire for them; this block is the only place their lifecycle\n * logic can live.\n *\n * Two rules follow from \"ships to every visitor\", and neither is a style\n * preference:\n *\n * 1. **No secrets.** No API keys, no `process.env`, no logic you would mind\n * a reader of the bundle seeing. Put that in `callbacks`.\n * 2. **Not a security boundary.** A `browserCallbacks.afterRead` that\n * redacts a field redacts it *after* the browser already holds the row —\n * for a direct transport the raw document came straight from the store.\n * It is presentation. Redaction that has to hold belongs in `callbacks`,\n * or in the store's own rules.\n *\n * On a server-transport collection (the default) the server has already run\n * `callbacks` before the row arrives, so a `browserCallbacks.afterRead`\n * here runs *in addition* — write it to be idempotent, or don't write it.\n *\n * ```ts\n * admin: {\n * browserCallbacks: {\n * afterRead: ({ row }) => ({ ...row, label: `${row.city} (${row.code})` })\n * }\n * }\n * ```\n */\n browserCallbacks?: CollectionCallbacks<M, USER>;\n\n /**\n * How a record of this collection shows up — its title, subtitle, image,\n * status, date and tags.\n *\n * Each role takes a property path or a resolver, and a resolver may be\n * async:\n *\n * ```ts\n * display: {\n * title: \"name\",\n * image: \"cover.url\",\n * subtitle: ({ entity }) => `${entity.values.city}, ${entity.values.country}`,\n * status: async ({ entity, context }) =>\n * (await context.data.audits.get(`${entity.id}/latest`))?.state\n * }\n * ```\n *\n * Every role left out is derived from the property schema exactly as before,\n * so a collection that says nothing renders as it always did. See\n * {@link EntityDisplay} for what each role means and\n * {@link EntityDisplayResolver} for what a resolver is handed.\n */\n readonly display?: EntityDisplay<PropertyPath<M>, M, USER>;\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 * How the generated form is laid out: which properties are grouped into\n * sections in the main column, and which are pulled out into the metadata\n * rail beside it.\n *\n * Entirely optional. With no `form` block the layout is derived from the\n * properties themselves — see {@link FormLayoutConfig} — which is what most\n * collections should rely on. Reach for this when the derived grouping is\n * wrong for your domain, not to restate it.\n *\n * Unlike {@link FormViewConfig}, this does not replace the generated form:\n * every field keeps its validation, error focus, local-changes restore and\n * autosave wiring.\n */\n form?: FormLayoutConfig<M>;\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 admin 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 *\n * One key, or several applied in order of significance — the second breaks\n * ties on the first, and so on. The row id breaks the last tie, so the\n * order is total and paging over it neither repeats nor skips rows.\n *\n * @example sort: [\"order\", \"asc\"]\n * @example sort: [[\"roles\", \"asc\"], [\"createdAt\", \"desc\"]]\n */\n readonly sort?: OrderBySpec<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 * Note that this covers *both* roles at once. A collection that is a root\n * collection **and** the target of a many-relation is hidden in both places,\n * which is rarely what you want for a join or audit table: it should not be\n * a destination in the drawer, but it is exactly what you want to see on its\n * parent. Use {@link hideFromEntityViews} to separate the two.\n */\n hideFromNavigation?: boolean;\n\n /**\n * Should this collection be hidden from the tab strip of a parent entity,\n * when it is reached as a child view (a Firestore subcollection, or the\n * target of a `many`-cardinality relation).\n *\n * Independent of {@link hideFromNavigation}, which governs the drawer. The\n * two exist separately because a collection commonly plays both roles and\n * wants a different answer for each:\n *\n * - a join table (`company_members`) is not a destination but *is* a\n * meaningful tab → `hideFromNavigation: true`, this left unset;\n * - a table with a dedicated workspace (`scraped_jobs`) may want the\n * opposite, so the workspace stays the only way in.\n *\n * Defaults to `false`. Setting {@link hideFromNavigation} does not imply it.\n */\n hideFromEntityViews?: 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 * - \"list\": Display entities as a list (default)\n * - \"table\": Display entities in a table with inline editing\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * - any `key` from {@link customViews}\n * Defaults to \"list\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"list\", \"table\", \"cards\", \"kanban\", and any `key` from\n * {@link customViews}.\n * Defaults to all four built-ins plus every declared custom view.\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 * With a single entry the view switcher is hidden.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Additional ways to render this collection's rows, offered in the view\n * switcher beside list / table / cards / kanban.\n *\n * Can be an array of `CollectionCustomView` or a string naming the `key` of\n * one registered globally on `<RebaseCMS collectionViews={…}>`. The\n * string form is what lets a React-free config package reference React UI,\n * and it is what the collection editor stores.\n *\n * A custom view is another rendering of the *same query* — it is handed the\n * live table controller and inherits filters, search and the entity side\n * panel. Use an `AppView` instead for a workflow spanning collections.\n *\n * @example\n * ```ts\n * admin: {\n * customViews: [\n * { key: \"map\", name: \"Map\", icon: \"Map\", Builder: MapView }\n * ],\n * enabledViews: [\"table\", \"map\"],\n * defaultViewMode: \"map\"\n * }\n * ```\n */\n customViews?: (string | CollectionCustomView<Record<string, unknown>>)[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n *\n * A board is only half-configured without {@link orderProperty}. Cards\n * still drag between columns — that writes `columnProperty` — but their\n * order *within* a column has nowhere to be stored, so it resets on the\n * next read and the board renders a warning bar saying so. Declare both,\n * always.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n *\n * Must reference a **string** property — a `number` can never hold one of\n * these keys, so a numeric `sortOrder` leaves the board permanently asking\n * to be initialised. The convention across the collections here is a\n * hidden `__order: { type: \"string\", admin: { disabled: true,\n * hideFromCollection: true } }`.\n *\n * Reordering writes a `fractional-indexing` key built from the base36,\n * lower-case alphabet `0123456789abcdefghijklmnopqrstuvwxyz` — `\"i0\"`,\n * `\"i1\"`, `\"i0i\"`. Single case because *Postgres* does the sorting and its\n * default collation is not byte ordering; base36 rather than the library's\n * default base62 for the same reason. Generating a key without passing\n * that alphabet yields base62 keys (`\"a0\"`), which this board rejects.\n *\n * Nothing assigns a key on insert. A row created by a cron, a seed, a\n * migration or the REST API lands with this property null, and the board\n * shows an **Initialize** bar until someone clicks it. Backends that\n * create rows for a board should append a key themselves — see the\n * \"Kanban boards\" section of the `rebase-collections` skill.\n *\n * Used by Kanban view for ordering within columns and can be used for\n * 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 * An entry may be the action itself, or the `key` of one registered app-level\n * on `<RebaseCMS entityActions={…}>` — `resolveEntityAction` looks a string\n * up against that list.\n *\n * The key form is what lets a collection declared in a React-free config\n * package use an action whose UI is React: an action carries an `onClick` and\n * usually renders a dialog, so importing one into a collection file pulls the\n * admin bundle into any backend that loads it for its schema. Naming it costs\n * nothing there.\n *\n * `string` was accepted at runtime and by the collection editor — which stores\n * exactly these keys — long before the type said so, which meant the documented\n * approach needed a cast. Mirrors `entityViews`, typed this way already.\n */\n entityActions?: (string | 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/cms-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 * `defineCollection` lives in `./define_collection`, and this is why.\n *\n * A collection file is loaded by the backend as well as by the panel, so the\n * builder it imports has to be reachable without React. This module names\n * `React` throughout — it describes the panel's option types — so the builder\n * sits apart from it and the barrel exports both.\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/** Compiles only when `T` is `never` — see {@link _EveryAdminCollectionOptionIsListed}. */\ntype AssertNeverKey<T extends never = never> = T;\n\n/** Local alias, so the assertion below can name the list's element type. */\ntype AdminCollectionKeyName = typeof CORE_ADMIN_COLLECTION_KEYS[number];\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 */\nexport const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];\n\n/**\n * And the reverse: an option declared here that core's list does not name.\n *\n * This direction was believed to have no type-level expression — `keyof` over\n * optional properties does in fact yield them all, so it does. A test counted\n * the list instead, which catches a *change* in size but not a key added to the\n * options and forgotten here.\n *\n * It matters more since the boot validator started warning about unrecognised\n * `admin` keys: a real option missing from this list would make the server\n * report a correct config as a typo, and a check that cries wolf gets switched\n * off. The compile error arrives at the person adding the option, which is the\n * only moment anyone can fix it cheaply.\n */\ntype _EveryAdminCollectionOptionIsListed =\n AssertNeverKey<Exclude<keyof AdminCollectionOptions, AdminCollectionKeyName>>;\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 *\n * The nesting itself lives in `@rebasepro/types` because the schema editor in\n * `@rebasepro/server` — which cannot import this package — has to do exactly the\n * same thing when it writes a collection file back to disk. Two copies of the\n * rule disagreed on which side wins, and the disagreement was invisible.\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 return nestAdminCollectionKeys(collection as Record<string, unknown>) as unknown as CollectionConfig<M, USER>;\n}\n","/**\n * How a record shows up: its title, image, subtitle, status, date and tags.\n *\n * Every surface that draws a record draws some subset of these six roles. A list\n * row is image + title + subtitle + status + date; a card is the same with the\n * image on top; a board card drops the image; a reference picker is title +\n * subtitle; a page heading is the title alone. The roles are stable — what fills\n * them is not.\n *\n * Before this block, the roles were derived and only derived. `titleProperty`\n * was the single exception, and it could only ever name a property of the\n * collection: seven separate implementations read that key, disagreed about the\n * fallback, and none of them could await. (It is gone now — `display.title`\n * replaced it outright.) The other five roles could not be\n * stated at all — the image was whichever storage property came first, the\n * status whichever enum, the date whichever timestamp. Right often enough to\n * feel automatic, and wrong with no way to say so.\n *\n * So: one mechanism, six roles, two forms each.\n *\n * ```ts\n * admin: {\n * display: {\n * title: \"name\", // a property path\n * image: \"photos.0\", // a dotted path\n * subtitle: ({ entity }) => // computed\n * `${entity.values.city}, ${entity.values.country}`,\n * status: async ({ entity, context }) => { // and may be async\n * const latest = await context.data.audits.get(`${entity.id}/latest`);\n * return latest?.state;\n * }\n * }\n * }\n * ```\n *\n * Anything left out is derived exactly as it is today, so an existing collection\n * renders identically, and a collection that needs one role fixed states that\n * one role.\n */\nimport type { Entity, User } from \"@rebasepro/types\";\nimport type { RebaseContext } from \"../rebase_context\";\n\n/**\n * What a resolver is handed.\n *\n * The whole {@link RebaseContext}, matching `AdditionalFieldDelegate.value` and\n * `EntityAction.onClick` — so a resolver can reach `context.data` and\n * `context.client` and read anything the panel can read, including a document in\n * a subcollection that the entity itself never loads.\n */\nexport type EntityDisplayResolverParams<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n entity: Entity<M>;\n context: RebaseContext<USER>;\n};\n\n/**\n * Computes what fills one display role for one record.\n *\n * May be async. While a promise is in flight the surface shows the derived value\n * and swaps the resolved one in when it lands — a title is never a spinner.\n * Results are cached per record and per role, and concurrent calls for the same\n * pair share one execution, so a list of fifty rows resolves each row once\n * rather than once per render.\n *\n * Return `undefined` when this record has nothing for this role. Do not return a\n * placeholder: the surface's own fallback is better informed than the resolver\n * is about what belongs there instead — a heading wants the collection's\n * singular name, a link wants the id.\n *\n * A resolver that throws is treated as `undefined` and logged once. A title that\n * cannot be fetched must not take down the row that shows it.\n */\nexport type EntityDisplayResolver<\n M extends Record<string, unknown> = Record<string, unknown>,\n T = unknown,\n USER extends User = User\n> = {\n /**\n * Declared as a *method* and then indexed back out, which is the only way to\n * write a standalone function type whose parameters stay bivariant.\n *\n * Not a style choice. `AdminCollectionOptions<M>` has to remain assignable\n * to `AdminCollectionOptions<Record<string, unknown>>` — every consumer that\n * takes a collection it did not author depends on it, and losing it breaks\n * `defineCollection`'s own overloads. A resolver takes `Entity<M>` in\n * parameter position, so written as `(params) => …` it makes the entire\n * admin block invariant in `M`, and a typed collection stops being usable as\n * a collection. Method syntax is bivariant under `strictFunctionTypes`; the\n * sibling callbacks (`EntityAction.onClick`, `AdditionalFieldDelegate.value`)\n * are all written this way, and `packages/types/__tests__/bivariance` is the\n * record of finding it out the hard way.\n */\n resolve(params: EntityDisplayResolverParams<M, USER>): T | undefined | Promise<T | undefined>;\n}[\"resolve\"];\n\n/**\n * Where one display role gets its value: a property path on this collection, or\n * a resolver that computes it.\n *\n * The path arm is checked against `M` and read with `getValueInPath`, so\n * `\"profile.displayName\"` is as valid as `\"title\"`. It also keeps the property's\n * own rendering — an enum status stays a coloured chip, a date stays formatted,\n * a storage path stays a thumbnail — which a resolver returning a bare string\n * cannot. Prefer the path whenever the value is on the record.\n *\n * `Path` is a type parameter rather than `PropertyPath<M>` directly, so this\n * module does not import from `admin_collection`, which imports it.\n */\nexport type EntityDisplaySource<\n Path extends string,\n M extends Record<string, unknown> = Record<string, unknown>,\n T = unknown,\n USER extends User = User\n> = Path | EntityDisplayResolver<M, T, USER>;\n\n/**\n * The six roles, and what may fill each.\n *\n * The value types describe what the renderers accept, not what a resolver must\n * produce exactly: a `date` resolver may return a `Date`, an ISO string or an\n * epoch number, and `tags` takes a single string as shorthand for one tag. A\n * property path is not constrained by them at all — the property's own type\n * decides how it renders.\n */\nexport type EntityDisplay<\n Path extends string = string,\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n /**\n * What the record is called: the heading, the breadcrumb, the row, and the\n * label of every relation chip and reference that points at it.\n */\n title?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The line under the title — a short description, a location, a summary.\n */\n subtitle?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The record's picture. A storage path or a URL: the same two things a\n * `storage` property holds, so a resolver may return either.\n */\n image?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The state chip — published, archived, paid. Rendered with the enum's own\n * colour when it comes from an enum property.\n */\n status?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The timestamp a row is stamped with, usually when it last changed.\n */\n date?: EntityDisplaySource<Path, M, Date | string | number, USER>;\n\n /**\n * Free chips beside the status: labels, categories, topics. A single string\n * is accepted as shorthand for one tag.\n */\n tags?: EntityDisplaySource<Path, M, string[] | string, USER>;\n};\n\n/**\n * The roles as data, so every consumer iterates the same list instead of\n * repeating it — the mistake that let `titleProperty` grow seven readers.\n */\nexport const ENTITY_DISPLAY_ROLES = [\n \"title\",\n \"subtitle\",\n \"image\",\n \"status\",\n \"date\",\n \"tags\"\n] as const;\n\n/** One of the six display roles. */\nexport type EntityDisplayRole = typeof ENTITY_DISPLAY_ROLES[number];\n","import type { ColumnKey } from \"../admin_collection\";\n\n/**\n * The number of columns the form grid is divided into. A field's\n * {@link AdminPropertyOptions.span} is expressed against this.\n *\n * Fixed rather than configurable on purpose: the whole point of a span is that\n * two fields written by two different people line up, and they only do that if\n * everyone is counting against the same grid.\n *\n * @group Models\n */\nexport const FORM_GRID_COLUMNS = 4;\n\n/**\n * How wide a field sits on the {@link FORM_GRID_COLUMNS}-column form grid.\n *\n * `4` is the full width of the main column. A field always takes at least a\n * whole row on narrow layouts (the side panel, the split pane, mobile), where\n * the grid collapses to one column and spans are ignored.\n *\n * @group Entity properties\n */\nexport type PropertySpan = 1 | 2 | 3 | 4;\n\n/**\n * A titled group of fields in the main column of the form.\n *\n * @group Models\n */\nexport interface FormSection<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identity for this section. Used as the React key and to remember\n * the collapsed state across visits, so renaming `title` does not lose it.\n */\n key: string;\n\n /**\n * Shown above the group. A section with no title renders its fields with no\n * heading and no rule — useful for the first group, which rarely needs one.\n */\n title?: string;\n\n /**\n * Property and additional-field keys in this section, in render order.\n *\n * Keys naming a property that does not exist, is hidden, or has been routed\n * to {@link FormLayoutConfig.sidebar} are skipped. Any property *not* named\n * by a section lands in the last section that has no explicit title, or in\n * an untitled trailing group if there is none — a new column is never\n * silently dropped from the form.\n */\n properties: ColumnKey<M>[];\n\n /**\n * Start collapsed. Defaults to `false`.\n *\n * Only meaningful when the section can be collapsed at all; a section with\n * no `title` has nothing to click, so this is ignored there.\n */\n collapsed?: boolean;\n\n /**\n * Can the user collapse this section. Defaults to `true` for a titled\n * section, `false` for an untitled one.\n *\n * A section holding a required field is still collapsible — but a\n * validation error inside a collapsed section expands it, so an error can\n * never hide.\n */\n collapsible?: boolean;\n\n /**\n * How this section arranges itself in the **read-only** view of a record.\n * Defaults to `\"grid\"` — the same grid the form uses.\n *\n * `\"summary\"` stacks the fields as right-aligned label/value rows with the\n * last one emphasised, which is what a run of related figures wants: a\n * subtotal, a tax, a discount and a total are one calculation, and four\n * equal cells on a four-column grid is the one arrangement that says they\n * are unrelated. Opt in per section — nothing about a group of numbers tells\n * us it adds up, so this is never derived.\n *\n * Read-only only, and named for it. The form goes on rendering the grid:\n * a summary row is a reading arrangement, and shrinking a control to fit one\n * would make the fields harder to edit to make them prettier to skim.\n */\n readVariant?: \"grid\" | \"summary\";\n}\n\n/**\n * How the generated form is laid out.\n *\n * Everything here is optional, and the defaults are the point: with no config\n * at all the layout is derived from the properties themselves —\n *\n * - the id and the `createdAt`/`updatedAt` timestamps go to the rail, read-only\n * - short enums, booleans, dates and numbers take a narrow span\n * - long text, markdown, arrays, maps and storage fields take the full width\n * - everything else takes half\n *\n * so a collection that never mentions `form` still gets a two-column layout\n * rather than one flat run of full-width fields. Use this block when the\n * derived answer is wrong for your domain.\n *\n * @group Models\n */\nexport interface FormLayoutConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Property keys shown in the metadata rail beside the main column instead\n * of in it — status, ownership, publication dates, flags.\n *\n * The rail is narrow and does not use the grid, so `span` is ignored for\n * these. On layouts too narrow for a rail (the side panel, the split pane,\n * mobile) they render as an ordinary leading section, so nothing is lost.\n *\n * Set to `[]` to suppress the derived rail entirely and keep every field in\n * the main column.\n */\n sidebar?: ColumnKey<M>[];\n\n /**\n * Groups for the main column. When omitted, every field lands in a single\n * untitled group, which is the pre-existing behaviour.\n */\n sections?: FormSection<M>[];\n\n /**\n * Show the read-only record block (id, created, updated) at the foot of the\n * rail. Defaults to `true` when a rail is shown.\n *\n * This is what replaces `hideIdFromForm` for most collections: the id stops\n * being a field in the middle of the form and becomes a copyable line of\n * metadata.\n */\n showRecordMeta?: boolean;\n}\n","import React from \"react\";\n\nimport type { CollectionActionsProps, EntityTableController, SelectionController } from \"../collections\";\nimport type { Entity } from \"@rebasepro/types\";\nimport type { PluginFormActionProps, PluginGenericProps, PluginHomePageActionsProps, PluginHomePageAdditionalCardsProps } from \"./plugins\";\nimport type { Property } from \"@rebasepro/types\";\nimport type { RebaseContext } from \"../rebase_context\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\n\n/**\n * Registry mapping slot names to their component prop types.\n * Each key represents a UI extension point in the admin.\n * @group Plugins\n */\nexport interface SlotRegistry {\n // ── Home page ─────────────────────────────────────────────────────\n \"home.actions\": PluginGenericProps;\n \"home.cards\": PluginHomePageAdditionalCardsProps;\n \"home.children.start\": PluginGenericProps;\n \"home.children.end\": PluginGenericProps;\n /** Compact widget rendered inline in a home page collection card. */\n \"home.card.widget\": HomeCardWidgetSlotProps;\n \"home.collection.actions\": PluginHomePageActionsProps;\n\n // ── Navigation / Drawer ───────────────────────────────────────────\n /** Rendered below the logo in the sidebar drawer. */\n \"navigation.header\": NavigationSlotProps;\n /** Rendered above the collapse toggle at the bottom of the drawer. */\n \"navigation.footer\": NavigationSlotProps;\n\n // ── Collection view ───────────────────────────────────────────────\n \"collection.actions\": CollectionActionsProps;\n \"collection.actions.start\": CollectionActionsProps;\n \"collection.header.action\": CollectionHeaderActionProps;\n \"collection.add-column\": CollectionAddColumnProps;\n \"collection.error\": CollectionErrorProps;\n /** Extra widgets rendered inside the collection toolbar row. */\n \"collection.toolbar\": CollectionToolbarProps;\n /** Custom empty-state component when a collection has no data. */\n \"collection.empty-state\": CollectionEmptyStateProps;\n /** Widgets rendered above the collection table. */\n \"collection.widgets\": CollectionWidgetsSlotProps;\n\n // ── Entity / Form ─────────────────────────────────────────────────\n \"form.actions\": PluginFormActionProps;\n \"form.actions.top\": PluginFormActionProps;\n /** Rendered before the form title / field list. */\n \"form.before\": PluginFormActionProps;\n /** Rendered after the form field list. */\n \"form.after\": PluginFormActionProps;\n\n // ── Entity row actions ────────────────────────────────────────────\n /** Per-row actions in entity tables (e.g. bulk actions, row context menus). */\n \"entity.row.actions\": EntityRowActionsProps;\n\n // ── Entity field decoration ───────────────────────────────────────\n /** Inject UI before an individual form field. */\n \"entity.field.before\": EntityFieldSlotProps;\n /** Inject UI after an individual form field. */\n \"entity.field.after\": EntityFieldSlotProps;\n\n // ── Collection filter panel ───────────────────────────────────────\n /** Custom filter sidebar for a collection. */\n \"collection.filter-panel\": CollectionFilterPanelProps;\n\n // ── Dashboard ─────────────────────────────────────────────────────\n /** Widget rendered on the dashboard / home page. */\n \"dashboard.widget\": DashboardWidgetProps;\n\n // ── Global ────────────────────────────────────────────────────────\n /** Cross-collection search bar component. */\n \"global.search\": GlobalSearchProps;\n /** Top-level toolbar actions rendered in the shell toolbar area. */\n \"shell.toolbar\": ShellToolbarProps;\n\n // ── Kanban ────────────────────────────────────────────────────────\n \"kanban.setup\": KanbanSetupProps;\n \"kanban.add-column\": KanbanAddColumnProps;\n}\n\n/**\n * Valid slot names for UI extension points.\n * @group Plugins\n */\n/**\n * Slots this build declares but renders nowhere.\n *\n * Every name here appears in {@link SlotRegistry}, has a props interface, and\n * is listed in the public slot reference alongside the ones that work — so a\n * plugin author picks one off the table, registers a component, sees nothing,\n * and has no way to tell whether the fault is theirs. Seven of twenty-nine were\n * in that state.\n *\n * This is a statement of fact, not a wish list: `slot-render-sites.test.ts`\n * derives the same set by scanning for render sites and fails when the two\n * disagree. Implementing a slot therefore forces its removal from here, and\n * declaring one without rendering it forces its addition — at which point\n * `Rebase` warns anyone who registers for it, which is the whole point.\n */\nexport const UNRENDERED_SLOTS = [\n \"collection.filter-panel\",\n \"dashboard.widget\",\n \"entity.field.after\",\n \"entity.field.before\",\n \"entity.row.actions\",\n \"global.search\",\n \"shell.toolbar\"\n] as const satisfies readonly (keyof SlotRegistry)[];\n\nexport type SlotName = keyof SlotRegistry;\n\n/**\n * A single UI component contribution to a named slot.\n * @group Plugins\n */\nexport interface SlotContribution<K extends SlotName = SlotName> {\n /**\n * Which slot to contribute to.\n */\n slot: K;\n\n /**\n * The component to render in the slot, taking that slot's props.\n *\n * This was `React.ComponentType<any>`, \"typed loosely so mixed-slot arrays\n * work\" — and the looseness was doing real damage: `{ slot:\n * \"collection.actions\", Component: MyThing }` typechecked whatever\n * `MyThing`'s props were, so a component written against the wrong slot's\n * props compiled, registered, and then read `undefined` off every prop it\n * expected. The only check was at the `useSlot` render site, which is\n * inside the framework and reports nothing to the author.\n *\n * The mixed-array problem is real but is a problem with the *array*, not\n * with this field: see {@link AnySlotContribution}, which distributes over\n * the slot names so each element is checked against its own slot.\n */\n Component: React.ComponentType<SlotRegistry[K]>;\n\n /**\n * Additional props to merge into the slot props before rendering.\n */\n props?: Record<string, unknown>;\n\n /**\n * Ordering hint. Lower values render first. Defaults to 50.\n */\n order?: number;\n}\n\n/**\n * A contribution to *some* slot, checked against that slot.\n *\n * `SlotContribution[]` cannot be the type of a mixed list: with `K` left at its\n * default the props become the union of every slot's, and a component is\n * contravariant in its props, so nothing satisfies it. Distributing the union\n * over the slot names instead gives one member per slot, and TypeScript picks\n * the member whose `slot` matches — which is what makes\n * `{ slot: \"collection.actions\", Component: WrongProps }` an error at the\n * declaration rather than a silent `undefined` at render.\n *\n * @group Plugins\n */\nexport type AnySlotContribution = { [K in SlotName]: SlotContribution<K> }[SlotName];\n\n// ── Prop interfaces for slots ─────────────────────────────────────────\n\n/**\n * Props for `navigation.header` and `navigation.footer` slots.\n * @group Plugins\n */\nexport interface NavigationSlotProps {\n drawerOpen: boolean;\n drawerHovered: boolean;\n context: RebaseContext;\n}\n\n/**\n * Props for the `collection.toolbar` slot.\n * @group Plugins\n */\nexport interface CollectionToolbarProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n tableController: EntityTableController;\n selectionController: SelectionController;\n}\n\n/**\n * Props for the `collection.empty-state` slot.\n * @group Plugins\n */\nexport interface CollectionEmptyStateProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n canCreate: boolean;\n onNewClick?: () => void;\n}\n\n/**\n * Props for the `collection.header.action` slot.\n * @group Plugins\n */\nexport interface CollectionHeaderActionProps {\n property: Property;\n propertyKey: string;\n path: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n onHover: boolean;\n collection: AdminCollection;\n tableController: EntityTableController;\n}\n\n/**\n * Props for the `collection.add-column` slot.\n * @group Plugins\n */\nexport interface CollectionAddColumnProps {\n path: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n collection: AdminCollection;\n tableController: EntityTableController;\n}\n\n/**\n * Props for the `collection.error` slot.\n * @group Plugins\n */\nexport interface CollectionErrorProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n error: Error;\n}\n\n/**\n * Props for the `kanban.setup` slot.\n * @group Plugins\n */\nexport interface KanbanSetupProps {\n collection: AdminCollection;\n fullPath: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n}\n\n/**\n * Props for the `kanban.add-column` slot.\n * @group Plugins\n */\nexport interface KanbanAddColumnProps {\n collection: AdminCollection;\n fullPath: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n columnProperty: string;\n}\n\n// ── New slot prop interfaces ──────────────────────────────────────────\n\n/**\n * Props for `entity.row.actions` slot.\n * Rendered for each row in a entity collection table.\n * @group Plugins\n */\nexport interface EntityRowActionsProps {\n entity: Entity;\n entityId: string;\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n selectionController: SelectionController;\n context: RebaseContext;\n}\n\n/**\n * Props for `entity.field.before` and `entity.field.after` slots.\n * Rendered around individual form fields in the entity edit view.\n * @group Plugins\n */\nexport interface EntityFieldSlotProps {\n propertyKey: string;\n property: Property;\n path: string;\n entityId?: string | number;\n collection: AdminCollection;\n context: RebaseContext;\n}\n\n/**\n * Props for `collection.filter-panel` slot.\n * Custom filter sidebar rendered alongside the collection table.\n * @group Plugins\n */\nexport interface CollectionFilterPanelProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n tableController: EntityTableController;\n context: RebaseContext;\n}\n\n/**\n * Props for `dashboard.widget` slot.\n * Widgets rendered on the home / dashboard page.\n * @group Plugins\n */\nexport interface DashboardWidgetProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `global.search` slot.\n * Cross-collection search bar rendered in the app shell.\n * @group Plugins\n */\nexport interface GlobalSearchProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `shell.toolbar` slot.\n * Actions rendered in the top-level toolbar / app bar area.\n * @group Plugins\n */\nexport interface ShellToolbarProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `collection.widgets` slot.\n * Widgets rendered above the collection table.\n * @group Plugins\n */\nexport interface CollectionWidgetsSlotProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n}\n\n/**\n * Props for `home.card.widget` slot.\n * Compact widget rendered inline in a home page collection card.\n * @group Plugins\n */\nexport interface HomeCardWidgetSlotProps {\n slug: string;\n collection: AdminCollection;\n context: RebaseContext;\n}\n"],"mappings":";;;;;;;;AA8aA,IAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;ACtYnC,SAAgB,eAAkB,WAAoD;CAClF,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAoB,WAAqD;CACrF,OAAO;AACX;;;;;;;;;;AC6HA,SAAgB,iBACZ,YACgB;CAChB,OAAO,oBAAoB,UAAU;AACzC;;;;;;;;ACueA,IAAa,wBAAwB;;;;;;;;;;;;;AAoFrC,SAAgB,uBAGd,YAA4F;CAC1F,MAAM,QAAS,WAA2D;CAC1E,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EAAE,GAAI;EAAyC,GAAG;EAAO,OAAO;CAAM;AACjF;;;;;;;;;;;;;;AAeA,SAAgB,wBAGd,YAA6F;CAC3F,OAAO,wBAAwB,UAAqC;AACxE;;;;;;;AC/lBA,IAAa,uBAAuB;CAChC;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;ACtKA,IAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;ACuFjC,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/types/property_options.ts","../src/react_component_ref.ts","../src/define_collection.ts","../src/admin_collection.ts","../src/types/entity_display.ts","../src/types/form_layout.ts","../src/types/slots.tsx"],"sourcesContent":["/**\n * Per-property presentation options.\n *\n * These lived in `@rebasepro/types` next to the property types they belong to, which\n * meant a BaaS install shipped `Field`, `Preview`, `columnWidth` and `hideFromCollection`\n * in its type surface with nothing to render them. They are attached to the property\n * types by `augment.ts` instead.\n */\nimport type { ComponentRef, FilterValues, WhereFilterOp } from \"@rebasepro/types\";\nimport { ADMIN_PROPERTY_KEYS as CORE_ADMIN_PROPERTY_KEYS } from \"@rebasepro/types\";\nimport type { PropertySpan } from \"./form_layout\";\n\n/**\n * Interface including all common properties of an admin property.\n * @group Entity properties\n */\nexport interface AdminPropertyOptions<CustomProps = unknown> {\n /**\n * Width of this property's column in the table view, in pixels. Omit and the\n * table derives one from the property type.\n *\n * A person can drag a column wider, and that is remembered per user; this is\n * the width everyone starts from.\n */\n columnWidth?: number;\n /**\n * Keep this property out of the table and card views. It is still on the\n * entity form, and still read and written by the API.\n *\n * For a field that should not leave the server at all, use\n * `excludeFromApi` on the property itself — this one is presentation, and\n * hiding a secret with it hides it from exactly one screen.\n */\n hideFromCollection?: boolean;\n /**\n * Render the **value**, not a control. Defaults to `false`.\n *\n * The distinction from {@link disabled} is what the field looks like and\n * what it can do. `readOnly` shows the value as text: nothing to focus,\n * nothing to clear, and no explanation owed, because there is no control to\n * wonder about. Use it for something the server owns — a computed total, an\n * `autoValue` timestamp.\n *\n * A property that is read-only *for some people* is not this: that is a\n * security rule, and writing it here leaves the column writable through the\n * API by anyone who skips the panel.\n */\n readOnly?: boolean;\n /**\n * Render the control, greyed out. Defaults to `false`.\n *\n * The counterpart to {@link readOnly}: this one is a field that *could* be\n * edited but is not, right now — usually because of a condition, which is\n * why it takes a config with a `disabledMessage` saying why and a\n * `clearOnDisabled` for the value that no longer applies. The control stays\n * visible so the reader can see the shape of what they are not allowed to\n * fill in.\n */\n disabled?: boolean | PropertyDisabledConfig;\n\n /**\n * How many of the form grid's {@link FORM_GRID_COLUMNS} columns this field\n * occupies. Omit to let the layout derive one from the property type.\n *\n * Spans snap to a shared grid, so two fields line up whatever order they\n * are declared in.\n */\n span?: PropertySpan;\n /**\n * Anything your own {@link Field} or {@link Preview} needs, passed straight\n * through untouched.\n *\n * Typed by the property's own `CustomProps` parameter, so a custom field\n * declares what it expects and a collection that supplies the wrong shape is\n * a compile error rather than an `undefined` at render time.\n */\n customProps?: CustomProps;\n /**\n * Replace the form control for this property.\n *\n * The component receives `FieldProps` — the value, `setValue`, the resolved\n * property, the whole entity's values, and any {@link customProps}. It owns\n * the input; validation, the label and the error line stay with the form.\n *\n * A `ComponentRef` rather than a component so a collection stays\n * serializable: the reference is a registered key, which survives being sent\n * to the schema editor and written back to the file.\n */\n Field?: ComponentRef<any>;\n /**\n * Replace how this property renders when it is *not* being edited — a table\n * cell, a card line, a reference chip.\n *\n * Separate from {@link Field} because the two are read in different places\n * and at different sizes; overriding one and not the other is normal.\n */\n Preview?: ComponentRef<any>;\n\n /**\n * Narrow the filter operators offered for this property in collection\n * filter UIs (table header filters and the Filters dialog).\n *\n * The final offered set is the **intersection** of the engine's\n * capabilities, the property-type defaults, and this list — you can only\n * *restrict*, never enable an operator the underlying engine cannot run.\n *\n * Pass an empty array to disable filtering on this property entirely.\n *\n * @example\n * // Email column: exact match, contains, and null check only\n * admin: { filterOperators: [\"==\", \"ilike\", \"is-null\"] }\n */\n filterOperators?: readonly WhereFilterOp[];\n\n /**\n * Replace the filter field rendered for this property in collection\n * filter UIs. The component receives `FilterFieldBindingProps`\n * (property, resolved `operators`, `value`, `setValue`, …).\n *\n * Takes precedence over the collection-level\n * `components[\"Collection.FilterField\"]` override and the built-in\n * per-type filter fields.\n */\n Filter?: ComponentRef<any>;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminStringOptions extends AdminPropertyOptions {\n /**\n * Is this string property long enough so it should be displayed in\n * a multiple line field. Defaults to false. If set to true,\n * the number of lines adapts to the content\n */\n multiline?: boolean;\n /**\n * Should this string property be displayed as a markdown field. If true,\n * the field is rendered as a text editor that supports markdown highlight\n * syntax. It also includes a preview of the result.\n */\n markdown?: boolean;\n /**\n * Should this string be rendered as a tag instead of just text.\n */\n previewAsTag?: boolean;\n /**\n * Add an icon that sets the value to `null`. Defaults to `false`.\n *\n * Worth setting where empty and empty-string are different answers — an\n * unset middle name is not the same as one somebody deleted.\n */\n clearable?: boolean;\n /**\n * How to render a string that holds a URL: one of the supported media types\n * for an inline preview, or `true` for a plain link.\n *\n * Only presentation. Whether the string *is* a URL is `url` on the property\n * itself, which is what the OpenAPI contract is generated from — and a\n * property that declares `url: true` renders as a link with no help from\n * here. Set this only to upgrade that link to an inline rendering of what it\n * points at.\n *\n * `true` was always honoured at runtime (both the preview and the skeleton\n * branch on `typeof … === \"boolean\"`) and was missing from this type, so the\n * one value that means \"just a link\" was the one value that did not compile.\n */\n urlPreview?: PreviewType | boolean;\n}\n\n/**\n * How a number is written out for reading.\n *\n * A thin, explicit subset of `Intl.NumberFormatOptions`. Explicit is the whole\n * point: nothing here is inferred. A collection that happens to carry a\n * `currency` column alongside a `total` column has not told us that one formats\n * the other — that is a relationship only the author knows, and guessing it\n * would put a euro sign on the one number that was never money.\n *\n * Presentation only. It changes what {@link PropertyPreview} renders — the\n * detail view, the table cell, a reference card — and never what the number\n * input holds, because a formatted string is not a number you can type into.\n */\nexport interface NumberFormatOptions {\n /** Defaults to `\"decimal\"`. `\"currency\"` requires {@link currency}. */\n style?: \"decimal\" | \"currency\" | \"percent\";\n /**\n * ISO 4217 code — `\"EUR\"`, `\"USD\"`. Setting it implies `style: \"currency\"`,\n * so the common case is one key.\n */\n currency?: string;\n /**\n * BCP 47 tag. Defaults to the panel's locale, which is what makes the same\n * amount read `1,234.50` for one user and `1.234,50` for another.\n */\n locale?: string;\n /** Pad to at least this many decimals — `2` writes `5` as `5.00`. */\n minimumFractionDigits?: number;\n /** Round to at most this many decimals. Does not change the stored value. */\n maximumFractionDigits?: number;\n /** `\"compact\"` renders `12000` as `12K`. Useful in narrow table columns. */\n notation?: \"standard\" | \"compact\";\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminNumberOptions extends AdminPropertyOptions {\n /**\n * Add an icon that sets the value to `null`. Defaults to `false`.\n *\n * Numbers are where this matters most: without it, clearing the input\n * leaves `0`, and \"no price\" and \"free\" become the same row.\n */\n clearable?: boolean;\n /**\n * Write this number out as currency, a percentage, or with fixed decimals.\n * Omit and the raw value renders, which stays the default: a number in the\n * database is shown as the number in the database.\n */\n format?: NumberFormatOptions;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminVectorOptions extends AdminPropertyOptions {\n /**\n * Add an icon that sets the embedding to `null`. Defaults to `false`.\n *\n * A vector is normally written by whatever generates it, so this is for the\n * case where a human needs to say \"this one is stale\" and let it be\n * recomputed.\n */\n clearable?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminDateOptions extends AdminPropertyOptions {\n /**\n * Add an icon to clear the value and set it to `null`. Defaults to `false`\n */\n clearable?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminReferenceOptions extends AdminPropertyOptions {\n /**\n * Which of the *target's* properties are shown in the chip that stands in\n * for the referenced entity. At most three fit; the rest are ignored.\n *\n * Defaults to the target collection's own `admin.previewProperties`, then to\n * a derived guess. Name them here when the referring context wants different\n * ones — an order line wants the product's SKU, the catalogue wants its\n * name.\n */\n previewProperties?: string[];\n\n /**\n * Offer only entities that pass this filter in the selection dialog.\n * e.g. `fixedFilter: { age: [\">=\", 18] }`\n */\n fixedFilter?: FilterValues<string>;\n\n /** Show the referenced entity's id in previews. Defaults to `true`. */\n includeId?: boolean;\n\n /** Show a link that opens the referenced entity. Defaults to `true`. */\n includeEntityLink?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminRelationOptions extends AdminPropertyOptions {\n /**\n * Which of the *target's* properties are shown in the chip that stands in\n * for the related row. At most three fit; the rest are ignored.\n *\n * Defaults to the target collection's own `admin.previewProperties`, then to\n * a derived guess. Name them here when this side wants different ones.\n */\n previewProperties?: string[];\n\n /**\n * Which widget selects the related entity. Defaults to `select`.\n */\n widget?: \"select\" | \"dialog\";\n\n /**\n * Offer only entities that pass this filter in the selection widget.\n * e.g. `fixedFilter: { age: [\">=\", 18] }`\n */\n fixedFilter?: FilterValues<string>;\n\n /** Show the related entity's id in previews. Defaults to `true`. */\n includeId?: boolean;\n\n /** Show a link that opens the related entity. Defaults to `true`. */\n includeEntityLink?: boolean;\n\n /**\n * Render a **many**-relation as a picker inside the entity form as well as\n * the tab it already gets. Defaults to `false`.\n *\n * The entity view lists a many-relation's rows as a tab, which is the whole\n * treatment: the child rows are a list, not a value the form holds. This\n * flag exists for the project that wants the inline picker anyway — it is\n * off by default because the two surfaces are redundant by construction.\n *\n * No effect on a to-one relation: a foreign key gets no tab, so its picker\n * is always rendered.\n */\n renderInForm?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminArrayOptions extends AdminPropertyOptions {\n /**\n * Open every element on load instead of collapsing them to one line each.\n * Defaults to `false`.\n *\n * Expanding is right for a short list of small elements and wrong for a long\n * one: twenty open cards is a form nobody can find the bottom of.\n */\n expanded?: boolean;\n /**\n * Drop the per-element chrome — the frame, the header, the index — and\n * render the children alone. Defaults to `false`.\n *\n * For an array of one simple field, where the chrome is most of the pixels.\n */\n minimalistView?: boolean;\n\n /**\n * Can the elements in this array be reordered by dragging. Defaults to\n * `true`. No effect when the property is disabled.\n */\n sortable?: boolean;\n\n /**\n * Can elements be added to this array. Defaults to `true`. No effect when\n * the property is disabled.\n */\n canAddElements?: boolean;\n}\n\n/**\n * @group Entity properties\n */\nexport interface AdminMapOptions extends AdminPropertyOptions {\n /**\n * Open the map's fields on load instead of collapsing them behind its\n * header. Defaults to `false`.\n */\n expanded?: boolean;\n /**\n * Drop the map's frame and header and render its fields alone. Defaults to\n * `false`.\n */\n minimalistView?: boolean;\n /**\n * Lay the map's fields out as if they were the parent's own, rather than\n * grouped inside it. Defaults to `false`.\n *\n * Presentation only — the values still nest under this property's key in the\n * row, and in every read the API serves. It is for a group that is a\n * grouping in the schema and not in the form: an address, a set of\n * dimensions.\n */\n spreadChildren?: boolean;\n\n /**\n * Which of the map's own properties are shown when it is rendered as a\n * preview. Defaults to all of them, in `propertiesOrder`.\n */\n previewProperties?: string[];\n}\n\n/**\n * @group Entity properties\n */\nexport interface PropertyDisabledConfig {\n /**\n * Enable this flag if you would like to clear the value of the field\n * when the corresponding property gets disabled.\n *\n * This is useful for keeping data consistency when you have conditional\n * properties.\n */\n clearOnDisabled?: boolean;\n\n /**\n * Explanation of why this property is disabled (e.g. a different field\n * needs to be enabled)\n */\n disabledMessage?: string;\n\n /**\n * Set this flag to true if you want to hide this field when disabled\n */\n hidden?: boolean;\n}\n\n/**\n * Used for previewing urls if the download file is known\n * @group Entity properties\n */\nexport type PreviewType = \"image\" | \"video\" | \"audio\" | \"file\";\n\n/**\n * Every key any property `admin` block accepts, across the base options and the\n * per-type extensions.\n */\ntype AnyAdminPropertyOptionKey =\n | keyof AdminPropertyOptions\n | keyof AdminStringOptions\n | keyof AdminNumberOptions\n | keyof AdminVectorOptions\n | keyof AdminDateOptions\n | keyof AdminReferenceOptions\n | keyof AdminRelationOptions\n | keyof AdminArrayOptions\n | keyof AdminMapOptions;\n\n/**\n * Core's list, re-exported through the same `satisfies` agreement check that\n * {@link ADMIN_COLLECTION_KEYS} gets: core owns the data because the boot-time\n * collection validator in `@rebasepro/server` needs it and may not import this\n * package, and this clause is what stops the data from drifting off the types.\n */\nexport const ADMIN_PROPERTY_KEYS = CORE_ADMIN_PROPERTY_KEYS satisfies readonly AnyAdminPropertyOptionKey[];\n\n/**\n * And the reverse direction: an option key these types declare that core's list\n * does not name.\n *\n * The `satisfies` above only closes one side. This one matters since the boot\n * validator started warning about unrecognised keys inside a property's `admin`\n * block: an option missing from the list would make the server call a correct\n * config a typo, and a check that cries wolf is a check people turn off.\n */\ntype _EveryAdminPropertyOptionIsListed =\n AssertNeverPropertyKey<Exclude<AnyAdminPropertyOptionKey, typeof CORE_ADMIN_PROPERTY_KEYS[number]>>;\n\n/** Compiles only when `T` is `never`. */\ntype AssertNeverPropertyKey<T extends never = never> = T;\n","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 * `defineCollection` — the admin-aware builder, in a module a backend can load.\n *\n * This is the function every scaffolded collection file imports, and it must be\n * reachable from a Node process that has no React and no DOM. So it lives here,\n * apart from `admin_collection.ts` (which describes the panel's option types and\n * names `React` throughout) and well away from `collections.ts` (the panel's\n * view models, which import React as a value).\n *\n * The side-effect import below is the other half of what the import buys you:\n * `augment.ts` is what declares `admin` on `BaseCollectionConfig` and on every\n * property type, so importing this builder brings the block's type-checking with\n * it. It is types only, and compiles to nothing.\n */\n// Side-effect import: this is what adds `admin` back onto the core types.\nimport \"./augment\";\n\nimport type {\n FirebaseCollectionConfig,\n FirebaseProperties,\n FirebaseProperty,\n InferEntityType,\n MongoDBCollectionConfig,\n MongoProperties,\n MongoProperty,\n PostgresCollectionConfig,\n PostgresProperties,\n PostgresProperty,\n Properties,\n Property,\n StrictProperties,\n User,\n CollectionConfig\n} from \"@rebasepro/types\";\nimport { resolveResourceRefs, type ResourceRef } from \"@rebasepro/types\";\n\n/**\n * The engines a collection can name. Absent means Postgres.\n *\n * The discriminant that replaced three overloads of `defineCollection`. See\n * {@link CollectionConfigForEngine} for why that mattered.\n */\ntype CollectionEngine = \"postgres\" | \"firestore\" | \"mongodb\";\n\n/**\n * The concrete collection type an `engine` selects.\n *\n * `defineCollection` used to be three overloads — one per engine — and overload\n * resolution is what made its errors unreadable. When no overload matches,\n * TypeScript emits **one** diagnostic at the call site listing each overload's\n * *first* failure, so:\n *\n * - a bad `defaultValue` **and** a misspelled `admin.display.title` in the same\n * collection reported only the first. Fixing it revealed the second on the\n * next run, one per edit-compile cycle;\n * - the error landed on `defineCollection(`, not on the key that was wrong;\n * - and every Postgres collection's error dragged `FirebaseCollectionConfig`\n * and `MongoDBCollectionConfig` through the message, naming two engines the\n * author had not mentioned and does not use.\n *\n * With one signature there is no resolution to fail: each error is reported\n * where it is, all of them at once, against the one config type the `engine`\n * selects.\n */\ntype CollectionConfigForEngine<E, P, USER extends User> =\n E extends \"firestore\" ? FirebaseCollectionConfig<EntityShapeOf<P>, USER>\n : E extends \"mongodb\" ? MongoDBCollectionConfig<EntityShapeOf<P>, USER>\n : PostgresCollectionConfig<EntityShapeOf<P>, USER>;\n\n/**\n * `InferEntityType`, tolerant of a property map that has an error in it.\n *\n * `P` is deliberately **unconstrained** on the builder, and this is why. A\n * constraint TypeScript cannot satisfy is a constraint it silently falls back\n * from: one property with a bad `defaultValue` made `P extends PostgresProperties`\n * fail, `P` became `PostgresProperties`, `M` became `Record<string, unknown>`,\n * and every `admin` key — `display.title`, `listProperties`, `propertiesOrder` —\n * widened to `string` and stopped being checked. So a collection with two\n * mistakes reported one, and reported the second only after the first was fixed.\n *\n * With no constraint, `keyof P` survives a bad property and the `admin` block is\n * still checked against the real key set. Exactness and the engine gate move\n * into `StrictProperties`, which reports them on the property itself.\n */\ntype EntityShapeOf<P> = InferEntityType<{\n [K in keyof P]: P[K] extends Property ? P[K] : Property;\n}>;\n\n/** The property union an engine admits — the engine gate, as a type. */\ntype PropertyForEngine<E> =\n E extends \"firestore\" ? FirebaseProperty\n : E extends \"mongodb\" ? MongoProperty\n : PostgresProperty;\n\n/** {@link PropertyForEngine} as a property map, for the `P` constraint. */\ntype PropertiesForEngine<E> =\n E extends \"firestore\" ? FirebaseProperties\n : E extends \"mongodb\" ? MongoProperties\n : PostgresProperties;\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.display`, `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 * display: { title: \"title\" }, // completion: \"title\" | \"status\"\n * listProperties: [\"title\", \"status\"]\n * }\n * });\n *\n * @group Builder\n */\nexport function defineCollection<\n const E extends CollectionEngine = \"postgres\",\n /**\n * The properties, **constrained**. This is what checks them, and — just as\n * importantly — what supplies the contextual type inside them: without a\n * constraint the parameter of an inline\n * `callbacks: { beforeSave: ({ value }) => … }` has nothing to be typed\n * from, and TypeScript reports an implicit `any` on a callback the author\n * wrote correctly.\n */\n const P extends PropertiesForEngine<E> & Properties = PropertiesForEngine<E> & Properties,\n /**\n * The properties again, **unconstrained**, and this is why there are two.\n *\n * A constraint TypeScript cannot satisfy is one it silently falls back\n * from: one property with a bad `defaultValue` made `P` become\n * `PostgresProperties`, the entity shape become `Record<string, unknown>`,\n * and every `admin` key — `display.title`, `listProperties`,\n * `propertiesOrder` — widen to `string` and stop being checked. A\n * collection with two mistakes reported one, and revealed the second only\n * after the first was fixed.\n *\n * `KEYS` has no constraint to fall back from, so `keyof KEYS` survives a bad\n * property and the `admin` block is still checked against the real key set.\n */\n const KEYS = Properties,\n USER extends User = User\n>(\n collection: Omit<CollectionConfigForEngine<E, KEYS, USER>, \"properties\" | \"engine\" | \"dataSource\">\n & {\n engine?: E;\n properties: StrictProperties<P, PropertyForEngine<E>> & KEYS;\n dataSource?: ResourceRef;\n }\n): CollectionConfigForEngine<E, KEYS, USER> & { properties: KEYS };\n\n/**\n * At runtime this records the collection as data: a resource handle written\n * where a key belongs — `dataSource: analytics`, `storageSource: media` — is\n * replaced by its key, so what leaves here serialises and compares like the\n * string it always was. The signature above is the rest of the point.\n * @group Builder\n */\nexport function defineCollection(\n collection: Omit<CollectionConfig, \"dataSource\"> & { dataSource?: ResourceRef }\n): CollectionConfig {\n return resolveResourceRefs(collection) as CollectionConfig;\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 CollectionCallbacks,\n CollectionConfig,\n ComponentRef,\n FilterPreset,\n FilterValues,\n OrderBySpec,\n PostgresCollectionConfig,\n Property,\n Properties,\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, nestAdminCollectionKeys } 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 { CollectionCustomView } from \"./types/collection_views\";\nimport type { EntityDisplay } from \"./types/entity_display\";\nimport type { FormLayoutConfig } from \"./types/form_layout\";\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 * The `display` block for a collection, with its property paths checked\n * against `M`.\n *\n * `EntityDisplay` is generic over the path type so that\n * `@rebasepro/cms-types`' two halves do not import each other in a cycle;\n * this alias is what an authoring site actually names.\n */\nexport type CollectionDisplay<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = EntityDisplay<PropertyPath<M>, M, USER>;\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 * Lifecycle callbacks that run **in the browser**, in the admin panel.\n *\n * The twin of the collection's top-level `callbacks`, and the distinction is\n * only where the code runs — the shape is identical:\n *\n * - `callbacks` runs on the server, on every path that reaches it (REST,\n * realtime, `dataAsAdmin`). Its bodies are stripped from the admin bundle,\n * so a secret read there never leaves the server.\n * - `browserCallbacks` runs in the panel, and nowhere else. It ships to\n * every visitor.\n *\n * This exists for collections on a `direct` or `custom` transport — a\n * Firestore collection the panel talks to itself, with no Rebase server in\n * the request path. Nothing server-side sees those writes, so `callbacks`\n * can never fire for them; this block is the only place their lifecycle\n * logic can live.\n *\n * Two rules follow from \"ships to every visitor\", and neither is a style\n * preference:\n *\n * 1. **No secrets.** No API keys, no `process.env`, no logic you would mind\n * a reader of the bundle seeing. Put that in `callbacks`.\n * 2. **Not a security boundary.** A `browserCallbacks.afterRead` that\n * redacts a field redacts it *after* the browser already holds the row —\n * for a direct transport the raw document came straight from the store.\n * It is presentation. Redaction that has to hold belongs in `callbacks`,\n * or in the store's own rules.\n *\n * On a server-transport collection (the default) the server has already run\n * `callbacks` before the row arrives, so a `browserCallbacks.afterRead`\n * here runs *in addition* — write it to be idempotent, or don't write it.\n *\n * ```ts\n * admin: {\n * browserCallbacks: {\n * afterRead: ({ row }) => ({ ...row, label: `${row.city} (${row.code})` })\n * }\n * }\n * ```\n */\n browserCallbacks?: CollectionCallbacks<M, USER>;\n\n /**\n * How a record of this collection shows up — its title, subtitle, image,\n * status, date and tags.\n *\n * Each role takes a property path or a resolver, and a resolver may be\n * async:\n *\n * ```ts\n * display: {\n * title: \"name\",\n * image: \"cover.url\",\n * subtitle: ({ entity }) => `${entity.values.city}, ${entity.values.country}`,\n * status: async ({ entity, context }) =>\n * (await context.data.audits.get(`${entity.id}/latest`))?.state\n * }\n * ```\n *\n * Every role left out is derived from the property schema exactly as before,\n * so a collection that says nothing renders as it always did. See\n * {@link EntityDisplay} for what each role means and\n * {@link EntityDisplayResolver} for what a resolver is handed.\n */\n readonly display?: EntityDisplay<PropertyPath<M>, M, USER>;\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\"`, or for a user who may not edit the record). In edit mode\n * `formContext` is the record form's live context; in read-only mode\n * `formContext.disabled` and `formContext.readOnly` are both `true`.\n */\n formView?: FormViewConfig;\n\n /**\n * How the generated form is laid out: which properties are grouped into\n * sections in the main column, and which are pulled out into the metadata\n * rail beside it.\n *\n * Entirely optional. With no `form` block the layout is derived from the\n * properties themselves — see {@link FormLayoutConfig} — which is what most\n * collections should rely on. Reach for this when the derived grouping is\n * wrong for your domain, not to restate it.\n *\n * Unlike {@link FormViewConfig}, this does not replace the generated form:\n * every field keeps its validation, error focus, local-changes restore and\n * autosave wiring.\n */\n form?: FormLayoutConfig<M>;\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 admin 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 *\n * One key, or several applied in order of significance — the second breaks\n * ties on the first, and so on. The row id breaks the last tie, so the\n * order is total and paging over it neither repeats nor skips rows.\n *\n * @example sort: [\"order\", \"asc\"]\n * @example sort: [[\"roles\", \"asc\"], [\"createdAt\", \"desc\"]]\n */\n readonly sort?: OrderBySpec<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 * Note that this covers *both* roles at once. A collection that is a root\n * collection **and** the target of a many-relation is hidden in both places,\n * which is rarely what you want for a join or audit table: it should not be\n * a destination in the drawer, but it is exactly what you want to see on its\n * parent. Use {@link hideFromEntityViews} to separate the two.\n */\n hideFromNavigation?: boolean;\n\n /**\n * Should this collection be hidden from the tab strip of a parent entity,\n * when it is reached as a child view (a Firestore subcollection, or the\n * target of a `many`-cardinality relation).\n *\n * Independent of {@link hideFromNavigation}, which governs the drawer. The\n * two exist separately because a collection commonly plays both roles and\n * wants a different answer for each:\n *\n * - a join table (`company_members`) is not a destination but *is* a\n * meaningful tab → `hideFromNavigation: true`, this left unset;\n * - a table with a dedicated workspace (`scraped_jobs`) may want the\n * opposite, so the workspace stays the only way in.\n *\n * Defaults to `false`. Setting {@link hideFromNavigation} does not imply it.\n */\n hideFromEntityViews?: 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 * - \"list\": Display entities as a list (default)\n * - \"table\": Display entities in a table with inline editing\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * - any `key` from {@link customViews}\n * Defaults to \"list\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"list\", \"table\", \"cards\", \"kanban\", and any `key` from\n * {@link customViews}.\n * Defaults to all four built-ins plus every declared custom view.\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 * With a single entry the view switcher is hidden.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Additional ways to render this collection's rows, offered in the view\n * switcher beside list / table / cards / kanban.\n *\n * Can be an array of `CollectionCustomView` or a string naming the `key` of\n * one registered globally on `<RebaseCMS collectionViews={…}>`. The\n * string form is what lets a React-free config package reference React UI,\n * and it is what the collection editor stores.\n *\n * A custom view is another rendering of the *same query* — it is handed the\n * live table controller and inherits filters, search and the entity side\n * panel. Use an `AppView` instead for a workflow spanning collections.\n *\n * @example\n * ```ts\n * admin: {\n * customViews: [\n * { key: \"map\", name: \"Map\", icon: \"Map\", Builder: MapView }\n * ],\n * enabledViews: [\"table\", \"map\"],\n * defaultViewMode: \"map\"\n * }\n * ```\n */\n customViews?: (string | CollectionCustomView<Record<string, unknown>>)[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n *\n * A board is only half-configured without {@link orderProperty}. Cards\n * still drag between columns — that writes `columnProperty` — but their\n * order *within* a column has nowhere to be stored, so it resets on the\n * next read and the board renders a warning bar saying so. Declare both,\n * always.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n *\n * Must reference a **string** property — a `number` can never hold one of\n * these keys, so a numeric `sortOrder` leaves the board permanently asking\n * to be initialised. The convention across the collections here is a\n * hidden `__order: { type: \"string\", admin: { disabled: true,\n * hideFromCollection: true } }`.\n *\n * Reordering writes a `fractional-indexing` key built from the base36,\n * lower-case alphabet `0123456789abcdefghijklmnopqrstuvwxyz` — `\"i0\"`,\n * `\"i1\"`, `\"i0i\"`. Single case because *Postgres* does the sorting and its\n * default collation is not byte ordering; base36 rather than the library's\n * default base62 for the same reason. Generating a key without passing\n * that alphabet yields base62 keys (`\"a0\"`), which this board rejects.\n *\n * Nothing assigns a key on insert. A row created by a cron, a seed, a\n * migration or the REST API lands with this property null, and the board\n * shows an **Initialize** bar until someone clicks it. Backends that\n * create rows for a board should append a key themselves — see the\n * \"Kanban boards\" section of the `rebase-collections` skill.\n *\n * Used by Kanban view for ordering within columns and can be used for\n * 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 * An entry may be the action itself, or the `key` of one registered app-level\n * on `<RebaseCMS entityActions={…}>` — `resolveEntityAction` looks a string\n * up against that list.\n *\n * The key form is what lets a collection declared in a React-free config\n * package use an action whose UI is React: an action carries an `onClick` and\n * usually renders a dialog, so importing one into a collection file pulls the\n * admin bundle into any backend that loads it for its schema. Naming it costs\n * nothing there.\n *\n * `string` was accepted at runtime and by the collection editor — which stores\n * exactly these keys — long before the type said so, which meant the documented\n * approach needed a cast. Mirrors `entityViews`, typed this way already.\n */\n entityActions?: (string | 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/cms-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 * `defineCollection` lives in `./define_collection`, and this is why.\n *\n * A collection file is loaded by the backend as well as by the panel, so the\n * builder it imports has to be reachable without React. This module names\n * `React` throughout — it describes the panel's option types — so the builder\n * sits apart from it and the barrel exports both.\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/** Compiles only when `T` is `never` — see {@link _EveryAdminCollectionOptionIsListed}. */\ntype AssertNeverKey<T extends never = never> = T;\n\n/** Local alias, so the assertion below can name the list's element type. */\ntype AdminCollectionKeyName = typeof CORE_ADMIN_COLLECTION_KEYS[number];\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 */\nexport const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];\n\n/**\n * And the reverse: an option declared here that core's list does not name.\n *\n * This direction was believed to have no type-level expression — `keyof` over\n * optional properties does in fact yield them all, so it does. A test counted\n * the list instead, which catches a *change* in size but not a key added to the\n * options and forgotten here.\n *\n * It matters more since the boot validator started warning about unrecognised\n * `admin` keys: a real option missing from this list would make the server\n * report a correct config as a typo, and a check that cries wolf gets switched\n * off. The compile error arrives at the person adding the option, which is the\n * only moment anyone can fix it cheaply.\n */\ntype _EveryAdminCollectionOptionIsListed =\n AssertNeverKey<Exclude<keyof AdminCollectionOptions, AdminCollectionKeyName>>;\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 *\n * The nesting itself lives in `@rebasepro/types` because the schema editor in\n * `@rebasepro/server` — which cannot import this package — has to do exactly the\n * same thing when it writes a collection file back to disk. Two copies of the\n * rule disagreed on which side wins, and the disagreement was invisible.\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 return nestAdminCollectionKeys(collection as Record<string, unknown>) as unknown as CollectionConfig<M, USER>;\n}\n","/**\n * How a record shows up: its title, image, subtitle, status, date and tags.\n *\n * Every surface that draws a record draws some subset of these six roles. A list\n * row is image + title + subtitle + status + date; a card is the same with the\n * image on top; a board card drops the image; a reference picker is title +\n * subtitle; a page heading is the title alone. The roles are stable — what fills\n * them is not.\n *\n * Before this block, the roles were derived and only derived. `titleProperty`\n * was the single exception, and it could only ever name a property of the\n * collection: seven separate implementations read that key, disagreed about the\n * fallback, and none of them could await. (It is gone now — `display.title`\n * replaced it outright.) The other five roles could not be\n * stated at all — the image was whichever storage property came first, the\n * status whichever enum, the date whichever timestamp. Right often enough to\n * feel automatic, and wrong with no way to say so.\n *\n * So: one mechanism, six roles, two forms each.\n *\n * ```ts\n * admin: {\n * display: {\n * title: \"name\", // a property path\n * image: \"photos.0\", // a dotted path\n * subtitle: ({ entity }) => // computed\n * `${entity.values.city}, ${entity.values.country}`,\n * status: async ({ entity, context }) => { // and may be async\n * const latest = await context.data.audits.get(`${entity.id}/latest`);\n * return latest?.state;\n * }\n * }\n * }\n * ```\n *\n * Anything left out is derived exactly as it is today, so an existing collection\n * renders identically, and a collection that needs one role fixed states that\n * one role.\n */\nimport type { Entity, User } from \"@rebasepro/types\";\nimport type { RebaseContext } from \"../rebase_context\";\n\n/**\n * What a resolver is handed.\n *\n * The whole {@link RebaseContext}, matching `AdditionalFieldDelegate.value` and\n * `EntityAction.onClick` — so a resolver can reach `context.data` and\n * `context.client` and read anything the panel can read, including a document in\n * a subcollection that the entity itself never loads.\n */\nexport type EntityDisplayResolverParams<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n entity: Entity<M>;\n context: RebaseContext<USER>;\n};\n\n/**\n * Computes what fills one display role for one record.\n *\n * May be async. While a promise is in flight the surface shows the derived value\n * and swaps the resolved one in when it lands — a title is never a spinner.\n * Results are cached per record and per role, and concurrent calls for the same\n * pair share one execution, so a list of fifty rows resolves each row once\n * rather than once per render.\n *\n * Return `undefined` when this record has nothing for this role. Do not return a\n * placeholder: the surface's own fallback is better informed than the resolver\n * is about what belongs there instead — a heading wants the collection's\n * singular name, a link wants the id.\n *\n * A resolver that throws is treated as `undefined` and logged once. A title that\n * cannot be fetched must not take down the row that shows it.\n */\nexport type EntityDisplayResolver<\n M extends Record<string, unknown> = Record<string, unknown>,\n T = unknown,\n USER extends User = User\n> = {\n /**\n * Declared as a *method* and then indexed back out, which is the only way to\n * write a standalone function type whose parameters stay bivariant.\n *\n * Not a style choice. `AdminCollectionOptions<M>` has to remain assignable\n * to `AdminCollectionOptions<Record<string, unknown>>` — every consumer that\n * takes a collection it did not author depends on it, and losing it breaks\n * `defineCollection`'s own overloads. A resolver takes `Entity<M>` in\n * parameter position, so written as `(params) => …` it makes the entire\n * admin block invariant in `M`, and a typed collection stops being usable as\n * a collection. Method syntax is bivariant under `strictFunctionTypes`; the\n * sibling callbacks (`EntityAction.onClick`, `AdditionalFieldDelegate.value`)\n * are all written this way, and `packages/types/__tests__/bivariance` is the\n * record of finding it out the hard way.\n */\n resolve(params: EntityDisplayResolverParams<M, USER>): T | undefined | Promise<T | undefined>;\n}[\"resolve\"];\n\n/**\n * Where one display role gets its value: a property path on this collection, or\n * a resolver that computes it.\n *\n * The path arm is checked against `M` and read with `getValueInPath`, so\n * `\"profile.displayName\"` is as valid as `\"title\"`. It also keeps the property's\n * own rendering — an enum status stays a coloured chip, a date stays formatted,\n * a storage path stays a thumbnail — which a resolver returning a bare string\n * cannot. Prefer the path whenever the value is on the record.\n *\n * `Path` is a type parameter rather than `PropertyPath<M>` directly, so this\n * module does not import from `admin_collection`, which imports it.\n */\nexport type EntityDisplaySource<\n Path extends string,\n M extends Record<string, unknown> = Record<string, unknown>,\n T = unknown,\n USER extends User = User\n> = Path | EntityDisplayResolver<M, T, USER>;\n\n/**\n * The six roles, and what may fill each.\n *\n * The value types describe what the renderers accept, not what a resolver must\n * produce exactly: a `date` resolver may return a `Date`, an ISO string or an\n * epoch number, and `tags` takes a single string as shorthand for one tag. A\n * property path is not constrained by them at all — the property's own type\n * decides how it renders.\n */\nexport type EntityDisplay<\n Path extends string = string,\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = {\n /**\n * What the record is called: the heading, the breadcrumb, the row, and the\n * label of every relation chip and reference that points at it.\n */\n title?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The line under the title — a short description, a location, a summary.\n */\n subtitle?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The record's picture. A storage path or a URL: the same two things a\n * `storage` property holds, so a resolver may return either.\n */\n image?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The state chip — published, archived, paid. Rendered with the enum's own\n * colour when it comes from an enum property.\n */\n status?: EntityDisplaySource<Path, M, string, USER>;\n\n /**\n * The timestamp a row is stamped with, usually when it last changed.\n */\n date?: EntityDisplaySource<Path, M, Date | string | number, USER>;\n\n /**\n * Free chips beside the status: labels, categories, topics. A single string\n * is accepted as shorthand for one tag.\n */\n tags?: EntityDisplaySource<Path, M, string[] | string, USER>;\n};\n\n/**\n * The roles as data, so every consumer iterates the same list instead of\n * repeating it — the mistake that let `titleProperty` grow seven readers.\n */\nexport const ENTITY_DISPLAY_ROLES = [\n \"title\",\n \"subtitle\",\n \"image\",\n \"status\",\n \"date\",\n \"tags\"\n] as const;\n\n/** One of the six display roles. */\nexport type EntityDisplayRole = typeof ENTITY_DISPLAY_ROLES[number];\n","import type { ColumnKey } from \"../admin_collection\";\n\n/**\n * The number of columns the form grid is divided into. A field's\n * {@link AdminPropertyOptions.span} is expressed against this.\n *\n * Fixed rather than configurable on purpose: the whole point of a span is that\n * two fields written by two different people line up, and they only do that if\n * everyone is counting against the same grid.\n *\n * @group Models\n */\nexport const FORM_GRID_COLUMNS = 4;\n\n/**\n * How wide a field sits on the {@link FORM_GRID_COLUMNS}-column form grid.\n *\n * `4` is the full width of the main column. A field always takes at least a\n * whole row on narrow layouts (the side panel, the split pane, mobile), where\n * the grid collapses to one column and spans are ignored.\n *\n * @group Entity properties\n */\nexport type PropertySpan = 1 | 2 | 3 | 4;\n\n/**\n * A titled group of fields in the main column of the form.\n *\n * @group Models\n */\nexport interface FormSection<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identity for this section. Used as the React key and to remember\n * the collapsed state across visits, so renaming `title` does not lose it.\n */\n key: string;\n\n /**\n * Shown above the group. A section with no title renders its fields with no\n * heading and no rule — useful for the first group, which rarely needs one.\n */\n title?: string;\n\n /**\n * Property and additional-field keys in this section, in render order.\n *\n * Keys naming a property that does not exist, is hidden, or has been routed\n * to {@link FormLayoutConfig.sidebar} are skipped. Any property *not* named\n * by a section lands in the last section that has no explicit title, or in\n * an untitled trailing group if there is none — a new column is never\n * silently dropped from the form.\n */\n properties: ColumnKey<M>[];\n\n /**\n * Start collapsed. Defaults to `false`.\n *\n * Only meaningful when the section can be collapsed at all; a section with\n * no `title` has nothing to click, so this is ignored there.\n */\n collapsed?: boolean;\n\n /**\n * Can the user collapse this section. Defaults to `true` for a titled\n * section, `false` for an untitled one.\n *\n * A section holding a required field is still collapsible — but a\n * validation error inside a collapsed section expands it, so an error can\n * never hide.\n */\n collapsible?: boolean;\n\n /**\n * How this section arranges itself in the **read-only** view of a record.\n * Defaults to `\"grid\"` — the same grid the form uses.\n *\n * `\"summary\"` stacks the fields as right-aligned label/value rows with the\n * last one emphasised, which is what a run of related figures wants: a\n * subtotal, a tax, a discount and a total are one calculation, and four\n * equal cells on a four-column grid is the one arrangement that says they\n * are unrelated. Opt in per section — nothing about a group of numbers tells\n * us it adds up, so this is never derived.\n *\n * Read-only only, and named for it. The form goes on rendering the grid:\n * a summary row is a reading arrangement, and shrinking a control to fit one\n * would make the fields harder to edit to make them prettier to skim.\n */\n readVariant?: \"grid\" | \"summary\";\n}\n\n/**\n * How the generated form is laid out.\n *\n * Everything here is optional, and the defaults are the point: with no config\n * at all the layout is derived from the properties themselves —\n *\n * - the id and the `createdAt`/`updatedAt` timestamps go to the rail, read-only\n * - short enums, booleans, dates and numbers take a narrow span\n * - long text, markdown, arrays, maps and storage fields take the full width\n * - everything else takes half\n *\n * so a collection that never mentions `form` still gets a two-column layout\n * rather than one flat run of full-width fields. Use this block when the\n * derived answer is wrong for your domain.\n *\n * @group Models\n */\nexport interface FormLayoutConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Property keys shown in the metadata rail beside the main column instead\n * of in it — status, ownership, publication dates, flags.\n *\n * The rail is narrow and does not use the grid, so `span` is ignored for\n * these. On layouts too narrow for a rail (the side panel, the split pane,\n * mobile) they render as an ordinary leading section, so nothing is lost.\n *\n * Set to `[]` to suppress the derived rail entirely and keep every field in\n * the main column.\n */\n sidebar?: ColumnKey<M>[];\n\n /**\n * Groups for the main column. When omitted, every field lands in a single\n * untitled group, which is the pre-existing behaviour.\n */\n sections?: FormSection<M>[];\n\n /**\n * Show the read-only record block (id, created, updated) at the foot of the\n * rail. Defaults to `true` when a rail is shown.\n *\n * This is what replaces `hideIdFromForm` for most collections: the id stops\n * being a field in the middle of the form and becomes a copyable line of\n * metadata.\n */\n showRecordMeta?: boolean;\n}\n","import React from \"react\";\n\nimport type { CollectionActionsProps, EntityTableController, SelectionController } from \"../collections\";\nimport type { Entity } from \"@rebasepro/types\";\nimport type { PluginFormActionProps, PluginGenericProps, PluginHomePageActionsProps, PluginHomePageAdditionalCardsProps } from \"./plugins\";\nimport type { Property } from \"@rebasepro/types\";\nimport type { RebaseContext } from \"../rebase_context\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\n\n/**\n * Registry mapping slot names to their component prop types.\n * Each key represents a UI extension point in the admin.\n * @group Plugins\n */\nexport interface SlotRegistry {\n // ── Home page ─────────────────────────────────────────────────────\n \"home.actions\": PluginGenericProps;\n \"home.cards\": PluginHomePageAdditionalCardsProps;\n \"home.children.start\": PluginGenericProps;\n \"home.children.end\": PluginGenericProps;\n /** Compact widget rendered inline in a home page collection card. */\n \"home.card.widget\": HomeCardWidgetSlotProps;\n \"home.collection.actions\": PluginHomePageActionsProps;\n\n // ── Navigation / Drawer ───────────────────────────────────────────\n /** Rendered below the logo in the sidebar drawer. */\n \"navigation.header\": NavigationSlotProps;\n /** Rendered above the collapse toggle at the bottom of the drawer. */\n \"navigation.footer\": NavigationSlotProps;\n\n // ── Collection view ───────────────────────────────────────────────\n \"collection.actions\": CollectionActionsProps;\n \"collection.actions.start\": CollectionActionsProps;\n \"collection.header.action\": CollectionHeaderActionProps;\n \"collection.add-column\": CollectionAddColumnProps;\n \"collection.error\": CollectionErrorProps;\n /** Extra widgets rendered inside the collection toolbar row. */\n \"collection.toolbar\": CollectionToolbarProps;\n /** Custom empty-state component when a collection has no data. */\n \"collection.empty-state\": CollectionEmptyStateProps;\n /** Widgets rendered above the collection table. */\n \"collection.widgets\": CollectionWidgetsSlotProps;\n\n // ── Entity / Form ─────────────────────────────────────────────────\n \"form.actions\": PluginFormActionProps;\n \"form.actions.top\": PluginFormActionProps;\n /** Rendered before the form title / field list. */\n \"form.before\": PluginFormActionProps;\n /** Rendered after the form field list. */\n \"form.after\": PluginFormActionProps;\n\n // ── Entity row actions ────────────────────────────────────────────\n /** Per-row actions in entity tables (e.g. bulk actions, row context menus). */\n \"entity.row.actions\": EntityRowActionsProps;\n\n // ── Entity field decoration ───────────────────────────────────────\n /** Inject UI before an individual form field. */\n \"entity.field.before\": EntityFieldSlotProps;\n /** Inject UI after an individual form field. */\n \"entity.field.after\": EntityFieldSlotProps;\n\n // ── Collection filter panel ───────────────────────────────────────\n /** Custom filter sidebar for a collection. */\n \"collection.filter-panel\": CollectionFilterPanelProps;\n\n // ── Dashboard ─────────────────────────────────────────────────────\n /** Widget rendered on the dashboard / home page. */\n \"dashboard.widget\": DashboardWidgetProps;\n\n // ── Global ────────────────────────────────────────────────────────\n /** Cross-collection search bar component. */\n \"global.search\": GlobalSearchProps;\n /** Top-level toolbar actions rendered in the shell toolbar area. */\n \"shell.toolbar\": ShellToolbarProps;\n\n // ── Kanban ────────────────────────────────────────────────────────\n \"kanban.setup\": KanbanSetupProps;\n \"kanban.add-column\": KanbanAddColumnProps;\n}\n\n/**\n * Valid slot names for UI extension points.\n * @group Plugins\n */\n/**\n * Slots this build declares but renders nowhere.\n *\n * Every name here appears in {@link SlotRegistry}, has a props interface, and\n * is listed in the public slot reference alongside the ones that work — so a\n * plugin author picks one off the table, registers a component, sees nothing,\n * and has no way to tell whether the fault is theirs. Seven of twenty-nine were\n * in that state.\n *\n * This is a statement of fact, not a wish list: `slot-render-sites.test.ts`\n * derives the same set by scanning for render sites and fails when the two\n * disagree. Implementing a slot therefore forces its removal from here, and\n * declaring one without rendering it forces its addition — at which point\n * `Rebase` warns anyone who registers for it, which is the whole point.\n */\nexport const UNRENDERED_SLOTS = [\n \"collection.filter-panel\",\n \"dashboard.widget\",\n \"entity.field.after\",\n \"entity.field.before\",\n \"entity.row.actions\",\n \"global.search\",\n \"shell.toolbar\"\n] as const satisfies readonly (keyof SlotRegistry)[];\n\nexport type SlotName = keyof SlotRegistry;\n\n/**\n * A single UI component contribution to a named slot.\n * @group Plugins\n */\nexport interface SlotContribution<K extends SlotName = SlotName> {\n /**\n * Which slot to contribute to.\n */\n slot: K;\n\n /**\n * The component to render in the slot, taking that slot's props.\n *\n * This was `React.ComponentType<any>`, \"typed loosely so mixed-slot arrays\n * work\" — and the looseness was doing real damage: `{ slot:\n * \"collection.actions\", Component: MyThing }` typechecked whatever\n * `MyThing`'s props were, so a component written against the wrong slot's\n * props compiled, registered, and then read `undefined` off every prop it\n * expected. The only check was at the `useSlot` render site, which is\n * inside the framework and reports nothing to the author.\n *\n * The mixed-array problem is real but is a problem with the *array*, not\n * with this field: see {@link AnySlotContribution}, which distributes over\n * the slot names so each element is checked against its own slot.\n */\n Component: React.ComponentType<SlotRegistry[K]>;\n\n /**\n * Additional props to merge into the slot props before rendering.\n */\n props?: Record<string, unknown>;\n\n /**\n * Ordering hint. Lower values render first. Defaults to 50.\n */\n order?: number;\n}\n\n/**\n * A contribution to *some* slot, checked against that slot.\n *\n * `SlotContribution[]` cannot be the type of a mixed list: with `K` left at its\n * default the props become the union of every slot's, and a component is\n * contravariant in its props, so nothing satisfies it. Distributing the union\n * over the slot names instead gives one member per slot, and TypeScript picks\n * the member whose `slot` matches — which is what makes\n * `{ slot: \"collection.actions\", Component: WrongProps }` an error at the\n * declaration rather than a silent `undefined` at render.\n *\n * @group Plugins\n */\nexport type AnySlotContribution = { [K in SlotName]: SlotContribution<K> }[SlotName];\n\n// ── Prop interfaces for slots ─────────────────────────────────────────\n\n/**\n * Props for `navigation.header` and `navigation.footer` slots.\n * @group Plugins\n */\nexport interface NavigationSlotProps {\n drawerOpen: boolean;\n drawerHovered: boolean;\n context: RebaseContext;\n}\n\n/**\n * Props for the `collection.toolbar` slot.\n * @group Plugins\n */\nexport interface CollectionToolbarProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n tableController: EntityTableController;\n selectionController: SelectionController;\n}\n\n/**\n * Props for the `collection.empty-state` slot.\n * @group Plugins\n */\nexport interface CollectionEmptyStateProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n canCreate: boolean;\n onNewClick?: () => void;\n}\n\n/**\n * Props for the `collection.header.action` slot.\n * @group Plugins\n */\nexport interface CollectionHeaderActionProps {\n property: Property;\n propertyKey: string;\n path: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n onHover: boolean;\n collection: AdminCollection;\n tableController: EntityTableController;\n}\n\n/**\n * Props for the `collection.add-column` slot.\n * @group Plugins\n */\nexport interface CollectionAddColumnProps {\n path: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n collection: AdminCollection;\n tableController: EntityTableController;\n}\n\n/**\n * Props for the `collection.error` slot.\n * @group Plugins\n */\nexport interface CollectionErrorProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n error: Error;\n}\n\n/**\n * Props for the `kanban.setup` slot.\n * @group Plugins\n */\nexport interface KanbanSetupProps {\n collection: AdminCollection;\n fullPath: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n}\n\n/**\n * Props for the `kanban.add-column` slot.\n * @group Plugins\n */\nexport interface KanbanAddColumnProps {\n collection: AdminCollection;\n fullPath: string;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n columnProperty: string;\n}\n\n// ── New slot prop interfaces ──────────────────────────────────────────\n\n/**\n * Props for `entity.row.actions` slot.\n * Rendered for each row in a entity collection table.\n * @group Plugins\n */\nexport interface EntityRowActionsProps {\n entity: Entity;\n entityId: string;\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n selectionController: SelectionController;\n context: RebaseContext;\n}\n\n/**\n * Props for `entity.field.before` and `entity.field.after` slots.\n * Rendered around individual form fields in the entity edit view.\n * @group Plugins\n */\nexport interface EntityFieldSlotProps {\n propertyKey: string;\n property: Property;\n path: string;\n entityId?: string | number;\n collection: AdminCollection;\n context: RebaseContext;\n}\n\n/**\n * Props for `collection.filter-panel` slot.\n * Custom filter sidebar rendered alongside the collection table.\n * @group Plugins\n */\nexport interface CollectionFilterPanelProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n tableController: EntityTableController;\n context: RebaseContext;\n}\n\n/**\n * Props for `dashboard.widget` slot.\n * Widgets rendered on the home / dashboard page.\n * @group Plugins\n */\nexport interface DashboardWidgetProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `global.search` slot.\n * Cross-collection search bar rendered in the app shell.\n * @group Plugins\n */\nexport interface GlobalSearchProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `shell.toolbar` slot.\n * Actions rendered in the top-level toolbar / app bar area.\n * @group Plugins\n */\nexport interface ShellToolbarProps {\n context: RebaseContext;\n}\n\n/**\n * Props for `collection.widgets` slot.\n * Widgets rendered above the collection table.\n * @group Plugins\n */\nexport interface CollectionWidgetsSlotProps {\n path: string;\n collection: AdminCollection;\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n}\n\n/**\n * Props for `home.card.widget` slot.\n * Compact widget rendered inline in a home page collection card.\n * @group Plugins\n */\nexport interface HomeCardWidgetSlotProps {\n slug: string;\n collection: AdminCollection;\n context: RebaseContext;\n}\n"],"mappings":";;;;;;;;AAqbA,IAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;AC7YnC,SAAgB,eAAkB,WAAoD;CAClF,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAoB,WAAqD;CACrF,OAAO;AACX;;;;;;;;;;AC6HA,SAAgB,iBACZ,YACgB;CAChB,OAAO,oBAAoB,UAAU;AACzC;;;;;;;;ACyeA,IAAa,wBAAwB;;;;;;;;;;;;;AAoFrC,SAAgB,uBAGd,YAA4F;CAC1F,MAAM,QAAS,WAA2D;CAC1E,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EAAE,GAAI;EAAyC,GAAG;EAAO,OAAO;CAAM;AACjF;;;;;;;;;;;;;;AAeA,SAAgB,wBAGd,YAA6F;CAC3F,OAAO,wBAAwB,UAAqC;AACxE;;;;;;;ACjmBA,IAAa,uBAAuB;CAChC;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;ACtKA,IAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;ACuFjC,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ"}
|
package/dist/rebase_context.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AnalyticsController } from "./controllers/analytics_controller.js"
|
|
|
2
2
|
import type { AuthController } from "./controllers/auth.js";
|
|
3
3
|
import type { UserConfigurationPersistence } from "./controllers/local_config_persistence.js";
|
|
4
4
|
import type { DatabaseAdmin } from "@rebasepro/types";
|
|
5
|
-
import type { RebaseCallContext } from "@rebasepro/types";
|
|
5
|
+
import type { RebaseCallContext, RebaseClient } from "@rebasepro/types";
|
|
6
6
|
import type { User } from "@rebasepro/types";
|
|
7
7
|
/**
|
|
8
8
|
* Context that includes the internal controllers and contexts used by the app.
|
|
@@ -12,6 +12,13 @@ import type { User } from "@rebasepro/types";
|
|
|
12
12
|
* @see useRebaseContext
|
|
13
13
|
*/
|
|
14
14
|
export type RebaseContext<USER extends User = User, AuthControllerType extends AuthController<USER> = AuthController<USER>> = RebaseCallContext<USER> & {
|
|
15
|
+
/**
|
|
16
|
+
* The app's Rebase client, `data` included. Wider than the callback
|
|
17
|
+
* context's `client`, which leaves `data` off because a server-side
|
|
18
|
+
* callback's client has none — a collection callback's queries go through
|
|
19
|
+
* `context.data` on both sides.
|
|
20
|
+
*/
|
|
21
|
+
client: RebaseClient;
|
|
15
22
|
authController: AuthControllerType;
|
|
16
23
|
/**
|
|
17
24
|
* Controller mapping strings to collections
|
|
@@ -58,8 +58,11 @@ export interface FormContext<M extends Record<string, unknown> = Record<string,
|
|
|
58
58
|
*/
|
|
59
59
|
isSaving?: boolean;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* There is no form behind this context: the record is being shown, not
|
|
62
|
+
* edited, and `setFieldValue`, `save` and `submit` throw. Always paired
|
|
63
|
+
* with `disabled: true` — the read-only detail view, a record the user may
|
|
64
|
+
* not edit, and an entity view tab in the moment before the form has
|
|
65
|
+
* mounted all get one. Where the record is being edited this is absent.
|
|
63
66
|
*/
|
|
64
67
|
readOnly?: boolean;
|
|
65
68
|
}
|
|
@@ -90,7 +93,15 @@ export type EntityCustomView<M extends Record<string, unknown> = Record<string,
|
|
|
90
93
|
* and has full control over the UI.
|
|
91
94
|
*
|
|
92
95
|
* The form tab still appears in the tab bar but renders your Builder
|
|
93
|
-
* instead of the auto-generated field form.
|
|
96
|
+
* instead of the auto-generated field form. It is rendered *inside* the
|
|
97
|
+
* record's form, so on the edit screen `formContext` is that form's live
|
|
98
|
+
* context: `formContext.setFieldValue` edits the record, the identity bar's
|
|
99
|
+
* Save stores it, and closing with an edit pending asks first — the same as
|
|
100
|
+
* for a generated field.
|
|
101
|
+
*
|
|
102
|
+
* Where the record cannot be edited — the read-only detail view, or a user
|
|
103
|
+
* without permission to edit it — the Builder gets `formContext.disabled` and
|
|
104
|
+
* `formContext.readOnly` set, and writes throw.
|
|
94
105
|
*
|
|
95
106
|
* @group Models
|
|
96
107
|
*/
|
|
@@ -100,8 +111,11 @@ export type FormViewConfig<M extends Record<string, unknown> = Record<string, un
|
|
|
100
111
|
*/
|
|
101
112
|
Builder: ComponentRef<EntityCustomViewParams<M>>;
|
|
102
113
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
114
|
+
* Whether the identity bar offers Save, Save and close and Discard while
|
|
115
|
+
* this view is on screen. Set it to `false` when the Builder stores the
|
|
116
|
+
* record on its own terms, through `formContext.submit` or
|
|
117
|
+
* `formContext.save`. Delete is a record action, in the bar's overflow
|
|
118
|
+
* menu, and is offered either way. Defaults to true.
|
|
105
119
|
*/
|
|
106
120
|
includeActions?: boolean;
|
|
107
121
|
};
|
|
@@ -146,13 +146,20 @@ export interface AdminStringOptions extends AdminPropertyOptions {
|
|
|
146
146
|
*/
|
|
147
147
|
clearable?: boolean;
|
|
148
148
|
/**
|
|
149
|
-
* How to render a string that holds a URL:
|
|
150
|
-
*
|
|
149
|
+
* How to render a string that holds a URL: one of the supported media types
|
|
150
|
+
* for an inline preview, or `true` for a plain link.
|
|
151
151
|
*
|
|
152
152
|
* Only presentation. Whether the string *is* a URL is `url` on the property
|
|
153
|
-
* itself, which is what the OpenAPI contract is generated from
|
|
153
|
+
* itself, which is what the OpenAPI contract is generated from — and a
|
|
154
|
+
* property that declares `url: true` renders as a link with no help from
|
|
155
|
+
* here. Set this only to upgrade that link to an inline rendering of what it
|
|
156
|
+
* points at.
|
|
157
|
+
*
|
|
158
|
+
* `true` was always honoured at runtime (both the preview and the skeleton
|
|
159
|
+
* branch on `typeof … === "boolean"`) and was missing from this type, so the
|
|
160
|
+
* one value that means "just a link" was the one value that did not compile.
|
|
154
161
|
*/
|
|
155
|
-
urlPreview?: PreviewType;
|
|
162
|
+
urlPreview?: PreviewType | boolean;
|
|
156
163
|
}
|
|
157
164
|
/**
|
|
158
165
|
* How a number is written out for reading.
|
|
@@ -488,6 +488,26 @@ export interface RebaseTranslations {
|
|
|
488
488
|
some_entities_deleted: string;
|
|
489
489
|
error_deleting_entities: string;
|
|
490
490
|
deleted: string;
|
|
491
|
+
/**
|
|
492
|
+
* Selecting rows across a paginated view: the count, the escalation from
|
|
493
|
+
* "the ones I ticked" to "every row that matches", and what the bulk
|
|
494
|
+
* actions say while they work.
|
|
495
|
+
*/
|
|
496
|
+
selection_select_all_loaded: string;
|
|
497
|
+
selection_deselect_all: string;
|
|
498
|
+
selection_options: string;
|
|
499
|
+
selection_menu_all_loaded: string;
|
|
500
|
+
selection_menu_all_matching: string;
|
|
501
|
+
selection_menu_none: string;
|
|
502
|
+
selection_reading_rows: string;
|
|
503
|
+
selection_reading_rows_unknown: string;
|
|
504
|
+
selection_deleting_progress: string;
|
|
505
|
+
confirm_delete_selection: string;
|
|
506
|
+
confirm_delete_selection_unknown: string;
|
|
507
|
+
confirm_delete_selection_body: string;
|
|
508
|
+
/** What an export is about to download, once a selection narrows it. */
|
|
509
|
+
export_selection_count: string;
|
|
510
|
+
export_selection_all_matching: string;
|
|
491
511
|
select_reference: string;
|
|
492
512
|
select_references: string;
|
|
493
513
|
account_settings?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/cms-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Type definitions for the Rebase admin panel — CMS and Studio — the React-flavoured half of the type surface",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"node": ">=22.22.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@rebasepro/types": "0.
|
|
35
|
+
"@rebasepro/types": "0.21.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/jest": "^30.0.0",
|