@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/app",
3
3
  "type": "module",
4
- "version": "0.11.1-canary.gfadf355",
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.11.1-canary.gfadf355",
56
- "@rebasepro/common": "0.11.1-canary.gfadf355",
57
- "@rebasepro/forms": "0.11.1-canary.gfadf355",
58
- "@rebasepro/ui": "0.11.1-canary.gfadf355",
59
- "@rebasepro/types": "0.11.1-canary.gfadf355",
60
- "@rebasepro/utils": "0.11.1-canary.gfadf355"
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 { ArrayProperty, ConditionContext, EnumValueConfig, Property, PropertyConditions, ReferenceProperty } from "@rebasepro/types";
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
- (result as ReferenceProperty).fixedFilter = evaluateCondition(conditions.referenceFilter, context) as ReferenceProperty["fixedFilter"];
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
- (result as ArrayProperty).canAddElements = evaluateCondition(conditions.canAddElements, context) as boolean;
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
- (result as ArrayProperty).sortable = evaluateCondition(conditions.sortable, context) as boolean;
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 { Property, Relation } from "@rebasepro/types";
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)).catch(() => undefined);
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)).catch(() => undefined);
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 { RebaseAuthConfig } from "@rebasepro/admin-types";
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 }: RebaseAuthConfig) {
13
+ export function RebaseAuth({ loginView }: RebaseAuthViewConfig) {
14
14
  const dispatch = useRebaseRegistryDispatch();
15
15
  const registeredRef = useRef(false);
16
16
 
@@ -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 interface EffectiveRoleController {
51
- effectiveRole: string | null;
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, RebaseAuthConfig } from "@rebasepro/admin-types";
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: RebaseAuthConfig) => void;
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: RebaseAuthConfig | null;
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<RebaseAuthConfig | null>(null);
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: RebaseAuthConfig) => setAuthConfig(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…",