@rebasepro/cms-types 0.17.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/admin_collection.d.ts +650 -0
- package/dist/augment.d.ts +81 -0
- package/dist/collections.d.ts +282 -0
- package/dist/controllers/analytics_controller.d.ts +7 -0
- package/dist/controllers/auth.d.ts +111 -0
- package/dist/controllers/customization_controller.d.ts +69 -0
- package/dist/controllers/dialogs_controller.d.ts +36 -0
- package/dist/controllers/index.d.ts +10 -0
- package/dist/controllers/local_config_persistence.d.ts +20 -0
- package/dist/controllers/navigation.d.ts +248 -0
- package/dist/controllers/registry.d.ts +96 -0
- package/dist/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/controllers/side_panel_controller.d.ts +97 -0
- package/dist/controllers/snackbar.d.ts +45 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.es.js +154 -0
- package/dist/index.es.js.map +1 -0
- package/dist/react_component_ref.d.ts +43 -0
- package/dist/rebase_context.d.ts +68 -0
- package/dist/types/breadcrumbs.d.ts +15 -0
- package/dist/types/builders.d.ts +15 -0
- package/dist/types/collection_views.d.ts +105 -0
- package/dist/types/component_overrides.d.ts +196 -0
- package/dist/types/entity_actions.d.ts +112 -0
- package/dist/types/entity_display.d.ts +148 -0
- package/dist/types/entity_link_builder.d.ts +7 -0
- package/dist/types/entity_views.d.ts +115 -0
- package/dist/types/export_import.d.ts +21 -0
- package/dist/types/form_layout.d.ts +126 -0
- package/dist/types/formex.d.ts +40 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/locales.d.ts +4 -0
- package/dist/types/modify_collections.d.ts +5 -0
- package/dist/types/plugins.d.ts +277 -0
- package/dist/types/property_config.d.ts +74 -0
- package/dist/types/property_options.d.ts +255 -0
- package/dist/types/slots.d.ts +279 -0
- package/dist/types/translations.d.ts +989 -0
- package/dist/types/user_management_delegate.d.ts +22 -0
- package/package.json +103 -0
- package/src/admin_collection.ts +775 -0
- package/src/augment.ts +79 -0
- package/src/collections.ts +312 -0
- package/src/controllers/analytics_controller.tsx +57 -0
- package/src/controllers/auth.ts +122 -0
- package/src/controllers/customization_controller.tsx +81 -0
- package/src/controllers/dialogs_controller.tsx +37 -0
- package/src/controllers/index.ts +10 -0
- package/src/controllers/local_config_persistence.tsx +22 -0
- package/src/controllers/navigation.ts +288 -0
- package/src/controllers/registry.ts +114 -0
- package/src/controllers/side_dialogs_controller.tsx +82 -0
- package/src/controllers/side_panel_controller.tsx +112 -0
- package/src/controllers/snackbar.ts +51 -0
- package/src/index.ts +20 -0
- package/src/react_component_ref.ts +52 -0
- package/src/rebase_context.ts +81 -0
- package/src/types/breadcrumbs.ts +16 -0
- package/src/types/builders.ts +18 -0
- package/src/types/collection_views.tsx +125 -0
- package/src/types/component_overrides.ts +244 -0
- package/src/types/entity_actions.tsx +134 -0
- package/src/types/entity_display.ts +182 -0
- package/src/types/entity_link_builder.ts +8 -0
- package/src/types/entity_views.tsx +135 -0
- package/src/types/export_import.ts +26 -0
- package/src/types/form_layout.ts +137 -0
- package/src/types/formex.ts +45 -0
- package/src/types/index.ts +18 -0
- package/src/types/locales.ts +81 -0
- package/src/types/modify_collections.tsx +6 -0
- package/src/types/plugins.tsx +346 -0
- package/src/types/property_config.tsx +97 -0
- package/src/types/property_options.ts +300 -0
- package/src/types/slots.tsx +334 -0
- package/src/types/translations.ts +1104 -0
- package/src/types/user_management_delegate.ts +23 -0
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ADMIN_COLLECTION_KEYS as ADMIN_COLLECTION_KEYS$1, ADMIN_PROPERTY_KEYS as ADMIN_PROPERTY_KEYS$1, nestAdminCollectionKeys } from "@rebasepro/types";
|
|
2
|
+
//#region src/types/property_options.ts
|
|
3
|
+
/**
|
|
4
|
+
* Core's list, re-exported through the same `satisfies` agreement check that
|
|
5
|
+
* {@link ADMIN_COLLECTION_KEYS} gets: core owns the data because the boot-time
|
|
6
|
+
* collection validator in `@rebasepro/server` needs it and may not import this
|
|
7
|
+
* package, and this clause is what stops the data from drifting off the types.
|
|
8
|
+
*/
|
|
9
|
+
var ADMIN_PROPERTY_KEYS = ADMIN_PROPERTY_KEYS$1;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/react_component_ref.ts
|
|
12
|
+
/**
|
|
13
|
+
* The `ComponentLike` contract, as a signature the compiler has to keep true.
|
|
14
|
+
*
|
|
15
|
+
* The split rests on one claim: **every form a React component takes is
|
|
16
|
+
* assignable to `ComponentLike`** — function components, class components,
|
|
17
|
+
* `memo`, `forwardRef`. If that stopped holding, core's `ComponentRef` would
|
|
18
|
+
* quietly begin rejecting real components, and the failure would surface far away
|
|
19
|
+
* in whichever collection file happened to use the broken form.
|
|
20
|
+
*
|
|
21
|
+
* So the claim is not left to a test that someone has to run. This function's
|
|
22
|
+
* parameter and return types state it, and `pnpm typecheck` enforces it on every
|
|
23
|
+
* commit. It is also useful on its own: an explicit widening at the point where
|
|
24
|
+
* an authored component enters a collection config.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { MyField } from "./MyField";
|
|
28
|
+
* admin: { Field: asComponentRef(MyField) }
|
|
29
|
+
*/
|
|
30
|
+
function asComponentRef(component) {
|
|
31
|
+
return component;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The same contract in the other direction: a `ComponentLike` is only renderable
|
|
35
|
+
* once narrowed, and this is the single sanctioned place that narrowing is
|
|
36
|
+
* spelled out. `resolveComponentRef` in `@rebasepro/app` does the runtime half.
|
|
37
|
+
*/
|
|
38
|
+
function asReactComponent(component) {
|
|
39
|
+
return component;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/admin_collection.ts
|
|
43
|
+
/** Identity at runtime; the overloads above are the whole point. @group Builder */
|
|
44
|
+
function defineCollection(collection) {
|
|
45
|
+
return collection;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Core's list, re-exported through a `satisfies` clause that is the agreement
|
|
49
|
+
* check: a key core names that is not an option here fails to compile, and
|
|
50
|
+
* `satisfies` keeps the literal tuple type rather than widening it to `string[]`.
|
|
51
|
+
*
|
|
52
|
+
* The reverse direction — an option missing from core's list — has no type-level
|
|
53
|
+
* expression, since there is no exhaustiveness check over an optional-property
|
|
54
|
+
* keyof. `test/admin_collection.test.ts` counts them instead.
|
|
55
|
+
*/
|
|
56
|
+
var ADMIN_COLLECTION_KEYS = ADMIN_COLLECTION_KEYS$1;
|
|
57
|
+
/**
|
|
58
|
+
* Flatten a collection's `admin` block onto it, producing the panel's view model.
|
|
59
|
+
*
|
|
60
|
+
* Shallow by design: the block's fields are independent, so a deep merge would
|
|
61
|
+
* only create opportunities for a nested object to be half from one source and
|
|
62
|
+
* half from the other. `admin` survives on the result.
|
|
63
|
+
*
|
|
64
|
+
* Idempotent — flattening an already-flat collection returns an equivalent one —
|
|
65
|
+
* because the panel resolves collections at more than one entry point (the
|
|
66
|
+
* registry, `<Rebase collections>`, a plugin's `modifyCollection`) and they must
|
|
67
|
+
* not fight over which has run.
|
|
68
|
+
*/
|
|
69
|
+
function resolveAdminCollection(collection) {
|
|
70
|
+
const block = collection.admin;
|
|
71
|
+
if (!block) return collection;
|
|
72
|
+
return {
|
|
73
|
+
...collection,
|
|
74
|
+
...block,
|
|
75
|
+
admin: block
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The inverse: lift flattened admin fields back into the block.
|
|
80
|
+
*
|
|
81
|
+
* Used on the way out — persisting from the collection editor, or handing a
|
|
82
|
+
* collection to anything that expects the authoring shape. Any key in
|
|
83
|
+
* {@link ADMIN_COLLECTION_KEYS} found at the top level is moved down, so a
|
|
84
|
+
* round trip through the panel does not leave the file flat.
|
|
85
|
+
*
|
|
86
|
+
* The nesting itself lives in `@rebasepro/types` because the schema editor in
|
|
87
|
+
* `@rebasepro/server` — which cannot import this package — has to do exactly the
|
|
88
|
+
* same thing when it writes a collection file back to disk. Two copies of the
|
|
89
|
+
* rule disagreed on which side wins, and the disagreement was invisible.
|
|
90
|
+
*/
|
|
91
|
+
function toAdminCollectionConfig(collection) {
|
|
92
|
+
return nestAdminCollectionKeys(collection);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/types/entity_display.ts
|
|
96
|
+
/**
|
|
97
|
+
* The roles as data, so every consumer iterates the same list instead of
|
|
98
|
+
* repeating it — the mistake that let `titleProperty` grow seven readers.
|
|
99
|
+
*/
|
|
100
|
+
var ENTITY_DISPLAY_ROLES = [
|
|
101
|
+
"title",
|
|
102
|
+
"subtitle",
|
|
103
|
+
"image",
|
|
104
|
+
"status",
|
|
105
|
+
"date",
|
|
106
|
+
"tags"
|
|
107
|
+
];
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/types/form_layout.ts
|
|
110
|
+
/**
|
|
111
|
+
* The number of columns the form grid is divided into. A field's
|
|
112
|
+
* {@link AdminPropertyOptions.span} is expressed against this.
|
|
113
|
+
*
|
|
114
|
+
* Fixed rather than configurable on purpose: the whole point of a span is that
|
|
115
|
+
* two fields written by two different people line up, and they only do that if
|
|
116
|
+
* everyone is counting against the same grid.
|
|
117
|
+
*
|
|
118
|
+
* @group Models
|
|
119
|
+
*/
|
|
120
|
+
var FORM_GRID_COLUMNS = 4;
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/types/slots.tsx
|
|
123
|
+
/**
|
|
124
|
+
* Valid slot names for UI extension points.
|
|
125
|
+
* @group Plugins
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* Slots this build declares but renders nowhere.
|
|
129
|
+
*
|
|
130
|
+
* Every name here appears in {@link SlotRegistry}, has a props interface, and
|
|
131
|
+
* is listed in the public slot reference alongside the ones that work — so a
|
|
132
|
+
* plugin author picks one off the table, registers a component, sees nothing,
|
|
133
|
+
* and has no way to tell whether the fault is theirs. Seven of twenty-nine were
|
|
134
|
+
* in that state.
|
|
135
|
+
*
|
|
136
|
+
* This is a statement of fact, not a wish list: `slot-render-sites.test.ts`
|
|
137
|
+
* derives the same set by scanning for render sites and fails when the two
|
|
138
|
+
* disagree. Implementing a slot therefore forces its removal from here, and
|
|
139
|
+
* declaring one without rendering it forces its addition — at which point
|
|
140
|
+
* `Rebase` warns anyone who registers for it, which is the whole point.
|
|
141
|
+
*/
|
|
142
|
+
var UNRENDERED_SLOTS = [
|
|
143
|
+
"collection.filter-panel",
|
|
144
|
+
"dashboard.widget",
|
|
145
|
+
"entity.field.after",
|
|
146
|
+
"entity.field.before",
|
|
147
|
+
"entity.row.actions",
|
|
148
|
+
"global.search",
|
|
149
|
+
"shell.toolbar"
|
|
150
|
+
];
|
|
151
|
+
//#endregion
|
|
152
|
+
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ENTITY_DISPLAY_ROLES, FORM_GRID_COLUMNS, UNRENDERED_SLOTS, asComponentRef, asReactComponent, defineCollection, resolveAdminCollection, toAdminCollectionConfig };
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=index.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/types/property_options.ts","../src/react_component_ref.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 columnWidth?: number;\n hideFromCollection?: boolean;\n readOnly?: boolean;\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 customProps?: CustomProps;\n Field?: ComponentRef<any>;\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 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 minimumFractionDigits?: number;\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 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 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 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 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 expanded?: boolean;\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 expanded?: boolean;\n minimalistView?: boolean;\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","import type React from \"react\";\nimport type { ComponentLike, ComponentRef, LazyComponentRef } from \"@rebasepro/types\";\n\n/**\n * `ComponentRef`, narrowed to real React types.\n *\n * Core's {@link ComponentRef} describes a component structurally\n * ({@link ComponentLike}) so that `properties.ts` — and therefore the whole\n * property model the backend reads — can live without React. The trade is that\n * the return type is `unknown`, so a function returning something React cannot\n * render type-checks there.\n *\n * Use this type wherever React genuinely exists: authoring a collection's admin\n * options, and inside the admin packages. Assignments flow into core unchanged,\n * because every member of this union is a member of that one.\n */\nexport type ReactComponentRef<P = any> =\n | string\n | LazyComponentRef<P>\n | (() => Promise<{ default: React.ComponentType<P> }>)\n | React.ComponentType<P>;\n\n/**\n * The `ComponentLike` contract, as a signature the compiler has to keep true.\n *\n * The split rests on one claim: **every form a React component takes is\n * assignable to `ComponentLike`** — function components, class components,\n * `memo`, `forwardRef`. If that stopped holding, core's `ComponentRef` would\n * quietly begin rejecting real components, and the failure would surface far away\n * in whichever collection file happened to use the broken form.\n *\n * So the claim is not left to a test that someone has to run. This function's\n * parameter and return types state it, and `pnpm typecheck` enforces it on every\n * commit. It is also useful on its own: an explicit widening at the point where\n * an authored component enters a collection config.\n *\n * @example\n * import { MyField } from \"./MyField\";\n * admin: { Field: asComponentRef(MyField) }\n */\nexport function asComponentRef<P>(component: React.ComponentType<P>): ComponentRef<P> {\n return component;\n}\n\n/**\n * The same contract in the other direction: a `ComponentLike` is only renderable\n * once narrowed, and this is the single sanctioned place that narrowing is\n * spelled out. `resolveComponentRef` in `@rebasepro/app` does the runtime half.\n */\nexport function asReactComponent<P>(component: ComponentLike<P>): React.ComponentType<P> {\n return component as React.ComponentType<P>;\n}\n","/**\n * The typed admin block, and the type you author a collection against.\n *\n * A collection is one file. Schema, security rules and callbacks sit at the top\n * level, where the backend reads them; everything the admin panel renders sits\n * under `admin`. `@rebasepro/types` does not declare that field at all — naming a\n * kanban column definition would drag `React.ReactNode` back into the BaaS\n * contract, and a server has no use for one. `augment.ts` in this package declares\n * it, by declaration merging, onto core's `CollectionConfig`. So this is the other\n * side of that boundary: the 38 fields, fully typed, in the package where React\n * exists, and reachable only by a program that has opted in.\n *\n * Each field is declared exactly once, here. Core does not carry a React-free\n * skeleton of the same shape; two definitions that agree only by luck is the\n * `WhereFilterOp` mistake, and this block is far bigger than one union.\n */\nimport type React from \"react\";\nimport type {\n CollectionConfig,\n ComponentRef,\n FilterPreset,\n FilterValues,\n FirebaseCollectionConfig,\n FirebaseProperties,\n InferEntityType,\n MongoDBCollectionConfig,\n MongoProperties,\n OrderBySpec,\n PostgresCollectionConfig,\n PostgresProperties,\n User\n} from \"@rebasepro/types\";\n// A value, not a type: the runtime list core owns.\nimport { ADMIN_COLLECTION_KEYS as CORE_ADMIN_COLLECTION_KEYS, 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 * 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 * 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 P extends PostgresProperties,\n USER extends User = User\n>(\n collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a Firestore-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends FirebaseProperties,\n USER extends User = User\n>(\n collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Define a MongoDB-backed collection with the admin block checked. @group Builder */\nexport function defineCollection<\n const P extends MongoProperties,\n USER extends User = User\n>(\n collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, \"properties\">\n & { properties: P }\n): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };\n\n/** Identity at runtime; the overloads above are the whole point. @group Builder */\nexport function defineCollection(collection: CollectionConfig): CollectionConfig {\n return collection;\n}\n\n/**\n * Re-exported from `@rebasepro/types`, where the list has to live: the ts-morph\n * schema editor in `@rebasepro/server` needs it to know which keys go inside the\n * block when it rewrites a collection file, and a core package may not import\n * this one. The list is plain data, so core is a fine home for it.\n *\n * What core *cannot* do is check the list against the type. That happens here.\n *\n * @group Models\n */\nexport type { AdminCollectionKey } from \"@rebasepro/types\";\n\n/**\n * Core's list, re-exported through a `satisfies` clause that is the agreement\n * check: a key core names that is not an option here fails to compile, and\n * `satisfies` keeps the literal tuple type rather than widening it to `string[]`.\n *\n * The reverse direction — an option missing from core's list — has no type-level\n * expression, since there is no exhaustiveness check over an optional-property\n * keyof. `test/admin_collection.test.ts` counts them instead.\n */\nexport const ADMIN_COLLECTION_KEYS = CORE_ADMIN_COLLECTION_KEYS satisfies readonly (keyof AdminCollectionOptions)[];\n\n\n/**\n * A collection as the admin panel works with it: the contract with the `admin`\n * block flattened onto the top level.\n *\n * The panel reads presentation fields in a few hundred places, and threading\n * `collection.admin?.propertiesOrder` through all of them would be noise that\n * buys nothing — the panel has already resolved the collection by then, merging\n * the declared config with the user's per-collection overrides from local\n * storage. So the panel gets a flat *view model*, exactly as it already does for\n * entities (`Entity` is an admin view model over flat rows, not a wire type).\n *\n * The distinction that matters is direction:\n *\n * - **Reading** a resolved collection → `AdminCollection` (flat, convenient).\n * - **Authoring or persisting** one → core's `CollectionConfig`, with the `admin`\n * block this package augments onto it (nested, which is what the file on disk\n * and the wire both look like).\n *\n * `admin` is kept alongside the flattened fields so the collection editor can\n * still see the block it has to write back.\n *\n * @group Models\n */\nexport type AdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = WithFlatAdmin<CollectionConfig<M, USER>, M, USER>;\n\n/**\n * Flatten the admin block onto one member of the collection union at a time.\n *\n * `CollectionConfig` is a union discriminated on `engine`\n * (Postgres | Firestore | MongoDB), and a bare `Omit<Union, \"admin\">` collapses it\n * into a single object type with the discriminant widened. The result stops being\n * assignable back to `CollectionConfig`, so every call that hands a resolved\n * collection to a core function fails — which is exactly what happened. The\n * `C extends unknown` clause makes the mapping distributive, so each member keeps\n * its literal `engine` and stays assignable to its counterpart.\n */\ntype WithFlatAdmin<C, M extends Record<string, unknown>, USER extends User> =\n C extends unknown\n ? Omit<C, \"admin\"> & AdminCollectionOptions<M, USER> & { admin?: AdminCollectionOptions<M, USER> }\n : never;\n\n/** {@link AdminCollection} for a Postgres collection. @group Models */\nexport type AdminPostgresCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n> = Omit<PostgresCollectionConfig<M, USER>, \"admin\">\n & AdminCollectionOptions<M, USER>\n & { admin?: AdminCollectionOptions<M, USER> };\n\n/**\n * Flatten a collection's `admin` block onto it, producing the panel's view model.\n *\n * Shallow by design: the block's fields are independent, so a deep merge would\n * only create opportunities for a nested object to be half from one source and\n * half from the other. `admin` survives on the result.\n *\n * Idempotent — flattening an already-flat collection returns an equivalent one —\n * because the panel resolves collections at more than one entry point (the\n * registry, `<Rebase collections>`, a plugin's `modifyCollection`) and they must\n * not fight over which has run.\n */\nexport function resolveAdminCollection<\n M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User\n>(collection: CollectionConfig<M, USER> | AdminCollection<M, USER>): AdminCollection<M, USER> {\n const block = (collection as { admin?: AdminCollectionOptions<M, USER> }).admin;\n if (!block) return collection as AdminCollection<M, USER>;\n return { ...(collection as AdminCollection<M, USER>), ...block, admin: block };\n}\n\n/**\n * The inverse: lift flattened admin fields back into the block.\n *\n * Used on the way out — persisting from the collection editor, or handing a\n * collection to anything that expects the authoring shape. Any key in\n * {@link ADMIN_COLLECTION_KEYS} found at the top level is moved down, so a\n * round trip through the panel does not leave the file flat.\n *\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.\n * Typed loosely so mixed-slot arrays work.\n * Type safety is provided at the `useSlot` call site.\n */\n Component: React.ComponentType<any>;\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// ── 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":";;;;;;;;AA2SA,IAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;ACnQnC,SAAgB,eAAkB,WAAoD;CAClF,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAoB,WAAqD;CACrF,OAAO;AACX;;;;AC4lBA,SAAgB,iBAAiB,YAAgD;CAC7E,OAAO;AACX;;;;;;;;;;AAuBA,IAAa,wBAAwB;;;;;;;;;;;;;AAmErC,SAAgB,uBAGd,YAA4F;CAC1F,MAAM,QAAS,WAA2D;CAC1E,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EAAE,GAAI;EAAyC,GAAG;EAAO,OAAO;CAAM;AACjF;;;;;;;;;;;;;;AAeA,SAAgB,wBAGd,YAA6F;CAC3F,OAAO,wBAAwB,UAAqC;AACxE;;;;;;;AC3lBA,IAAa,uBAAuB;CAChC;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;ACtKA,IAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;ACuFjC,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { ComponentLike, ComponentRef, LazyComponentRef } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* `ComponentRef`, narrowed to real React types.
|
|
5
|
+
*
|
|
6
|
+
* Core's {@link ComponentRef} describes a component structurally
|
|
7
|
+
* ({@link ComponentLike}) so that `properties.ts` — and therefore the whole
|
|
8
|
+
* property model the backend reads — can live without React. The trade is that
|
|
9
|
+
* the return type is `unknown`, so a function returning something React cannot
|
|
10
|
+
* render type-checks there.
|
|
11
|
+
*
|
|
12
|
+
* Use this type wherever React genuinely exists: authoring a collection's admin
|
|
13
|
+
* options, and inside the admin packages. Assignments flow into core unchanged,
|
|
14
|
+
* because every member of this union is a member of that one.
|
|
15
|
+
*/
|
|
16
|
+
export type ReactComponentRef<P = any> = string | LazyComponentRef<P> | (() => Promise<{
|
|
17
|
+
default: React.ComponentType<P>;
|
|
18
|
+
}>) | React.ComponentType<P>;
|
|
19
|
+
/**
|
|
20
|
+
* The `ComponentLike` contract, as a signature the compiler has to keep true.
|
|
21
|
+
*
|
|
22
|
+
* The split rests on one claim: **every form a React component takes is
|
|
23
|
+
* assignable to `ComponentLike`** — function components, class components,
|
|
24
|
+
* `memo`, `forwardRef`. If that stopped holding, core's `ComponentRef` would
|
|
25
|
+
* quietly begin rejecting real components, and the failure would surface far away
|
|
26
|
+
* in whichever collection file happened to use the broken form.
|
|
27
|
+
*
|
|
28
|
+
* So the claim is not left to a test that someone has to run. This function's
|
|
29
|
+
* parameter and return types state it, and `pnpm typecheck` enforces it on every
|
|
30
|
+
* commit. It is also useful on its own: an explicit widening at the point where
|
|
31
|
+
* an authored component enters a collection config.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* import { MyField } from "./MyField";
|
|
35
|
+
* admin: { Field: asComponentRef(MyField) }
|
|
36
|
+
*/
|
|
37
|
+
export declare function asComponentRef<P>(component: React.ComponentType<P>): ComponentRef<P>;
|
|
38
|
+
/**
|
|
39
|
+
* The same contract in the other direction: a `ComponentLike` is only renderable
|
|
40
|
+
* once narrowed, and this is the single sanctioned place that narrowing is
|
|
41
|
+
* spelled out. `resolveComponentRef` in `@rebasepro/app` does the runtime half.
|
|
42
|
+
*/
|
|
43
|
+
export declare function asReactComponent<P>(component: ComponentLike<P>): React.ComponentType<P>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AnalyticsController } from "./controllers/analytics_controller.js";
|
|
2
|
+
import type { AuthController } from "./controllers/auth.js";
|
|
3
|
+
import type { UserConfigurationPersistence } from "./controllers/local_config_persistence.js";
|
|
4
|
+
import type { DatabaseAdmin } from "@rebasepro/types";
|
|
5
|
+
import type { RebaseCallContext } from "@rebasepro/types";
|
|
6
|
+
import type { User } from "@rebasepro/types";
|
|
7
|
+
/**
|
|
8
|
+
* Context that includes the internal controllers and contexts used by the app.
|
|
9
|
+
* Some controllers and context included in this context can be accessed
|
|
10
|
+
* directly from their respective hooks.
|
|
11
|
+
* @group Hooks and utilities
|
|
12
|
+
* @see useRebaseContext
|
|
13
|
+
*/
|
|
14
|
+
export type RebaseContext<USER extends User = User, AuthControllerType extends AuthController<USER> = AuthController<USER>> = RebaseCallContext<USER> & {
|
|
15
|
+
authController: AuthControllerType;
|
|
16
|
+
/**
|
|
17
|
+
* Controller mapping strings to collections
|
|
18
|
+
*/
|
|
19
|
+
collectionRegistryController?: import("@rebasepro/types").CollectionRegistryController;
|
|
20
|
+
/**
|
|
21
|
+
* Controller for navigation state
|
|
22
|
+
*/
|
|
23
|
+
navigationStateController?: import("./controllers/navigation.js").NavigationStateController;
|
|
24
|
+
/**
|
|
25
|
+
* Controller for side dialogs (side sheets)
|
|
26
|
+
*/
|
|
27
|
+
sideDialogsController?: import("./controllers/side_dialogs_controller.js").SideDialogsController;
|
|
28
|
+
/**
|
|
29
|
+
* Controller to open the side panel displaying entity forms
|
|
30
|
+
*/
|
|
31
|
+
sidePanelController?: import("./controllers/side_panel_controller.js").SidePanelController;
|
|
32
|
+
/**
|
|
33
|
+
* Controller resolving URLs in the admin
|
|
34
|
+
*/
|
|
35
|
+
urlController?: import("./controllers/navigation.js").UrlController;
|
|
36
|
+
/**
|
|
37
|
+
* Controller to handle simple confirmation and alert dialogs
|
|
38
|
+
*/
|
|
39
|
+
dialogsController?: import("./controllers/dialogs_controller.js").DialogsController;
|
|
40
|
+
/**
|
|
41
|
+
* Controller for admin customization
|
|
42
|
+
*/
|
|
43
|
+
customizationController?: import("./controllers/customization_controller.js").CustomizationController;
|
|
44
|
+
/**
|
|
45
|
+
* Controller for effective role
|
|
46
|
+
*/
|
|
47
|
+
effectiveRoleController?: {
|
|
48
|
+
effectiveRole: string | null;
|
|
49
|
+
setEffectiveRole: (role: string | null) => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Use this controller to access data stored in the browser for the user
|
|
53
|
+
*/
|
|
54
|
+
userConfigPersistence?: UserConfigurationPersistence;
|
|
55
|
+
/**
|
|
56
|
+
* Callback to send analytics events
|
|
57
|
+
*/
|
|
58
|
+
analyticsController?: AnalyticsController;
|
|
59
|
+
/**
|
|
60
|
+
* Administrative database operations (SQL, schema discovery).
|
|
61
|
+
* Only available in developer/admin contexts.
|
|
62
|
+
*/
|
|
63
|
+
databaseAdmin?: DatabaseAdmin;
|
|
64
|
+
/**
|
|
65
|
+
* Controller for snackbars
|
|
66
|
+
*/
|
|
67
|
+
snackbarController?: import("./controllers/snackbar.js").SnackbarController;
|
|
68
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface BreadcrumbEntry {
|
|
2
|
+
title: string;
|
|
3
|
+
url: string;
|
|
4
|
+
/**
|
|
5
|
+
* Stable identifier for this entry (e.g., collection path). Lets the
|
|
6
|
+
* provider tell a rebuilt-but-unchanged trail from a real navigation.
|
|
7
|
+
*/
|
|
8
|
+
id?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BreadcrumbsController {
|
|
11
|
+
breadcrumbs: BreadcrumbEntry[];
|
|
12
|
+
set: (props: {
|
|
13
|
+
breadcrumbs: BreadcrumbEntry[];
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuthController } from "../controllers/auth.js";
|
|
2
|
+
import type { RebaseData } from "@rebasepro/types";
|
|
3
|
+
import type { User } from "@rebasepro/types";
|
|
4
|
+
import type { AppView } from "../controllers/navigation.js";
|
|
5
|
+
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
6
|
+
export type CollectionConfigsBuilder<EC extends AdminCollection = AdminCollection> = (params: {
|
|
7
|
+
user: User | null;
|
|
8
|
+
authController: AuthController;
|
|
9
|
+
data: RebaseData;
|
|
10
|
+
}) => EC[] | Promise<EC[]>;
|
|
11
|
+
export type AppViewsBuilder = (params: {
|
|
12
|
+
user: User | null;
|
|
13
|
+
authController: AuthController;
|
|
14
|
+
data: RebaseData;
|
|
15
|
+
}) => AppView[] | Promise<AppView[]>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Entity, ComponentRef } from "@rebasepro/types";
|
|
3
|
+
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
4
|
+
import type { CollectionSize, EntityTableController, SelectionController, ViewMode } from "../collections.js";
|
|
5
|
+
/**
|
|
6
|
+
* A custom rendering of *one* collection's rows, selectable from the same view
|
|
7
|
+
* switcher as list / table / cards / kanban.
|
|
8
|
+
*
|
|
9
|
+
* This is the middle scope of custom UI. The other two already exist and this
|
|
10
|
+
* is not a substitute for either:
|
|
11
|
+
*
|
|
12
|
+
* - one record → `entityViews` (the entity tab strip)
|
|
13
|
+
* - one collection's rows, rendered differently → **this**
|
|
14
|
+
* - a workflow spanning several collections → `AppView` (`views={[…]}`)
|
|
15
|
+
*
|
|
16
|
+
* A view mode is another rendering of the *same query*. The `Builder` is handed
|
|
17
|
+
* the live {@link EntityTableController}, so it inherits the collection's
|
|
18
|
+
* filters, search string, sort, pagination, role checks and entity side panel
|
|
19
|
+
* for free — which is the whole reason to declare one instead of an `AppView`.
|
|
20
|
+
*
|
|
21
|
+
* If your component ignores `tableController` and fetches tables of its own, it
|
|
22
|
+
* wants to be an `AppView`: the toolbar above it — search box, filters, the
|
|
23
|
+
* record count — would be describing a query the view does not render.
|
|
24
|
+
*
|
|
25
|
+
* @group Models
|
|
26
|
+
*/
|
|
27
|
+
export type CollectionCustomView<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
28
|
+
/**
|
|
29
|
+
* Identifies this view. It is what `defaultViewMode` and `enabledViews`
|
|
30
|
+
* name, and what the `__view` URL param carries, so it must not collide
|
|
31
|
+
* with a built-in mode ("list", "table", "cards", "kanban").
|
|
32
|
+
*
|
|
33
|
+
* Treat it as frozen once shipped: users have it persisted in their saved
|
|
34
|
+
* collection config and in bookmarked URLs.
|
|
35
|
+
*/
|
|
36
|
+
key: string;
|
|
37
|
+
/** Label shown in the view switcher. */
|
|
38
|
+
name: string;
|
|
39
|
+
/**
|
|
40
|
+
* Icon shown beside the name, as a `lucide-react` icon name (e.g. `"Map"`)
|
|
41
|
+
* or a rendered node. A name is what the collection editor can store.
|
|
42
|
+
*/
|
|
43
|
+
icon?: string | React.ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* The component that renders the rows.
|
|
46
|
+
*/
|
|
47
|
+
Builder: ComponentRef<CollectionCustomViewParams<M>>;
|
|
48
|
+
/**
|
|
49
|
+
* How clicking a record should present it, overriding what
|
|
50
|
+
* `resolveOpenEntityMode` would otherwise derive from the view mode.
|
|
51
|
+
* Defaults to `"side_panel"`, which is what the board uses: a custom view
|
|
52
|
+
* usually owns its whole surface and should keep it.
|
|
53
|
+
*/
|
|
54
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
55
|
+
/**
|
|
56
|
+
* Whether the size selector applies to this view. Off by default — most
|
|
57
|
+
* custom views have no notion of row height.
|
|
58
|
+
*/
|
|
59
|
+
sizeable?: boolean;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* What a {@link CollectionCustomView}'s `Builder` receives.
|
|
63
|
+
*
|
|
64
|
+
* This is the same set the built-in view bindings are given, so a custom view
|
|
65
|
+
* starts from parity with them.
|
|
66
|
+
*
|
|
67
|
+
* @group Models
|
|
68
|
+
*/
|
|
69
|
+
export interface CollectionCustomViewParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
70
|
+
/** The collection being rendered, fully resolved. */
|
|
71
|
+
collection: AdminCollection<M>;
|
|
72
|
+
/**
|
|
73
|
+
* The live query: rows, loading state, pagination, and the filter / sort /
|
|
74
|
+
* search state shared with the toolbar. Read rows from here rather than
|
|
75
|
+
* fetching — that is what keeps the toolbar honest.
|
|
76
|
+
*/
|
|
77
|
+
tableController: EntityTableController<M>;
|
|
78
|
+
/** Full path of the collection, e.g. `users/1234/addresses`. */
|
|
79
|
+
path: string;
|
|
80
|
+
/** Parent path segments, when this is a subcollection. */
|
|
81
|
+
parentCollectionSlugs?: string[];
|
|
82
|
+
parentEntityIds?: string[];
|
|
83
|
+
/**
|
|
84
|
+
* Open a record. Routes through the collection's resolved
|
|
85
|
+
* `openEntityMode`, so the side panel, split view and full-screen form all
|
|
86
|
+
* work without the view knowing which one it got.
|
|
87
|
+
*/
|
|
88
|
+
onEntityClick?: (entity: Entity<M>) => void;
|
|
89
|
+
/** Create a record. Undefined when the user may not create. */
|
|
90
|
+
onNewClick?: () => void;
|
|
91
|
+
/** Whether the current user may create records in this collection. */
|
|
92
|
+
canCreate?: boolean;
|
|
93
|
+
selectionController?: SelectionController<M>;
|
|
94
|
+
selectionEnabled?: boolean;
|
|
95
|
+
/** Records to draw as highlighted, e.g. the one open in the side panel. */
|
|
96
|
+
highlightedEntities?: Entity<M>[];
|
|
97
|
+
/** Records deleted in this session, for optimistic removal. */
|
|
98
|
+
deletedEntities?: Entity<M>[];
|
|
99
|
+
/** The shared empty state, so a custom view matches the built-ins. */
|
|
100
|
+
emptyComponent?: React.ReactNode;
|
|
101
|
+
/** Set when the view declared `sizeable`. */
|
|
102
|
+
size?: CollectionSize;
|
|
103
|
+
/** The mode this view was resolved to, for views that render several. */
|
|
104
|
+
viewMode?: ViewMode;
|
|
105
|
+
}
|