@authhero/react-admin 0.25.0 → 0.27.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @authhero/react-admin
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 76510cd: Fixes for branding page and endpoint
8
+
9
+ ## 0.26.0
10
+
11
+ ### Minor Changes
12
+
13
+ - c89fb59: Skip dialog if there is a env varitable for environment
14
+
3
15
  ## 0.25.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authhero/react-admin",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "packageManager": "pnpm@10.20.0",
5
5
  "private": false,
6
6
  "repository": {
@@ -315,7 +315,29 @@ export default (
315
315
 
316
316
  // Handle singleton resources
317
317
  if (resource === "branding") {
318
- return fetchSingleton(resource, () => managementClient.branding.get());
318
+ const branding = await managementClient.branding.get();
319
+ // Also fetch themes to include in branding data
320
+ const headers = createHeaders(tenantId);
321
+ let themes = null;
322
+ try {
323
+ const themesResponse = await httpClient(
324
+ `${apiUrl}/api/v2/branding/themes/default`,
325
+ { headers },
326
+ );
327
+ themes = themesResponse.json;
328
+ } catch (e) {
329
+ // Themes might not exist yet, that's ok
330
+ }
331
+ return {
332
+ data: [
333
+ {
334
+ ...branding,
335
+ themes,
336
+ id: resource,
337
+ },
338
+ ],
339
+ total: 1,
340
+ };
319
341
  }
320
342
 
321
343
  if (resource === "settings") {
@@ -534,9 +556,22 @@ export default (
534
556
  // Handle singleton resources
535
557
  if (resource === "branding") {
536
558
  const result = await managementClient.branding.get();
559
+ // Also fetch themes to include in branding data
560
+ const headers = createHeaders(tenantId);
561
+ let themes = null;
562
+ try {
563
+ const themesResponse = await httpClient(
564
+ `${apiUrl}/api/v2/branding/themes/default`,
565
+ { headers },
566
+ );
567
+ themes = themesResponse.json;
568
+ } catch (e) {
569
+ // Themes might not exist yet, that's ok
570
+ }
537
571
  return {
538
572
  data: {
539
573
  ...result,
574
+ themes,
540
575
  id: resource,
541
576
  },
542
577
  };
@@ -915,9 +950,12 @@ export default (
915
950
 
916
951
  // Special handling for branding to update theme data separately
917
952
  if (resource === "branding") {
918
- // Update branding
953
+ // Extract themes from the payload - it's updated via a separate endpoint
954
+ const { themes, ...brandingData } = cleanParams.data;
955
+
956
+ // Update branding (without themes)
919
957
  const brandingResult = await managementClient.branding.update(
920
- cleanParams.data,
958
+ brandingData,
921
959
  );
922
960
 
923
961
  // Update themes if provided
@@ -926,12 +964,17 @@ export default (
926
964
  ...brandingResult,
927
965
  };
928
966
 
929
- if (cleanParams.data.themes) {
930
- const themeUpdateResult = await (
931
- managementClient.branding.themes as any
932
- ).default.patch(cleanParams.data.themes);
933
- result.themes =
934
- (themeUpdateResult as any).response || themeUpdateResult;
967
+ if (themes) {
968
+ // Use HTTP directly since the SDK doesn't have this method
969
+ const themeResponse = await httpClient(
970
+ `${apiUrl}/api/v2/branding/themes/default`,
971
+ {
972
+ headers,
973
+ method: "PATCH",
974
+ body: JSON.stringify(themes),
975
+ },
976
+ );
977
+ result.themes = themeResponse.json;
935
978
  }
936
979
 
937
980
  return { data: result };
@@ -1,11 +1,4 @@
1
- import {
2
- DateField,
3
- Edit,
4
- FieldTitle,
5
- Labeled,
6
- TextInput,
7
- TabbedForm,
8
- } from "react-admin";
1
+ import { Edit, TextInput, TabbedForm } from "react-admin";
9
2
  import { ColorInput } from "react-admin-color-picker";
10
3
  import { useInput, useRecordContext } from "react-admin";
11
4
  import { useState, useEffect } from "react";
@@ -13,6 +6,54 @@ import { Box } from "@mui/material";
13
6
  import { ThemesTab } from "./ThemesTab";
14
7
  import { BrandingPreview } from "./BrandingPreview";
15
8
 
9
+ // Helper to recursively remove null values and empty objects from data
10
+ // This is needed because react-admin sends null for empty form fields,
11
+ // but the server schema expects fields to be omitted rather than null
12
+ function cleanObject(obj: Record<string, unknown>): Record<string, unknown> {
13
+ const result: Record<string, unknown> = {};
14
+
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ // Skip null and undefined values
17
+ if (value === null || value === undefined) {
18
+ continue;
19
+ }
20
+
21
+ // Recursively clean nested objects (but not arrays)
22
+ if (typeof value === "object" && !Array.isArray(value)) {
23
+ const cleaned = cleanObject(value as Record<string, unknown>);
24
+ // Only include non-empty objects
25
+ if (Object.keys(cleaned).length > 0) {
26
+ result[key] = cleaned;
27
+ }
28
+ } else {
29
+ result[key] = value;
30
+ }
31
+ }
32
+
33
+ return result;
34
+ }
35
+
36
+ // Transform function to clean up empty objects and null values before sending to server
37
+ // The server schema has strict requirements:
38
+ // - font.url is required if font object is present
39
+ // - page_background must be an object or omitted, not null
40
+ // - Many fields cannot be null, only omitted
41
+ const transformBranding = (data: Record<string, unknown>) => {
42
+ // First, recursively clean all null values and empty objects
43
+ const result = cleanObject(data);
44
+
45
+ // Remove font object if url is not set (font.url is required if font exists)
46
+ if (
47
+ result.font &&
48
+ typeof result.font === "object" &&
49
+ !(result.font as Record<string, unknown>).url
50
+ ) {
51
+ delete result.font;
52
+ }
53
+
54
+ return result;
55
+ };
56
+
16
57
  function PageBackgroundInput(props) {
17
58
  const { field } = useInput(props);
18
59
  const record = useRecordContext();
@@ -128,16 +169,6 @@ function BrandingFormContent() {
128
169
  {/* Form Section */}
129
170
  <Box sx={{ flex: "1 1 60%", minWidth: 0 }}>
130
171
  <TabbedForm>
131
- <TabbedForm.Tab label="Info">
132
- <TextInput source="id" />
133
- <TextInput source="name" />
134
- <Labeled label={<FieldTitle source="created_at" />}>
135
- <DateField source="created_at" showTime={true} />
136
- </Labeled>
137
- <Labeled label={<FieldTitle source="updated_at" />}>
138
- <DateField source="updated_at" showTime={true} />
139
- </Labeled>
140
- </TabbedForm.Tab>
141
172
  <TabbedForm.Tab label="Style">
142
173
  <ColorInput source="colors.primary" label="Primary Color" />
143
174
  <PageBackgroundInput source="colors.page_background" />
@@ -184,7 +215,7 @@ function BrandingFormContent() {
184
215
 
185
216
  export function BrandingEdit() {
186
217
  return (
187
- <Edit>
218
+ <Edit transform={transformBranding}>
188
219
  <BrandingFormContent />
189
220
  </Edit>
190
221
  );
package/src/index.tsx CHANGED
@@ -7,24 +7,12 @@ import { AuthCallback } from "./AuthCallback";
7
7
  import { DomainSelector } from "./components/DomainSelector";
8
8
  import { getSelectedDomainFromStorage } from "./utils/domainUtils";
9
9
 
10
- // Check if running on local.authhero.net - if so, auto-connect to localhost:3000
11
- const isLocalDevelopment = window.location.hostname.startsWith("local.");
12
- const LOCAL_DOMAIN = "localhost:3000";
13
-
14
- // Check if single domain mode is enabled - skips the domain selector entirely
15
- const isSingleDomainMode = import.meta.env.VITE_SINGLE_DOMAIN_MODE === "true";
10
+ // If a domain is configured via env, use single-domain mode automatically
16
11
  const envDomain = import.meta.env.VITE_AUTH0_DOMAIN;
17
12
 
18
13
  function Root() {
19
- // In single domain mode, always use the configured domain from env
20
- const getInitialDomain = () => {
21
- if (isLocalDevelopment) return LOCAL_DOMAIN;
22
- if (isSingleDomainMode && envDomain) return envDomain;
23
- return null;
24
- };
25
-
26
14
  const [selectedDomain, setSelectedDomain] = useState<string | null>(
27
- getInitialDomain(),
15
+ envDomain || null,
28
16
  );
29
17
  const currentPath = location.pathname;
30
18
  const isAuthCallback = currentPath === "/auth-callback";
@@ -35,10 +23,9 @@ function Root() {
35
23
  currentPath.startsWith("/tenants/create") ||
36
24
  currentPath === "/tenants/";
37
25
 
38
- // Load domain from cookies on component mount (skip for local development and single domain mode)
26
+ // Load domain from cookies on component mount (only when no env domain configured)
39
27
  useEffect(() => {
40
- if (isLocalDevelopment || isSingleDomainMode) {
41
- // For local development or single domain mode, always use the configured domain
28
+ if (envDomain) {
42
29
  return;
43
30
  }
44
31
  const savedDomain = getSelectedDomainFromStorage();
@@ -58,9 +45,8 @@ function Root() {
58
45
  );
59
46
  }
60
47
 
61
- // Show domain selector on root path or if no domain is selected
62
- // Skip for local development and single domain mode - redirect to /tenants instead
63
- if (!isLocalDevelopment && !isSingleDomainMode && (isRootPath || !selectedDomain)) {
48
+ // Show domain selector only if no domain is selected
49
+ if (!selectedDomain) {
64
50
  return (
65
51
  <DomainSelector
66
52
  onDomainSelected={(domain) => setSelectedDomain(domain)}
@@ -69,14 +55,8 @@ function Root() {
69
55
  );
70
56
  }
71
57
 
72
- // For single domain mode on root path, redirect to /tenants
73
- if (isSingleDomainMode && isRootPath) {
74
- window.location.href = "/tenants";
75
- return null;
76
- }
77
-
78
- // For local development on root path, redirect to /tenants
79
- if (isLocalDevelopment && isRootPath) {
58
+ // For env-configured domain on root path, redirect to /tenants
59
+ if (envDomain && isRootPath) {
80
60
  window.location.href = "/tenants";
81
61
  return null;
82
62
  }
@@ -106,12 +86,7 @@ function Root() {
106
86
  );
107
87
  }
108
88
 
109
- // Fallback to domain selector (or redirect for local development)
110
- if (isLocalDevelopment) {
111
- window.location.href = "/tenants";
112
- return null;
113
- }
114
-
89
+ // Fallback to domain selector
115
90
  return (
116
91
  <DomainSelector
117
92
  onDomainSelected={(domain) => setSelectedDomain(domain)}