@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 +12 -0
- package/package.json +1 -1
- package/src/auth0DataProvider.ts +52 -9
- package/src/components/branding/edit.tsx +50 -19
- package/src/index.tsx +9 -34
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
package/src/auth0DataProvider.ts
CHANGED
|
@@ -315,7 +315,29 @@ export default (
|
|
|
315
315
|
|
|
316
316
|
// Handle singleton resources
|
|
317
317
|
if (resource === "branding") {
|
|
318
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 (
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 (
|
|
26
|
+
// Load domain from cookies on component mount (only when no env domain configured)
|
|
39
27
|
useEffect(() => {
|
|
40
|
-
if (
|
|
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
|
|
62
|
-
|
|
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
|
|
73
|
-
if (
|
|
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
|
|
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)}
|