@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { Property } from "@rebasepro/types";
|
|
3
|
+
import type { WhereFilterOp } from "@rebasepro/types";
|
|
4
|
+
/**
|
|
5
|
+
* Components that can only be overridden at the **app level** via the
|
|
6
|
+
* `components` prop on `<Rebase>`.
|
|
7
|
+
*
|
|
8
|
+
* These are shell-level / global components that exist outside of any
|
|
9
|
+
* specific collection context.
|
|
10
|
+
*
|
|
11
|
+
* @group Component Overrides
|
|
12
|
+
*/
|
|
13
|
+
export type AppComponentName = "Shell.AppBar" | "Shell.Drawer" | "Shell.DrawerNavigationItem" | "Shell.DrawerNavigationGroup" | "HomePage" | "HomePage.CollectionCard" | "Auth.LoginView";
|
|
14
|
+
/**
|
|
15
|
+
* Components that can be overridden at the **collection level**
|
|
16
|
+
* (on an individual collection definition) or at the **app level**
|
|
17
|
+
* (as a default for all collections).
|
|
18
|
+
*
|
|
19
|
+
* When set at the app level, these act as defaults. When set on a
|
|
20
|
+
* specific collection, they override the app-level default for that
|
|
21
|
+
* collection only.
|
|
22
|
+
*
|
|
23
|
+
* @group Component Overrides
|
|
24
|
+
*/
|
|
25
|
+
export type CollectionComponentName = "Collection.View" | "Collection.Table" | "Collection.Card" | "Collection.EmptyState" | "Collection.Actions" | "Collection.FilterField" | "Entity.Form" | "EditView.FormActions" | "DetailView" | "Entity.SidePanel" | "EntityPreview" | "Entity.MissingReference";
|
|
26
|
+
/**
|
|
27
|
+
* All overridable component names across all scopes.
|
|
28
|
+
* @group Component Overrides
|
|
29
|
+
*/
|
|
30
|
+
export type OverridableComponentName = AppComponentName | CollectionComponentName;
|
|
31
|
+
/**
|
|
32
|
+
* Props received by a filter field component — whether it is a built-in
|
|
33
|
+
* per-type field, a property-level replacement (`property.ui.Filter`), or a
|
|
34
|
+
* `"Collection.FilterField"` override.
|
|
35
|
+
*
|
|
36
|
+
* The `operators` list is **already resolved**: it is the intersection of the
|
|
37
|
+
* engine's {@link DataSourceCapabilities.filterOperators}, the property-type
|
|
38
|
+
* defaults, and any `property.ui.filterOperators` narrowing. A custom field
|
|
39
|
+
* should only offer operators from this list — anything else may throw at
|
|
40
|
+
* query time on engines that cannot execute it.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function MyStatusFilter({ value, setValue, operators }: FilterFieldBindingProps) {
|
|
45
|
+
* return (
|
|
46
|
+
* <select
|
|
47
|
+
* value={value?.[1] as string ?? ""}
|
|
48
|
+
* onChange={e => setValue(e.target.value ? ["==", e.target.value] : undefined)}>
|
|
49
|
+
* <option value="">Any</option>
|
|
50
|
+
* <option value="active">Active</option>
|
|
51
|
+
* <option value="archived">Archived</option>
|
|
52
|
+
* </select>
|
|
53
|
+
* );
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @group Component Overrides
|
|
58
|
+
*/
|
|
59
|
+
export interface FilterFieldBindingProps {
|
|
60
|
+
/** Key of the property being filtered (the column id). */
|
|
61
|
+
propertyKey: string;
|
|
62
|
+
/**
|
|
63
|
+
* The resolved property. For array properties this is the **item**
|
|
64
|
+
* property (`property.of`), with `isArray` set to true.
|
|
65
|
+
*/
|
|
66
|
+
property: Property;
|
|
67
|
+
/** True when the underlying property is an array of `property`. */
|
|
68
|
+
isArray: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Operators this field may offer, already narrowed by engine
|
|
71
|
+
* capabilities, property-type defaults, and `property.ui.filterOperators`.
|
|
72
|
+
*/
|
|
73
|
+
operators: readonly WhereFilterOp[];
|
|
74
|
+
/** Current filter condition for this property, if any. */
|
|
75
|
+
value?: [WhereFilterOp, unknown];
|
|
76
|
+
/** Set (or clear, with `undefined`) the filter condition. */
|
|
77
|
+
setValue: (value?: [WhereFilterOp, unknown]) => void;
|
|
78
|
+
/** Display title for the field (usually the property name). */
|
|
79
|
+
title?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Coordination flags used by fields that open their own dialogs
|
|
82
|
+
* (e.g. the reference picker hides the parent filters dialog).
|
|
83
|
+
*/
|
|
84
|
+
hidden?: boolean;
|
|
85
|
+
setHidden?: (hidden: boolean) => void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A single component override entry.
|
|
89
|
+
*
|
|
90
|
+
* - **Eject mode** (default): Your component fully replaces the built-in one.
|
|
91
|
+
* It receives the same props as the original.
|
|
92
|
+
*
|
|
93
|
+
* - **Wrap mode** (`wrap: true`): Your component wraps the original. The
|
|
94
|
+
* built-in component is passed as `OriginalComponent` in props, so you can
|
|
95
|
+
* render it inside your custom layout/logic.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // Eject — full replacement
|
|
100
|
+
* { Component: MyCustomAppBar }
|
|
101
|
+
*
|
|
102
|
+
* // Wrap — augment the original
|
|
103
|
+
* {
|
|
104
|
+
* Component: ({ OriginalComponent, ...props }) => (
|
|
105
|
+
* <div>
|
|
106
|
+
* <MyBanner />
|
|
107
|
+
* <OriginalComponent {...props} />
|
|
108
|
+
* </div>
|
|
109
|
+
* ),
|
|
110
|
+
* wrap: true
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @group Component Overrides
|
|
115
|
+
*/
|
|
116
|
+
export interface ComponentOverride<P = Record<string, unknown>> {
|
|
117
|
+
/**
|
|
118
|
+
* The replacement component. Receives the same props as the built-in
|
|
119
|
+
* component it replaces.
|
|
120
|
+
*
|
|
121
|
+
* When `wrap` is true, an additional `OriginalComponent` prop is injected
|
|
122
|
+
* containing the default component, allowing you to render it within
|
|
123
|
+
* your custom wrapper.
|
|
124
|
+
*/
|
|
125
|
+
Component: React.ComponentType<P>;
|
|
126
|
+
/**
|
|
127
|
+
* When true, the original default component is injected as the
|
|
128
|
+
* `OriginalComponent` prop into your Component, enabling the
|
|
129
|
+
* wrapping pattern (similar to Docusaurus's `--wrap` swizzle mode).
|
|
130
|
+
*
|
|
131
|
+
* When false or omitted, your component fully replaces the default
|
|
132
|
+
* (similar to Docusaurus's `--eject` swizzle mode).
|
|
133
|
+
*
|
|
134
|
+
* @default false
|
|
135
|
+
*/
|
|
136
|
+
wrap?: boolean;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Collection-scoped overrides. Only collection-level components
|
|
140
|
+
* can be overridden here.
|
|
141
|
+
*
|
|
142
|
+
* Set on a collection's `components` field to customize
|
|
143
|
+
* components for that specific collection.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```tsx
|
|
147
|
+
* const productsCollection = {
|
|
148
|
+
* name: "Products",
|
|
149
|
+
* slug: "products",
|
|
150
|
+
* components: {
|
|
151
|
+
* "Entity.Form": { Component: ProductForm },
|
|
152
|
+
* "Collection.EmptyState": { Component: ProductsEmptyState },
|
|
153
|
+
* "Collection.Card": { Component: ProductCard },
|
|
154
|
+
* }
|
|
155
|
+
* };
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @group Component Overrides
|
|
159
|
+
*/
|
|
160
|
+
export type CollectionComponentOverrideMap = {
|
|
161
|
+
[K in CollectionComponentName]?: ComponentOverride;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* App-level overrides. Includes both app-only components (Shell, HomePage, Auth)
|
|
165
|
+
* and collection-level components (as defaults for all collections).
|
|
166
|
+
*
|
|
167
|
+
* Pass this to the `components` prop on `<Rebase>`.
|
|
168
|
+
*
|
|
169
|
+
* Collection-level components set here act as **defaults** — they apply to all
|
|
170
|
+
* collections unless a specific collection overrides them in its own
|
|
171
|
+
* `components`.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* <Rebase
|
|
176
|
+
* client={client}
|
|
177
|
+
* components={{
|
|
178
|
+
* // App-level: only available here
|
|
179
|
+
* "Shell.AppBar": { Component: MyAppBar },
|
|
180
|
+
* "HomePage": { Component: MyDashboard },
|
|
181
|
+
*
|
|
182
|
+
* // Collection defaults: apply to ALL collections
|
|
183
|
+
* "EditView.FormActions": {
|
|
184
|
+
* Component: MyFormActions,
|
|
185
|
+
* wrap: true
|
|
186
|
+
* },
|
|
187
|
+
* "Collection.EmptyState": { Component: MyEmptyState },
|
|
188
|
+
* }}
|
|
189
|
+
* />
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @group Component Overrides
|
|
193
|
+
*/
|
|
194
|
+
export type ComponentOverrideMap = {
|
|
195
|
+
[K in OverridableComponentName]?: ComponentOverride;
|
|
196
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Entity } from "@rebasepro/types";
|
|
3
|
+
import type { SelectionController } from "../collections.js";
|
|
4
|
+
import type { FormContext } from "./entity_views.js";
|
|
5
|
+
import type { User } from "@rebasepro/types";
|
|
6
|
+
import type { RebaseContext } from "../rebase_context.js";
|
|
7
|
+
import type { SidePanelController } from "../controllers/side_panel_controller.js";
|
|
8
|
+
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
9
|
+
/**
|
|
10
|
+
* A entity action is a custom action that can be performed on a entity.
|
|
11
|
+
* They are displayed in the entity view and in the collection view.
|
|
12
|
+
*/
|
|
13
|
+
export interface EntityAction<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
|
|
14
|
+
/**
|
|
15
|
+
* Title of the action
|
|
16
|
+
*/
|
|
17
|
+
name: string;
|
|
18
|
+
/**
|
|
19
|
+
* Key of the action. You only need to provide this if you want to
|
|
20
|
+
* override the default actions, or if you are not passing the action
|
|
21
|
+
* directly to the `entityActions` prop of a collection.
|
|
22
|
+
* You can define your actions at the app level, in which case you
|
|
23
|
+
* must provide a key.
|
|
24
|
+
* The default actions are:
|
|
25
|
+
* - edit
|
|
26
|
+
* - delete
|
|
27
|
+
* - copy
|
|
28
|
+
*/
|
|
29
|
+
key?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Icon of the action: a Lucide icon name (`"FileBarChart"`) or an element.
|
|
32
|
+
*
|
|
33
|
+
* The name form is what lets a collection declare one from the config package,
|
|
34
|
+
* which is plain `.ts` with no React dependency — an element would drag the
|
|
35
|
+
* whole UI layer into a backend that only loads the collection for its schema.
|
|
36
|
+
* It also matches how every other icon in a collection is written
|
|
37
|
+
* (`admin.icon`, `entityViews[].icon`), which were already strings while this
|
|
38
|
+
* one alone was not.
|
|
39
|
+
*/
|
|
40
|
+
icon?: React.ReactElement | string;
|
|
41
|
+
/**
|
|
42
|
+
* Callback when the action is clicked
|
|
43
|
+
* @param props
|
|
44
|
+
*/
|
|
45
|
+
onClick(props: EntityActionClickProps<M, USER>): Promise<void> | void;
|
|
46
|
+
/**
|
|
47
|
+
* Optional callback in case you want to disable the action
|
|
48
|
+
* @param props
|
|
49
|
+
*/
|
|
50
|
+
isEnabled?(props: EntityActionClickProps<M, USER>): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* When true, this action is rendered inline on each row in the list view.
|
|
53
|
+
* By default, entity actions only appear in the table view and entity form.
|
|
54
|
+
* Use this for actions that should be easily accessible regardless of view mode.
|
|
55
|
+
*/
|
|
56
|
+
showActionsInListView?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Show this action collapsed in the menu of the collection view.
|
|
59
|
+
* Defaults to true
|
|
60
|
+
* If false, the action will be shown in the menu
|
|
61
|
+
*/
|
|
62
|
+
collapsed?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Show this action in the form, defaults to true
|
|
65
|
+
*/
|
|
66
|
+
includeInForm?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type EntityActionClickProps<M extends Record<string, unknown>, USER extends User = User> = {
|
|
69
|
+
entity?: Entity<M>;
|
|
70
|
+
context?: RebaseContext<USER>;
|
|
71
|
+
path?: string;
|
|
72
|
+
collection?: AdminCollection<M>;
|
|
73
|
+
/**
|
|
74
|
+
* Optional form context, present if the action is being called from a form.
|
|
75
|
+
* This allows you to access the form state and methods, including modifying the form values.
|
|
76
|
+
*/
|
|
77
|
+
formContext?: FormContext;
|
|
78
|
+
/**
|
|
79
|
+
* Present if this actions is being called from a side dialog only
|
|
80
|
+
*/
|
|
81
|
+
sidePanelController?: SidePanelController;
|
|
82
|
+
/**
|
|
83
|
+
* Is the action being called from the collection view or from the entity form view?
|
|
84
|
+
*/
|
|
85
|
+
view: "collection" | "form";
|
|
86
|
+
/**
|
|
87
|
+
* If the action is rendered in the form, is it open in a side panel or full screen?
|
|
88
|
+
*/
|
|
89
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
90
|
+
/**
|
|
91
|
+
* Optional selection controller, present if the action is being called from a collection view
|
|
92
|
+
*/
|
|
93
|
+
selectionController?: SelectionController;
|
|
94
|
+
/**
|
|
95
|
+
* Optional highlight function to highlight the entity in the collection view
|
|
96
|
+
* @param entity
|
|
97
|
+
*/
|
|
98
|
+
highlightEntity?: (entity: Entity<Record<string, unknown>>) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Optional unhighlight function to remove the highlight from the entity in the collection view
|
|
101
|
+
* @param entity
|
|
102
|
+
*/
|
|
103
|
+
unhighlightEntity?: (entity: Entity<Record<string, unknown>>) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Optional function to navigate back (e.g. when deleting a entity or navigating from a form)
|
|
106
|
+
*/
|
|
107
|
+
navigateBack?: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Callback to be called when the collection changes, e.g. after a entity is deleted or created.
|
|
110
|
+
*/
|
|
111
|
+
onCollectionChange?: () => void;
|
|
112
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a record shows up: its title, image, subtitle, status, date and tags.
|
|
3
|
+
*
|
|
4
|
+
* Every surface that draws a record draws some subset of these six roles. A list
|
|
5
|
+
* row is image + title + subtitle + status + date; a card is the same with the
|
|
6
|
+
* image on top; a board card drops the image; a reference picker is title +
|
|
7
|
+
* subtitle; a page heading is the title alone. The roles are stable — what fills
|
|
8
|
+
* them is not.
|
|
9
|
+
*
|
|
10
|
+
* Before this block, the roles were derived and only derived. `titleProperty`
|
|
11
|
+
* was the single exception, and it could only ever name a property of the
|
|
12
|
+
* collection: seven separate implementations read that key, disagreed about the
|
|
13
|
+
* fallback, and none of them could await. (It is gone now — `display.title`
|
|
14
|
+
* replaced it outright.) The other five roles could not be
|
|
15
|
+
* stated at all — the image was whichever storage property came first, the
|
|
16
|
+
* status whichever enum, the date whichever timestamp. Right often enough to
|
|
17
|
+
* feel automatic, and wrong with no way to say so.
|
|
18
|
+
*
|
|
19
|
+
* So: one mechanism, six roles, two forms each.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* admin: {
|
|
23
|
+
* display: {
|
|
24
|
+
* title: "name", // a property path
|
|
25
|
+
* image: "photos.0", // a dotted path
|
|
26
|
+
* subtitle: ({ entity }) => // computed
|
|
27
|
+
* `${entity.values.city}, ${entity.values.country}`,
|
|
28
|
+
* status: async ({ entity, context }) => { // and may be async
|
|
29
|
+
* const latest = await context.data.audits.get(`${entity.id}/latest`);
|
|
30
|
+
* return latest?.state;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Anything left out is derived exactly as it is today, so an existing collection
|
|
37
|
+
* renders identically, and a collection that needs one role fixed states that
|
|
38
|
+
* one role.
|
|
39
|
+
*/
|
|
40
|
+
import type { Entity, User } from "@rebasepro/types";
|
|
41
|
+
import type { RebaseContext } from "../rebase_context.js";
|
|
42
|
+
/**
|
|
43
|
+
* What a resolver is handed.
|
|
44
|
+
*
|
|
45
|
+
* The whole {@link RebaseContext}, matching `AdditionalFieldDelegate.value` and
|
|
46
|
+
* `EntityAction.onClick` — so a resolver can reach `context.data` and
|
|
47
|
+
* `context.client` and read anything the panel can read, including a document in
|
|
48
|
+
* a subcollection that the entity itself never loads.
|
|
49
|
+
*/
|
|
50
|
+
export type EntityDisplayResolverParams<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = {
|
|
51
|
+
entity: Entity<M>;
|
|
52
|
+
context: RebaseContext<USER>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Computes what fills one display role for one record.
|
|
56
|
+
*
|
|
57
|
+
* May be async. While a promise is in flight the surface shows the derived value
|
|
58
|
+
* and swaps the resolved one in when it lands — a title is never a spinner.
|
|
59
|
+
* Results are cached per record and per role, and concurrent calls for the same
|
|
60
|
+
* pair share one execution, so a list of fifty rows resolves each row once
|
|
61
|
+
* rather than once per render.
|
|
62
|
+
*
|
|
63
|
+
* Return `undefined` when this record has nothing for this role. Do not return a
|
|
64
|
+
* placeholder: the surface's own fallback is better informed than the resolver
|
|
65
|
+
* is about what belongs there instead — a heading wants the collection's
|
|
66
|
+
* singular name, a link wants the id.
|
|
67
|
+
*
|
|
68
|
+
* A resolver that throws is treated as `undefined` and logged once. A title that
|
|
69
|
+
* cannot be fetched must not take down the row that shows it.
|
|
70
|
+
*/
|
|
71
|
+
export type EntityDisplayResolver<M extends Record<string, unknown> = Record<string, unknown>, T = unknown, USER extends User = User> = {
|
|
72
|
+
/**
|
|
73
|
+
* Declared as a *method* and then indexed back out, which is the only way to
|
|
74
|
+
* write a standalone function type whose parameters stay bivariant.
|
|
75
|
+
*
|
|
76
|
+
* Not a style choice. `AdminCollectionOptions<M>` has to remain assignable
|
|
77
|
+
* to `AdminCollectionOptions<Record<string, unknown>>` — every consumer that
|
|
78
|
+
* takes a collection it did not author depends on it, and losing it breaks
|
|
79
|
+
* `defineCollection`'s own overloads. A resolver takes `Entity<M>` in
|
|
80
|
+
* parameter position, so written as `(params) => …` it makes the entire
|
|
81
|
+
* admin block invariant in `M`, and a typed collection stops being usable as
|
|
82
|
+
* a collection. Method syntax is bivariant under `strictFunctionTypes`; the
|
|
83
|
+
* sibling callbacks (`EntityAction.onClick`, `AdditionalFieldDelegate.value`)
|
|
84
|
+
* are all written this way, and `packages/types/__tests__/bivariance` is the
|
|
85
|
+
* record of finding it out the hard way.
|
|
86
|
+
*/
|
|
87
|
+
resolve(params: EntityDisplayResolverParams<M, USER>): T | undefined | Promise<T | undefined>;
|
|
88
|
+
}["resolve"];
|
|
89
|
+
/**
|
|
90
|
+
* Where one display role gets its value: a property path on this collection, or
|
|
91
|
+
* a resolver that computes it.
|
|
92
|
+
*
|
|
93
|
+
* The path arm is checked against `M` and read with `getValueInPath`, so
|
|
94
|
+
* `"profile.displayName"` is as valid as `"title"`. It also keeps the property's
|
|
95
|
+
* own rendering — an enum status stays a coloured chip, a date stays formatted,
|
|
96
|
+
* a storage path stays a thumbnail — which a resolver returning a bare string
|
|
97
|
+
* cannot. Prefer the path whenever the value is on the record.
|
|
98
|
+
*
|
|
99
|
+
* `Path` is a type parameter rather than `PropertyPath<M>` directly, so this
|
|
100
|
+
* module does not import from `admin_collection`, which imports it.
|
|
101
|
+
*/
|
|
102
|
+
export type EntityDisplaySource<Path extends string, M extends Record<string, unknown> = Record<string, unknown>, T = unknown, USER extends User = User> = Path | EntityDisplayResolver<M, T, USER>;
|
|
103
|
+
/**
|
|
104
|
+
* The six roles, and what may fill each.
|
|
105
|
+
*
|
|
106
|
+
* The value types describe what the renderers accept, not what a resolver must
|
|
107
|
+
* produce exactly: a `date` resolver may return a `Date`, an ISO string or an
|
|
108
|
+
* epoch number, and `tags` takes a single string as shorthand for one tag. A
|
|
109
|
+
* property path is not constrained by them at all — the property's own type
|
|
110
|
+
* decides how it renders.
|
|
111
|
+
*/
|
|
112
|
+
export type EntityDisplay<Path extends string = string, M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = {
|
|
113
|
+
/**
|
|
114
|
+
* What the record is called: the heading, the breadcrumb, the row, and the
|
|
115
|
+
* label of every relation chip and reference that points at it.
|
|
116
|
+
*/
|
|
117
|
+
title?: EntityDisplaySource<Path, M, string, USER>;
|
|
118
|
+
/**
|
|
119
|
+
* The line under the title — a short description, a location, a summary.
|
|
120
|
+
*/
|
|
121
|
+
subtitle?: EntityDisplaySource<Path, M, string, USER>;
|
|
122
|
+
/**
|
|
123
|
+
* The record's picture. A storage path or a URL: the same two things a
|
|
124
|
+
* `storage` property holds, so a resolver may return either.
|
|
125
|
+
*/
|
|
126
|
+
image?: EntityDisplaySource<Path, M, string, USER>;
|
|
127
|
+
/**
|
|
128
|
+
* The state chip — published, archived, paid. Rendered with the enum's own
|
|
129
|
+
* colour when it comes from an enum property.
|
|
130
|
+
*/
|
|
131
|
+
status?: EntityDisplaySource<Path, M, string, USER>;
|
|
132
|
+
/**
|
|
133
|
+
* The timestamp a row is stamped with, usually when it last changed.
|
|
134
|
+
*/
|
|
135
|
+
date?: EntityDisplaySource<Path, M, Date | string | number, USER>;
|
|
136
|
+
/**
|
|
137
|
+
* Free chips beside the status: labels, categories, topics. A single string
|
|
138
|
+
* is accepted as shorthand for one tag.
|
|
139
|
+
*/
|
|
140
|
+
tags?: EntityDisplaySource<Path, M, string[] | string, USER>;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The roles as data, so every consumer iterates the same list instead of
|
|
144
|
+
* repeating it — the mistake that let `titleProperty` grow seven readers.
|
|
145
|
+
*/
|
|
146
|
+
export declare const ENTITY_DISPLAY_ROLES: readonly ["title", "subtitle", "image", "status", "date", "tags"];
|
|
147
|
+
/** One of the six display roles. */
|
|
148
|
+
export type EntityDisplayRole = typeof ENTITY_DISPLAY_ROLES[number];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Entity, EntityValues } from "@rebasepro/types";
|
|
3
|
+
import type { FormexController } from "./formex.js";
|
|
4
|
+
import type { ComponentRef } from "@rebasepro/types";
|
|
5
|
+
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
6
|
+
/**
|
|
7
|
+
* Context passed to custom fields and entity views.
|
|
8
|
+
* @group Form custom fields
|
|
9
|
+
*/
|
|
10
|
+
export interface FormContext<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
11
|
+
/**
|
|
12
|
+
* Current values of the entity
|
|
13
|
+
*/
|
|
14
|
+
values: M;
|
|
15
|
+
/**
|
|
16
|
+
* Update the value of a field
|
|
17
|
+
*/
|
|
18
|
+
setFieldValue: (key: string, value: unknown, shouldValidate?: boolean) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Quietly persist the entity to the database without any UI feedback
|
|
21
|
+
* (no validation, no snackbar, no form reset).
|
|
22
|
+
* Use this for programmatic/background saves from custom views.
|
|
23
|
+
*/
|
|
24
|
+
save: (values: M) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Submit the form — validates, saves, resets the form, and shows
|
|
27
|
+
* a success snackbar. This is what the Save button calls.
|
|
28
|
+
* Use this from custom views when you want the full "user saved" experience.
|
|
29
|
+
*/
|
|
30
|
+
submit: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Collection of the entity being modified
|
|
33
|
+
*/
|
|
34
|
+
collection?: AdminCollection<M>;
|
|
35
|
+
/**
|
|
36
|
+
* Entity id, it can be undefined if it's a new entity
|
|
37
|
+
*/
|
|
38
|
+
entityId?: string | number;
|
|
39
|
+
/**
|
|
40
|
+
* Path this entity is located at
|
|
41
|
+
*/
|
|
42
|
+
path?: string;
|
|
43
|
+
status: "new" | "existing" | "copy";
|
|
44
|
+
entity?: Entity<M>;
|
|
45
|
+
savingError?: Error;
|
|
46
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
47
|
+
/**
|
|
48
|
+
* The underlying formex controller that powers the form.
|
|
49
|
+
*/
|
|
50
|
+
formex: FormexController<M>;
|
|
51
|
+
disabled: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* A save is in flight. Covers the autosave debounce too, which `formex`
|
|
54
|
+
* cannot report because it never goes through `handleSubmit`.
|
|
55
|
+
*
|
|
56
|
+
* The identity bar reads this to label its Save button, which is what
|
|
57
|
+
* replaced the unlabelled floating status circle in the form.
|
|
58
|
+
*/
|
|
59
|
+
isSaving?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the form context is in read-only detail view mode.
|
|
62
|
+
* Custom entity views can use this to adjust their rendering.
|
|
63
|
+
*/
|
|
64
|
+
readOnly?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export type EntityCustomView<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
67
|
+
key: string;
|
|
68
|
+
name: string;
|
|
69
|
+
icon?: string | React.ReactNode;
|
|
70
|
+
tabComponent?: React.ReactNode;
|
|
71
|
+
includeActions?: boolean | "bottom";
|
|
72
|
+
Builder?: ComponentRef<EntityCustomViewParams<M>>;
|
|
73
|
+
/**
|
|
74
|
+
* Which side of the record's own tab this one sits on.
|
|
75
|
+
*
|
|
76
|
+
* `"end"` (the default) puts it after the record and before the
|
|
77
|
+
* subcollections, which is where an extra reading of a row belongs.
|
|
78
|
+
*
|
|
79
|
+
* `"start"` puts it *before* the record — the cover of an entity: a
|
|
80
|
+
* read-only summary an operator opens the row to see, with the form that
|
|
81
|
+
* edits it one tab to the right. Pair it with
|
|
82
|
+
* {@link AdminCollectionOptions.defaultSelectedView}, or the strip will open
|
|
83
|
+
* on a tab that is not the first one.
|
|
84
|
+
*/
|
|
85
|
+
position?: "start" | "end";
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Configuration to replace the default entity form with a custom component.
|
|
89
|
+
* The Builder receives the same props as entity view tabs (entity, formContext, etc.)
|
|
90
|
+
* and has full control over the UI.
|
|
91
|
+
*
|
|
92
|
+
* The form tab still appears in the tab bar but renders your Builder
|
|
93
|
+
* instead of the auto-generated field form.
|
|
94
|
+
*
|
|
95
|
+
* @group Models
|
|
96
|
+
*/
|
|
97
|
+
export type FormViewConfig<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
98
|
+
/**
|
|
99
|
+
* Custom component that replaces the default form.
|
|
100
|
+
*/
|
|
101
|
+
Builder: ComponentRef<EntityCustomViewParams<M>>;
|
|
102
|
+
/**
|
|
103
|
+
* If true, the save/delete action bar is rendered alongside the custom view.
|
|
104
|
+
* Defaults to true.
|
|
105
|
+
*/
|
|
106
|
+
includeActions?: boolean;
|
|
107
|
+
};
|
|
108
|
+
export interface EntityCustomViewParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
109
|
+
collection: AdminCollection<M>;
|
|
110
|
+
entity?: Entity<M>;
|
|
111
|
+
modifiedValues?: EntityValues<M>;
|
|
112
|
+
formContext: FormContext<M>;
|
|
113
|
+
parentCollectionSlugs?: string[];
|
|
114
|
+
parentEntityIds?: string[];
|
|
115
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Entity } from "@rebasepro/types";
|
|
2
|
+
import type { User } from "@rebasepro/types";
|
|
3
|
+
import type { RebaseContext } from "../rebase_context.js";
|
|
4
|
+
/**
|
|
5
|
+
* You can use this configuration to add additional fields to the data
|
|
6
|
+
* exports
|
|
7
|
+
* @group Models
|
|
8
|
+
*/
|
|
9
|
+
export interface ExportConfig<USER extends User = User> {
|
|
10
|
+
additionalFields: ExportMappingFunction<USER>[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* @group Models
|
|
14
|
+
*/
|
|
15
|
+
export interface ExportMappingFunction<USER extends User = User> {
|
|
16
|
+
key: string;
|
|
17
|
+
builder: ({ entity, context }: {
|
|
18
|
+
entity: Entity;
|
|
19
|
+
context: RebaseContext<USER>;
|
|
20
|
+
}) => Promise<string> | string;
|
|
21
|
+
}
|