@rebasepro/app 0.11.1-canary.gfd39654 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collections/filter-operator-resolution.d.ts +27 -2
- package/dist/components/LoginView/LoginView.d.ts +9 -1
- package/dist/components/RebaseAuth.d.ts +3 -3
- package/dist/core/Rebase.d.ts +1 -1
- package/dist/core/RebaseProps.d.ts +8 -11
- package/dist/hooks/useBuildModeController.d.ts +1 -1
- package/dist/hooks/useCustomizationController.d.ts +1 -1
- package/dist/hooks/useRebaseRegistry.d.ts +4 -4
- package/dist/hooks/useStudioBridge.d.ts +10 -10
- package/dist/index.es.js +169 -94
- package/dist/index.es.js.map +1 -1
- package/package.json +7 -7
- package/src/collections/filter-operator-resolution.ts +40 -2
- package/src/collections/property_presentation.ts +9 -4
- package/src/collections/title-property.ts +2 -2
- package/src/components/LoginView/LoginView.tsx +61 -5
- package/src/components/RebaseAuth.tsx +3 -3
- package/src/components/common/useColumnsIds.tsx +1 -1
- package/src/core/Rebase.tsx +2 -3
- package/src/core/RebaseProps.tsx +8 -10
- package/src/hooks/useBuildModeController.tsx +1 -1
- package/src/hooks/useCustomizationController.tsx +1 -1
- package/src/hooks/useRebaseContext.tsx +1 -1
- package/src/hooks/useRebaseRegistry.tsx +9 -9
- package/src/hooks/useStudioBridge.tsx +10 -10
- package/src/index.ts +1 -1
- package/src/locales/de.ts +10 -9
- package/src/locales/en.ts +10 -9
- package/src/locales/es.ts +10 -9
- package/src/locales/fr.ts +10 -9
- package/src/locales/hi.ts +11 -10
- package/src/locales/it.ts +11 -10
- package/src/locales/pt.ts +10 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"magic-string": "^0.30.0",
|
|
53
53
|
"notistack": "^3.0.2",
|
|
54
54
|
"react-i18next": "^17.0.8",
|
|
55
|
-
"@rebasepro/
|
|
56
|
-
"@rebasepro/types": "0.
|
|
57
|
-
"@rebasepro/ui": "0.
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
55
|
+
"@rebasepro/admin-types": "0.12.0",
|
|
56
|
+
"@rebasepro/types": "0.12.0",
|
|
57
|
+
"@rebasepro/ui": "0.12.0",
|
|
58
|
+
"@rebasepro/forms": "0.12.0",
|
|
59
|
+
"@rebasepro/utils": "0.12.0",
|
|
60
|
+
"@rebasepro/common": "0.12.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=19.0.0",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ALL_WHERE_FILTER_OPS,
|
|
3
3
|
DataType,
|
|
4
|
+
DEFAULT_FILTERABLE_RELATION_KINDS,
|
|
4
5
|
getDataSourceCapabilities,
|
|
5
6
|
Property,
|
|
7
|
+
RelationProperty,
|
|
6
8
|
WhereFilterOp
|
|
7
9
|
} from "@rebasepro/types";
|
|
8
10
|
|
|
@@ -30,6 +32,39 @@ const DEFAULT_OPS_BY_TYPE: Partial<Record<DataType, readonly WhereFilterOp[]>> =
|
|
|
30
32
|
/** Operators offered when the property is an *array of* a filterable type. */
|
|
31
33
|
const ARRAY_OPS: readonly WhereFilterOp[] = ["array-contains", "array-contains-any"];
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Whether a relation is one the collection's engine can compile into a
|
|
37
|
+
* `WHERE`.
|
|
38
|
+
*
|
|
39
|
+
* Which kinds those are is the *driver's* answer, not this function's, so it
|
|
40
|
+
* comes from {@link DataSourceCapabilities.filterableRelationKinds}. The admin
|
|
41
|
+
* is one UI over Postgres, MongoDB, Firestore and whatever a developer
|
|
42
|
+
* registers, and "a many-to-many compiles to an `EXISTS` over the junction" is
|
|
43
|
+
* a fact about the Postgres driver — true today, and not the sort of thing to
|
|
44
|
+
* assert on an engine's behalf.
|
|
45
|
+
*
|
|
46
|
+
* Offering an uncompilable filter is not a cosmetic bug. The Postgres driver
|
|
47
|
+
* used to drop a filter key it could not resolve, which *widened* the result
|
|
48
|
+
* set — filtering a many-to-many column returned every row instead of none.
|
|
49
|
+
* That now fails closed with a 400, which is correct for a real schema drift
|
|
50
|
+
* and wrong as the answer to a control the admin itself put on screen.
|
|
51
|
+
*
|
|
52
|
+
* A relation whose kind cannot be determined at all — no inline `relation`
|
|
53
|
+
* block and no stamped `resolvedRelation` — stays filterable. The server
|
|
54
|
+
* resolves relations before the config reaches the admin, so this is the
|
|
55
|
+
* hand-built-property case, and silently dropping a working filter there would
|
|
56
|
+
* be its own regression.
|
|
57
|
+
*/
|
|
58
|
+
export function isFilterableRelation(property: Property, engine?: string): boolean {
|
|
59
|
+
if (property.type !== "relation") return true;
|
|
60
|
+
const relationProperty = property as RelationProperty;
|
|
61
|
+
const kind = relationProperty.relation?.kind ?? relationProperty.resolvedRelation?.kind;
|
|
62
|
+
if (!kind) return true;
|
|
63
|
+
const kinds = getDataSourceCapabilities(engine).filterableRelationKinds
|
|
64
|
+
?? DEFAULT_FILTERABLE_RELATION_KINDS;
|
|
65
|
+
return kinds.includes(kind);
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
export interface ResolveFilterOperatorsParams {
|
|
34
69
|
/**
|
|
35
70
|
* The property to filter on. For array properties, pass the **item**
|
|
@@ -56,8 +91,9 @@ export interface ResolveFilterOperatorsParams {
|
|
|
56
91
|
* 2. what makes sense for the property type (e.g. no `>` on booleans);
|
|
57
92
|
* 3. the developer's optional narrowing — `property.admin.filterOperators`.
|
|
58
93
|
*
|
|
59
|
-
* Returns an empty array when the property is not filterable
|
|
60
|
-
*
|
|
94
|
+
* Returns an empty array when the property is not filterable — by type, by
|
|
95
|
+
* developer narrowing (`filterOperators: []`), or because it is a relation the
|
|
96
|
+
* query layer has no column for (see {@link isFilterableRelation}).
|
|
61
97
|
*
|
|
62
98
|
* @group Models
|
|
63
99
|
*/
|
|
@@ -66,6 +102,8 @@ export function resolveFilterOperators({
|
|
|
66
102
|
isArray,
|
|
67
103
|
engine
|
|
68
104
|
}: ResolveFilterOperatorsParams): WhereFilterOp[] {
|
|
105
|
+
if (!isFilterableRelation(property, engine)) return [];
|
|
106
|
+
|
|
69
107
|
const typeDefaults: readonly WhereFilterOp[] = isArray
|
|
70
108
|
? ARRAY_OPS
|
|
71
109
|
: DEFAULT_OPS_BY_TYPE[property.type] ?? [];
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* declarative conditions feature is authored and stored, never applied. It lives
|
|
15
15
|
* here now because here is where it would be called from once it is wired up.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
17
|
+
import type { ConditionContext, EnumValueConfig, Property, PropertyConditions, ReferenceProperty } from "@rebasepro/types";
|
|
18
|
+
import type { AdminArrayOptions, AdminReferenceOptions } from "@rebasepro/admin-types";
|
|
18
19
|
import { evaluateCondition } from "@rebasepro/common";
|
|
19
20
|
|
|
20
21
|
export function isReadOnly(property: Property): boolean {
|
|
@@ -126,7 +127,9 @@ export function applyPropertyConditions(
|
|
|
126
127
|
(result as ReferenceProperty).path = evaluateCondition(conditions.referencePath, context) as string;
|
|
127
128
|
}
|
|
128
129
|
if (conditions.referenceFilter) {
|
|
129
|
-
|
|
130
|
+
result.admin = result.admin || {};
|
|
131
|
+
(result.admin as AdminReferenceOptions).fixedFilter =
|
|
132
|
+
evaluateCondition(conditions.referenceFilter, context) as AdminReferenceOptions["fixedFilter"];
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -136,10 +139,12 @@ export function applyPropertyConditions(
|
|
|
136
139
|
|
|
137
140
|
if (result.type === "array") {
|
|
138
141
|
if (conditions.canAddElements !== undefined) {
|
|
139
|
-
|
|
142
|
+
result.admin = result.admin || {};
|
|
143
|
+
(result.admin as AdminArrayOptions).canAddElements = evaluateCondition(conditions.canAddElements, context) as boolean;
|
|
140
144
|
}
|
|
141
145
|
if (conditions.sortable !== undefined) {
|
|
142
|
-
|
|
146
|
+
result.admin = result.admin || {};
|
|
147
|
+
(result.admin as AdminArrayOptions).sortable = evaluateCondition(conditions.sortable, context) as boolean;
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isRelationalCollectionConfig, type Property, type Relation } from "@rebasepro/types";
|
|
2
2
|
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
3
3
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
4
4
|
import { getPrimaryKeys, isPropertyBuilder } from "@rebasepro/common";
|
|
@@ -104,7 +104,7 @@ function getForeignKeyColumns<M extends Record<string, unknown>>(collection: Adm
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
for (const relation of collection.relations ?? []) {
|
|
107
|
+
for (const relation of (isRelationalCollectionConfig(collection) ? collection.relations : undefined) ?? []) {
|
|
108
108
|
addRelationKeys(relation);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -21,6 +21,7 @@ declare global {
|
|
|
21
21
|
import {
|
|
22
22
|
ArrowLeftIcon,
|
|
23
23
|
Button,
|
|
24
|
+
Checkbox,
|
|
24
25
|
cls,
|
|
25
26
|
IconButton,
|
|
26
27
|
iconSize,
|
|
@@ -141,6 +142,15 @@ export interface LoginViewProps {
|
|
|
141
142
|
* Pre-fill the password field (e.g. for demo or testing environments).
|
|
142
143
|
*/
|
|
143
144
|
defaultPassword?: string;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* When set, the email/password forms render a "Join the newsletter"
|
|
148
|
+
* opt-in checkbox. Called with the address that just authenticated —
|
|
149
|
+
* sign-in or registration — if the box was ticked. Wire it to whatever
|
|
150
|
+
* stores the subscription; failures are the callback's problem, the
|
|
151
|
+
* login flow never waits on it.
|
|
152
|
+
*/
|
|
153
|
+
onNewsletterOptIn?: (email: string) => void;
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
type AuthMode = "buttons" | "login" | "register" | "forgot";
|
|
@@ -203,7 +213,8 @@ export function LoginView({
|
|
|
203
213
|
additionalComponent,
|
|
204
214
|
topComponent,
|
|
205
215
|
defaultEmail,
|
|
206
|
-
defaultPassword
|
|
216
|
+
defaultPassword,
|
|
217
|
+
onNewsletterOptIn
|
|
207
218
|
}: LoginViewProps) {
|
|
208
219
|
|
|
209
220
|
const modeState = useModeController();
|
|
@@ -213,6 +224,9 @@ export function LoginView({
|
|
|
213
224
|
const [mode, setMode] = useState<AuthMode>("buttons");
|
|
214
225
|
const [fadeIn, setFadeIn] = useState(false);
|
|
215
226
|
const [viewVisible, setViewVisible] = useState(true);
|
|
227
|
+
// Lives here, not in LoginForm: the form unmounts on every login↔register
|
|
228
|
+
// switch, and losing the tick on a mode change would silently drop opt-ins.
|
|
229
|
+
const [newsletterOptIn, setNewsletterOptIn] = useState(false);
|
|
216
230
|
|
|
217
231
|
const switchMode = (newMode: AuthMode) => {
|
|
218
232
|
setViewVisible(false);
|
|
@@ -360,6 +374,9 @@ export function LoginView({
|
|
|
360
374
|
bootstrapMode={true}
|
|
361
375
|
defaultEmail={defaultEmail}
|
|
362
376
|
defaultPassword={defaultPassword}
|
|
377
|
+
onNewsletterOptIn={onNewsletterOptIn}
|
|
378
|
+
newsletterOptIn={newsletterOptIn}
|
|
379
|
+
setNewsletterOptIn={setNewsletterOptIn}
|
|
363
380
|
/>
|
|
364
381
|
)}
|
|
365
382
|
|
|
@@ -442,6 +459,9 @@ export function LoginView({
|
|
|
442
459
|
switchToRegister={showRegistration ? () => switchMode("register") : undefined}
|
|
443
460
|
defaultEmail={defaultEmail}
|
|
444
461
|
defaultPassword={defaultPassword}
|
|
462
|
+
onNewsletterOptIn={onNewsletterOptIn}
|
|
463
|
+
newsletterOptIn={newsletterOptIn}
|
|
464
|
+
setNewsletterOptIn={setNewsletterOptIn}
|
|
445
465
|
/>
|
|
446
466
|
)}
|
|
447
467
|
|
|
@@ -457,6 +477,9 @@ export function LoginView({
|
|
|
457
477
|
switchToLogin={() => switchMode("login")}
|
|
458
478
|
defaultEmail={defaultEmail}
|
|
459
479
|
defaultPassword={defaultPassword}
|
|
480
|
+
onNewsletterOptIn={onNewsletterOptIn}
|
|
481
|
+
newsletterOptIn={newsletterOptIn}
|
|
482
|
+
setNewsletterOptIn={setNewsletterOptIn}
|
|
460
483
|
/>
|
|
461
484
|
)}
|
|
462
485
|
|
|
@@ -646,7 +669,10 @@ function LoginForm({
|
|
|
646
669
|
switchToRegister,
|
|
647
670
|
switchToLogin,
|
|
648
671
|
defaultEmail,
|
|
649
|
-
defaultPassword
|
|
672
|
+
defaultPassword,
|
|
673
|
+
onNewsletterOptIn,
|
|
674
|
+
newsletterOptIn = false,
|
|
675
|
+
setNewsletterOptIn
|
|
650
676
|
}: {
|
|
651
677
|
onClose: () => void,
|
|
652
678
|
onForgotPassword?: () => void,
|
|
@@ -658,9 +684,13 @@ function LoginForm({
|
|
|
658
684
|
switchToRegister?: () => void,
|
|
659
685
|
switchToLogin?: () => void,
|
|
660
686
|
defaultEmail?: string,
|
|
661
|
-
defaultPassword?: string
|
|
687
|
+
defaultPassword?: string,
|
|
688
|
+
onNewsletterOptIn?: (email: string) => void,
|
|
689
|
+
newsletterOptIn?: boolean,
|
|
690
|
+
setNewsletterOptIn?: (checked: boolean) => void
|
|
662
691
|
}) {
|
|
663
692
|
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
693
|
+
const { t } = useTranslation();
|
|
664
694
|
|
|
665
695
|
const [email, setEmail] = useState<string | undefined>(defaultEmail);
|
|
666
696
|
const [password, setPassword] = useState<string | undefined>(defaultPassword);
|
|
@@ -684,15 +714,28 @@ function LoginForm({
|
|
|
684
714
|
// is not one of those: without a catch here every rejected sign-in becomes an
|
|
685
715
|
// unhandled rejection, reported as a crash by anything listening for one,
|
|
686
716
|
// while the user has already been shown the error.
|
|
717
|
+
// Fires only on the resolution path, i.e. after the controller accepted
|
|
718
|
+
// the credentials — a ticked box on a failed attempt must not subscribe
|
|
719
|
+
// an address its owner never proved they control.
|
|
720
|
+
function subscribeIfOptedIn(email: string) {
|
|
721
|
+
if (newsletterOptIn && onNewsletterOptIn) {
|
|
722
|
+
onNewsletterOptIn(email);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
687
726
|
function handleEnterPassword() {
|
|
688
727
|
if (email && password && authController.emailPasswordLogin) {
|
|
689
|
-
void Promise.resolve(authController.emailPasswordLogin(email, password))
|
|
728
|
+
void Promise.resolve(authController.emailPasswordLogin(email, password))
|
|
729
|
+
.then(() => subscribeIfOptedIn(email))
|
|
730
|
+
.catch(() => undefined);
|
|
690
731
|
}
|
|
691
732
|
}
|
|
692
733
|
|
|
693
734
|
function handleRegistration() {
|
|
694
735
|
if (email && password && authController.register) {
|
|
695
|
-
void Promise.resolve(authController.register(email, password, displayName))
|
|
736
|
+
void Promise.resolve(authController.register(email, password, displayName))
|
|
737
|
+
.then(() => subscribeIfOptedIn(email))
|
|
738
|
+
.catch(() => undefined);
|
|
696
739
|
}
|
|
697
740
|
}
|
|
698
741
|
|
|
@@ -813,6 +856,19 @@ function LoginForm({
|
|
|
813
856
|
</div>
|
|
814
857
|
)}
|
|
815
858
|
|
|
859
|
+
{onNewsletterOptIn && (
|
|
860
|
+
<label className="flex items-center gap-2 cursor-pointer mt-1 mb-1">
|
|
861
|
+
<Checkbox
|
|
862
|
+
checked={newsletterOptIn}
|
|
863
|
+
onCheckedChange={(checked) => setNewsletterOptIn?.(checked === true)}
|
|
864
|
+
size="small"
|
|
865
|
+
/>
|
|
866
|
+
<Typography variant="caption" color="secondary" className="select-none">
|
|
867
|
+
{t("join_newsletter")}
|
|
868
|
+
</Typography>
|
|
869
|
+
</label>
|
|
870
|
+
)}
|
|
871
|
+
|
|
816
872
|
<LoadingButton
|
|
817
873
|
type="submit"
|
|
818
874
|
variant="filled"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import React, { useLayoutEffect, useRef } from "react";
|
|
2
2
|
import { useRebaseRegistryDispatch } from "../hooks";
|
|
3
|
-
import type {
|
|
3
|
+
import type { RebaseAuthViewConfig } from "@rebasepro/admin-types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Declarative component to configure authentication in Rebase.
|
|
7
7
|
* Renders nothing — purely registers config into the RebaseRegistry.
|
|
8
8
|
*
|
|
9
9
|
* This is a framework-level component that lives in core since
|
|
10
|
-
* authentication is cross-cutting (
|
|
10
|
+
* authentication is cross-cutting (admin, Studio, any app area).
|
|
11
11
|
* @group Core
|
|
12
12
|
*/
|
|
13
|
-
export function RebaseAuth({ loginView }:
|
|
13
|
+
export function RebaseAuth({ loginView }: RebaseAuthViewConfig) {
|
|
14
14
|
const dispatch = useRebaseRegistryDispatch();
|
|
15
15
|
const registeredRef = useRef(false);
|
|
16
16
|
|
|
@@ -6,7 +6,7 @@ import type { AdminCollection } from "@rebasepro/admin-types";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Get a property in a property tree from a dot-path like `address.street`.
|
|
9
|
-
* Inlined here to avoid importing property-aware utilities from the
|
|
9
|
+
* Inlined here to avoid importing property-aware utilities from the admin layer.
|
|
10
10
|
*/
|
|
11
11
|
function getPropertyInPath(properties: Properties, path: string): Property | undefined {
|
|
12
12
|
if (typeof properties === "object") {
|
package/src/core/Rebase.tsx
CHANGED
|
@@ -36,7 +36,7 @@ import { EffectiveRoleControllerContext } from "../contexts/EffectiveRoleControl
|
|
|
36
36
|
import { useBuildEffectiveRoleController } from "../hooks/useBuildEffectiveRoleController";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* If you are using independent components of the
|
|
39
|
+
* If you are using independent components of the admin
|
|
40
40
|
* you need to wrap them with this main component, so the internal hooks work.
|
|
41
41
|
*
|
|
42
42
|
* This is the main component of Rebase. It acts as the provider of all the
|
|
@@ -159,7 +159,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
159
159
|
const resolvedData = useMemo(() => {
|
|
160
160
|
const registeredDefault = dataSourcesValue.sources[DEFAULT_DATA_SOURCE_KEY];
|
|
161
161
|
if (registeredDefault) return registeredDefault;
|
|
162
|
-
//
|
|
162
|
+
// admin boundary: the SDK client returns flat rows; wrap them into the
|
|
163
163
|
// Entity view-model the admin (`useData()`) renders.
|
|
164
164
|
if (client?.data) return wrapAsEntityData(client.data, entityDataOptions);
|
|
165
165
|
const built = Object.values(dataSourcesValue.sources);
|
|
@@ -204,7 +204,6 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
204
204
|
if (!cancelled) console.debug("[Rebase] Could not load storage sources", e);
|
|
205
205
|
});
|
|
206
206
|
return () => { cancelled = true; };
|
|
207
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
208
207
|
}, [client, authUser, loginSkipped]);
|
|
209
208
|
|
|
210
209
|
// Normalize the prop + discovered definitions into a registry + sources map.
|
package/src/core/RebaseProps.tsx
CHANGED
|
@@ -47,10 +47,8 @@ type DeepPartial<T> = T extends object
|
|
|
47
47
|
* Controller to simulate different roles when dev mode is active.
|
|
48
48
|
* @group Models
|
|
49
49
|
*/
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
setEffectiveRole: (role: string | null) => void;
|
|
53
|
-
}
|
|
50
|
+
export type { EffectiveRoleController } from "@rebasepro/types";
|
|
51
|
+
import type { EffectiveRoleController } from "@rebasepro/types";
|
|
54
52
|
|
|
55
53
|
|
|
56
54
|
/**
|
|
@@ -122,7 +120,7 @@ export type RebaseProps<USER extends User> = {
|
|
|
122
120
|
basePath?: string;
|
|
123
121
|
|
|
124
122
|
/**
|
|
125
|
-
* Optional base path for the
|
|
123
|
+
* Optional base path for the admin collections.
|
|
126
124
|
* Defaults to "/c"
|
|
127
125
|
*/
|
|
128
126
|
baseCollectionPath?: string;
|
|
@@ -136,13 +134,13 @@ export type RebaseProps<USER extends User> = {
|
|
|
136
134
|
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
|
-
* Format of the dates in the
|
|
137
|
+
* Format of the dates in the admin.
|
|
140
138
|
* Defaults to 'MMMM dd, yyyy, HH:mm:ss'
|
|
141
139
|
*/
|
|
142
140
|
dateTimeFormat?: string;
|
|
143
141
|
|
|
144
142
|
/**
|
|
145
|
-
* Locale of the
|
|
143
|
+
* Locale of the admin, currently only affecting dates
|
|
146
144
|
*/
|
|
147
145
|
locale?: Locale;
|
|
148
146
|
|
|
@@ -246,7 +244,7 @@ export type RebaseProps<USER extends User> = {
|
|
|
246
244
|
userConfigPersistence?: UserConfigurationPersistence;
|
|
247
245
|
|
|
248
246
|
/**
|
|
249
|
-
* Callback used to get analytics events from the
|
|
247
|
+
* Callback used to get analytics events from the admin
|
|
250
248
|
*/
|
|
251
249
|
onAnalyticsEvent?: (event: AnalyticsEvent, data?: object) => void;
|
|
252
250
|
|
|
@@ -258,12 +256,12 @@ export type RebaseProps<USER extends User> = {
|
|
|
258
256
|
|
|
259
257
|
|
|
260
258
|
/**
|
|
261
|
-
* Plugins loaded in the
|
|
259
|
+
* Plugins loaded in the admin
|
|
262
260
|
*/
|
|
263
261
|
plugins?: RebasePlugin[];
|
|
264
262
|
|
|
265
263
|
/**
|
|
266
|
-
* Extra slots for the
|
|
264
|
+
* Extra slots for the admin
|
|
267
265
|
*/
|
|
268
266
|
slots?: SlotContribution[];
|
|
269
267
|
|
|
@@ -5,7 +5,7 @@ import { CustomizationControllerContext } from "../contexts/CustomizationControl
|
|
|
5
5
|
/**
|
|
6
6
|
* Use this hook to retrieve the customization controller.
|
|
7
7
|
* This hook includes all the customization options that can be used
|
|
8
|
-
* to customize the
|
|
8
|
+
* to customize the admin.
|
|
9
9
|
*
|
|
10
10
|
* You will likely not need to use this hook directly.
|
|
11
11
|
*
|
|
@@ -33,7 +33,7 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
|
|
|
33
33
|
const authController = useAuthController<USER, AuthControllerType>();
|
|
34
34
|
// `context.data` is the flat SDK view — identical in shape to the backend
|
|
35
35
|
// `context.data` and the frontend SDK client, so callbacks behave the same
|
|
36
|
-
// everywhere. The admin
|
|
36
|
+
// everywhere. The admin reads Entities via `useData()` directly.
|
|
37
37
|
const entityData = useData();
|
|
38
38
|
const data = React.useMemo(() => wrapAsSdkData(entityData), [entityData]);
|
|
39
39
|
const storageSource = useStorageSource();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from "react";
|
|
2
|
-
import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig,
|
|
2
|
+
import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, RebaseAuthViewConfig } from "@rebasepro/admin-types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Split into two contexts to prevent infinite re-render loops:
|
|
@@ -12,18 +12,18 @@ import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, R
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
interface RegistryDispatch {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
registerAdmin: (config: RebaseAdminConfig) => void;
|
|
16
|
+
unregisterAdmin: () => void;
|
|
17
17
|
registerStudio: (config: RebaseStudioConfig) => void;
|
|
18
18
|
unregisterStudio: () => void;
|
|
19
|
-
registerAuth: (config:
|
|
19
|
+
registerAuth: (config: RebaseAuthViewConfig) => void;
|
|
20
20
|
unregisterAuth: () => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface RegistryState {
|
|
24
24
|
cmsConfig: RebaseAdminConfig | null;
|
|
25
25
|
studioConfig: RebaseStudioConfig | null;
|
|
26
|
-
authConfig:
|
|
26
|
+
authConfig: RebaseAuthViewConfig | null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const RegistryDispatchContext = createContext<RegistryDispatch | undefined>(undefined);
|
|
@@ -36,15 +36,15 @@ const RegistryStateContext = createContext<RegistryState>({
|
|
|
36
36
|
export function RebaseRegistryProvider({ children }: { children: React.ReactNode }) {
|
|
37
37
|
const [cmsConfig, setCmsConfig] = useState<RebaseAdminConfig | null>(null);
|
|
38
38
|
const [studioConfig, setStudioConfig] = useState<RebaseStudioConfig | null>(null);
|
|
39
|
-
const [authConfig, setAuthConfig] = useState<
|
|
39
|
+
const [authConfig, setAuthConfig] = useState<RebaseAuthViewConfig | null>(null);
|
|
40
40
|
|
|
41
41
|
// Dispatch functions are stable — never change identity
|
|
42
42
|
const dispatch = useMemo<RegistryDispatch>(() => ({
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
registerAdmin: (config: RebaseAdminConfig) => setCmsConfig(config),
|
|
44
|
+
unregisterAdmin: () => setCmsConfig(null),
|
|
45
45
|
registerStudio: (config: RebaseStudioConfig) => setStudioConfig(config),
|
|
46
46
|
unregisterStudio: () => setStudioConfig(null),
|
|
47
|
-
registerAuth: (config:
|
|
47
|
+
registerAuth: (config: RebaseAuthViewConfig) => setAuthConfig(config),
|
|
48
48
|
unregisterAuth: () => setAuthConfig(null)
|
|
49
49
|
}), []);
|
|
50
50
|
|
|
@@ -18,9 +18,9 @@ export type { BreadcrumbEntry, BreadcrumbsController };
|
|
|
18
18
|
// ─── Bridge interface ───────────────────────────────────────────────
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* StudioBridge provides optional
|
|
22
|
-
* When
|
|
23
|
-
* When
|
|
21
|
+
* StudioBridge provides optional admin capabilities to Studio components.
|
|
22
|
+
* When the admin is present, a bridge provider injects real implementations.
|
|
23
|
+
* When the admin is absent, noop defaults ensure Studio works standalone.
|
|
24
24
|
*/
|
|
25
25
|
export interface StudioBridge {
|
|
26
26
|
collectionRegistry: CollectionRegistryController;
|
|
@@ -84,10 +84,10 @@ const NOOP_BRIDGE: StudioBridge = {
|
|
|
84
84
|
export const StudioBridgeContext = createContext<StudioBridge>(NOOP_BRIDGE);
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Provider that injects
|
|
87
|
+
* Provider that injects admin capabilities into Studio.
|
|
88
88
|
* Accepts partial overrides — any field not provided falls back to noop.
|
|
89
89
|
*
|
|
90
|
-
* Usage (in app wiring, when
|
|
90
|
+
* Usage (in app wiring, when the admin is present):
|
|
91
91
|
* ```tsx
|
|
92
92
|
* <StudioBridgeProvider value={{
|
|
93
93
|
* collectionRegistry: useCollectionRegistryController(),
|
|
@@ -121,27 +121,27 @@ export function StudioBridgeProvider({
|
|
|
121
121
|
|
|
122
122
|
// ─── Convenience hooks ──────────────────────────────────────────────
|
|
123
123
|
|
|
124
|
-
/** Collection registry — returns noop if
|
|
124
|
+
/** Collection registry — returns noop if the admin is not present. */
|
|
125
125
|
export function useStudioCollectionRegistry(): CollectionRegistryController {
|
|
126
126
|
return useContext(StudioBridgeContext).collectionRegistry;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/** Side panel controller — returns noop if
|
|
129
|
+
/** Side panel controller — returns noop if the admin is not present. */
|
|
130
130
|
export function useStudioSidePanelController(): SidePanelController {
|
|
131
131
|
return useContext(StudioBridgeContext).sidePanelController;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** URL controller — returns noop if
|
|
134
|
+
/** URL controller — returns noop if the admin is not present. */
|
|
135
135
|
export function useStudioUrlController(): UrlController {
|
|
136
136
|
return useContext(StudioBridgeContext).urlController;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/** Navigation state — returns noop if
|
|
139
|
+
/** Navigation state — returns noop if the admin is not present. */
|
|
140
140
|
export function useStudioNavigationState(): NavigationStateController {
|
|
141
141
|
return useContext(StudioBridgeContext).navigationState;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/** Breadcrumbs controller — returns noop if
|
|
144
|
+
/** Breadcrumbs controller — returns noop if the admin is not present. */
|
|
145
145
|
export function useStudioBreadcrumbs(): BreadcrumbsController {
|
|
146
146
|
return useContext(StudioBridgeContext).breadcrumbs;
|
|
147
147
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export * from "./i18n/RebaseI18nProvider";
|
|
|
24
24
|
export * from "./locales/en";
|
|
25
25
|
export * from "./locales/es";
|
|
26
26
|
|
|
27
|
-
// Studio Bridge — shared context for optional
|
|
27
|
+
// Studio Bridge — shared context for optional admin↔Studio integration
|
|
28
28
|
export * from "./hooks/useStudioBridge";
|
|
29
29
|
|
|
30
30
|
// Self-assembling bridge registration hook
|
package/src/locales/de.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const de: RebaseTranslations = {
|
|
|
58
58
|
dark_mode: "Dunkel",
|
|
59
59
|
light_mode: "Hell",
|
|
60
60
|
system_mode: "System",
|
|
61
|
+
join_newsletter: "Newsletter abonnieren, kein Spam",
|
|
61
62
|
ok: "Ok",
|
|
62
63
|
save_collection_config: "Sammlungsstruktur speichern",
|
|
63
64
|
search_for_more_icons: "Nach weiteren Symbolen suchen…",
|
|
@@ -277,7 +278,7 @@ export const de: RebaseTranslations = {
|
|
|
277
278
|
action_defined_in_code: "Diese Aktion ist im Code definiert mit dem Schlüssel",
|
|
278
279
|
add_custom_entity_action: "Benutzerdefinierte Entitätsaktion hinzufügen",
|
|
279
280
|
remove_this_action: "Diese Aktion entfernen?",
|
|
280
|
-
remove_action_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Aktion im
|
|
281
|
+
remove_action_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Aktion im Admin-Panel",
|
|
281
282
|
|
|
282
283
|
subcollections_of: "Untersammlungen von",
|
|
283
284
|
add_subcollection: "Untersammlung hinzufügen",
|
|
@@ -286,14 +287,14 @@ export const de: RebaseTranslations = {
|
|
|
286
287
|
view_defined_in_code: "Diese Ansicht ist im Code definiert mit dem Schlüssel",
|
|
287
288
|
add_custom_entity_view: "Benutzerdefinierte Entitätsansicht hinzufügen",
|
|
288
289
|
delete_this_subcollection: "Diese Untersammlung löschen?",
|
|
289
|
-
remove_collection_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Sammlung im
|
|
290
|
+
remove_collection_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Sammlung im Admin-Panel",
|
|
290
291
|
remove_this_view: "Diese Ansicht entfernen?",
|
|
291
|
-
remove_view_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Ansicht im
|
|
292
|
+
remove_view_warning: "Dadurch werden keine Daten gelöscht, sondern nur die Ansicht im Admin-Panel",
|
|
292
293
|
|
|
293
294
|
no_collection_selected: "Keine Sammlung ausgewählt",
|
|
294
295
|
code_for_collection: "Code für",
|
|
295
296
|
use_config_define_json: "Verwenden Sie diese Konfiguration, um die Sammlung im JSON-Format zu definieren.",
|
|
296
|
-
customise_collection_code: "Wenn Sie die Sammlung im Code anpassen möchten, können Sie diesen Sammlungscode zu Ihrer
|
|
297
|
+
customise_collection_code: "Wenn Sie die Sammlung im Code anpassen möchten, können Sie diesen Sammlungscode zu Ihrer Admin-App-Konfiguration hinzufügen.",
|
|
297
298
|
copied: "Kopiert",
|
|
298
299
|
|
|
299
300
|
property_cant_be_edited: "Diese Eigenschaft kann nicht bearbeitet werden",
|
|
@@ -464,7 +465,7 @@ export const de: RebaseTranslations = {
|
|
|
464
465
|
continue_from_scratch: "Von vorne beginnen",
|
|
465
466
|
|
|
466
467
|
/** Admin views config */
|
|
467
|
-
|
|
468
|
+
admin_users: "Admin-Benutzer",
|
|
468
469
|
roles_menu: "Rollen",
|
|
469
470
|
project_settings: "Projekteinstellungen",
|
|
470
471
|
|
|
@@ -726,8 +727,8 @@ no_filter: "No filter",
|
|
|
726
727
|
studio_sql_error_executing: "An error occurred while executing the query.",
|
|
727
728
|
studio_sql_error_explaining: "An error occurred while explaining the query.",
|
|
728
729
|
studio_sql_save_first_to_favorite: "Please save the snippet first before favoriting.",
|
|
729
|
-
|
|
730
|
-
|
|
730
|
+
studio_sql_collections_label: "Collections:",
|
|
731
|
+
studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
|
|
731
732
|
studio_sql_edit_entity: "Edit {{name}} #{{id}}",
|
|
732
733
|
studio_sql_sql_not_supported: "SQL execution not supported by this data source",
|
|
733
734
|
studio_sql_fetch_error: "Failed to fetch connection options: {{message}}",
|
|
@@ -832,7 +833,7 @@ no_filter: "No filter",
|
|
|
832
833
|
studio_add_kanban_column_add: "Add",
|
|
833
834
|
studio_add_kanban_column_cancel: "Cancel",
|
|
834
835
|
studio_collection_view_sql: "SQL Editor",
|
|
835
|
-
|
|
836
|
+
studio_collection_view_admin: "Admin View",
|
|
836
837
|
studio_editor_collection_tooltip: "Edit collection schema",
|
|
837
838
|
studio_editor_collection_no_permission: "You don't have permission to edit this collection",
|
|
838
839
|
studio_editor_collection_start_tooltip: "Copy path or edit schema",
|
|
@@ -865,7 +866,7 @@ no_filter: "No filter",
|
|
|
865
866
|
studio_editor_collection_start_saved: "Default config saved",
|
|
866
867
|
studio_home_duplicate_collection: "Duplicate",
|
|
867
868
|
studio_home_delete: "Delete",
|
|
868
|
-
studio_home_confirm_delete_no_data: "This will not delete any data, only the collection in the
|
|
869
|
+
studio_home_confirm_delete_no_data: "This will not delete any data, only the collection in the admin panel",
|
|
869
870
|
studio_home_collection_deleted: "Collection deleted",
|
|
870
871
|
studio_kanban_configure: "Configure Kanban",
|
|
871
872
|
studio_missing_reference_error: "No collection for path: {{path}}",
|