@rebasepro/app 0.11.1-canary.gfadf355 → 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/components/LoginView/LoginView.d.ts +9 -1
- package/dist/components/RebaseAuth.d.ts +2 -2
- package/dist/core/RebaseProps.d.ts +2 -5
- package/dist/hooks/useRebaseRegistry.d.ts +2 -2
- package/dist/index.es.js +55 -12
- package/dist/index.es.js.map +1 -1
- package/package.json +7 -7
- 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 +2 -2
- package/src/core/RebaseProps.tsx +2 -4
- package/src/hooks/useRebaseRegistry.tsx +5 -5
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +1 -0
- package/src/locales/it.ts +1 -0
- package/src/locales/pt.ts +1 -0
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/admin-types": "0.
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
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",
|
|
@@ -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,6 +1,6 @@
|
|
|
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.
|
|
@@ -10,7 +10,7 @@ import type { RebaseAuthConfig } from "@rebasepro/admin-types";
|
|
|
10
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
|
|
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
|
/**
|
|
@@ -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:
|
|
@@ -16,14 +16,14 @@ interface RegistryDispatch {
|
|
|
16
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,7 +36,7 @@ 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>(() => ({
|
|
@@ -44,7 +44,7 @@ export function RebaseRegistryProvider({ children }: { children: React.ReactNode
|
|
|
44
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
|
|
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…",
|
package/src/locales/en.ts
CHANGED
|
@@ -68,6 +68,7 @@ export const en: RebaseTranslations = {
|
|
|
68
68
|
dark_mode: "Dark",
|
|
69
69
|
light_mode: "Light",
|
|
70
70
|
system_mode: "System",
|
|
71
|
+
join_newsletter: "Join the newsletter, no spam",
|
|
71
72
|
ok: "Ok",
|
|
72
73
|
save_collection_config: "Save collection structure",
|
|
73
74
|
search_for_more_icons: "Search for more icons…",
|
package/src/locales/es.ts
CHANGED
|
@@ -66,6 +66,7 @@ export const es: RebaseTranslations = {
|
|
|
66
66
|
dark_mode: "Oscuro",
|
|
67
67
|
light_mode: "Claro",
|
|
68
68
|
system_mode: "Sistema",
|
|
69
|
+
join_newsletter: "Únete a la newsletter, sin spam",
|
|
69
70
|
ok: "Aceptar",
|
|
70
71
|
save_collection_config: "Guardar la estructura de la colección",
|
|
71
72
|
search_for_more_icons: "Buscar más iconos…",
|
package/src/locales/fr.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const fr: RebaseTranslations = {
|
|
|
58
58
|
dark_mode: "Sombre",
|
|
59
59
|
light_mode: "Clair",
|
|
60
60
|
system_mode: "Système",
|
|
61
|
+
join_newsletter: "Rejoignez la newsletter, pas de spam",
|
|
61
62
|
ok: "Ok",
|
|
62
63
|
save_collection_config: "Enregistrer la structure de la collection",
|
|
63
64
|
search_for_more_icons: "Rechercher d'autres icônes…",
|
package/src/locales/hi.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const hi: RebaseTranslations = {
|
|
|
58
58
|
dark_mode: "डार्क",
|
|
59
59
|
light_mode: "लाइट",
|
|
60
60
|
system_mode: "सिस्टम",
|
|
61
|
+
join_newsletter: "न्यूज़लेटर से जुड़ें, कोई स्पैम नहीं",
|
|
61
62
|
ok: "ठीक है",
|
|
62
63
|
save_collection_config: "संग्रह संरचना सहेजें",
|
|
63
64
|
search_for_more_icons: "और आइकन खोजें…",
|
package/src/locales/it.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const it: RebaseTranslations = {
|
|
|
58
58
|
dark_mode: "Scuro",
|
|
59
59
|
light_mode: "Chiaro",
|
|
60
60
|
system_mode: "Sistema",
|
|
61
|
+
join_newsletter: "Iscriviti alla newsletter, niente spam",
|
|
61
62
|
ok: "Ok",
|
|
62
63
|
save_collection_config: "Salva struttura della collezione",
|
|
63
64
|
search_for_more_icons: "Cerca altre icone…",
|
package/src/locales/pt.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const pt: RebaseTranslations = {
|
|
|
63
63
|
dark_mode: "Escuro",
|
|
64
64
|
light_mode: "Claro",
|
|
65
65
|
system_mode: "Sistema",
|
|
66
|
+
join_newsletter: "Assine a newsletter, sem spam",
|
|
66
67
|
ok: "Ok",
|
|
67
68
|
save_collection_config: "Guardar estrutura da coleção",
|
|
68
69
|
search_for_more_icons: "Pesquisar mais ícones…",
|