@salesforce/ui-bundle-template-app-react-sample-b2x 3.0.0 → 3.1.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/CHANGELOG.md +9 -0
- package/dist/README.md +77 -27
- package/dist/force-app/main/default/applications/PropertyManagement.app-meta.xml +33 -0
- package/dist/force-app/main/default/data/Application__c.json +10 -10
- package/dist/force-app/main/default/data/data-plan.json +0 -6
- package/dist/force-app/main/default/objects/Application__c/fields/User__c.field-meta.xml +2 -2
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +1 -1
- package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +76 -0
- package/dist/force-app/main/default/permissionsets/Property_Rental_Guest_User_Access.permissionset-meta.xml +217 -0
- package/dist/force-app/main/default/permissionsets/Tenant_Maintenance_Access.permissionset-meta.xml +1 -1
- package/dist/force-app/main/default/profiles/Property Rental Prospect Profile.profile-meta.xml +295 -0
- package/dist/force-app/main/default/roles/Admin.role-meta.xml +9 -0
- package/dist/force-app/main/default/sharingrules/Property__c.sharingRules-meta.xml +17 -0
- package/dist/force-app/main/default/tabs/Agent__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Application__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/KPI_Snapshot__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Lease__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Maintenance_Request__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Maintenance_Worker__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Notification__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Payment__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Cost__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Feature__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Image__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Listing__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Management_Company__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Owner__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property_Sale__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Property__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/tabs/Tenant__c.tab-meta.xml +5 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/applications/applicationApi.ts +5 -6
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +14751 -2937
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +4 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantProperties.graphql +24 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenants/tenantApi.ts +23 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/alerts/status-alert.tsx +11 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +24 -12
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/api/userProfileApi.ts +2 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/authenticationConfig.ts +9 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +21 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +15 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +2 -11
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +20 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Profile.tsx +80 -43
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +15 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +19 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/utils/helpers.ts +15 -52
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +2 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +7 -15
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +9 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/SearchFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/filters/TextFilter.tsx +5 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useAsyncData.ts +11 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useAsyncData.ts +67 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useGeocode.ts +5 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +3 -11
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyDetail.ts +6 -17
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +43 -34
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useWeather.ts +2 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +45 -44
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +5 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +2 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +43 -15
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +21 -19
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/routes.tsx +19 -25
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/org-setup.config.json +18 -3
- package/dist/scripts/org-setup.mjs +528 -44
- package/package.json +1 -1
- package/dist/force-app/main/default/data/Contact.json +0 -44
- package/dist/force-app/main/default/scripts/org-setup.config.json +0 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/query/tenantAccess.graphql +0 -13
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/tenantApi.ts +0 -12
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/card-skeleton.tsx +0 -38
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/authenticationRouteLayout.tsx +0 -21
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/hooks/useCachedAsyncData.ts +0 -188
|
@@ -43,6 +43,8 @@ export interface CreateMaintenanceRequestInput {
|
|
|
43
43
|
Priority__c?: string;
|
|
44
44
|
Status__c?: string;
|
|
45
45
|
Scheduled__c?: string | null;
|
|
46
|
+
Property__c?: string | null;
|
|
47
|
+
User__c?: string | null;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function getRecordIdFromResponse(result: Record<string, unknown>): string {
|
|
@@ -66,6 +68,8 @@ export async function createMaintenanceRequest(
|
|
|
66
68
|
};
|
|
67
69
|
if (input.Type__c?.trim()) fields.Type__c = input.Type__c.trim();
|
|
68
70
|
if (input.Scheduled__c?.trim()) fields.Scheduled__c = input.Scheduled__c.trim();
|
|
71
|
+
if (input.Property__c?.trim()) fields.Property__c = input.Property__c.trim();
|
|
72
|
+
if (input.User__c?.trim()) fields.User__c = input.User__c.trim();
|
|
69
73
|
const result = (await createRecord(OBJECT_API_NAME, fields)) as unknown as Record<
|
|
70
74
|
string,
|
|
71
75
|
unknown
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
query TenantProperties($userId: ID!) {
|
|
2
|
+
uiapi {
|
|
3
|
+
query {
|
|
4
|
+
Tenant__c(where: { User__c: { eq: $userId } }, first: 50) {
|
|
5
|
+
edges {
|
|
6
|
+
node {
|
|
7
|
+
Id
|
|
8
|
+
Property__c @optional {
|
|
9
|
+
value
|
|
10
|
+
}
|
|
11
|
+
Property__r @optional {
|
|
12
|
+
Name @optional {
|
|
13
|
+
value
|
|
14
|
+
}
|
|
15
|
+
Address__c @optional {
|
|
16
|
+
value
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import TENANT_PROPERTIES_QUERY from "../query/tenantProperties.graphql?raw";
|
|
2
|
+
import type {
|
|
3
|
+
TenantPropertiesQuery,
|
|
4
|
+
TenantPropertiesQueryVariables,
|
|
5
|
+
} from "../graphql-operations-types.js";
|
|
6
|
+
import { executeGraphQL } from "@/api/graphqlClient.js";
|
|
7
|
+
|
|
8
|
+
export type TenantProperty = NonNullable<
|
|
9
|
+
NonNullable<
|
|
10
|
+
NonNullable<NonNullable<TenantPropertiesQuery["uiapi"]["query"]["Tenant__c"]>["edges"]>[number]
|
|
11
|
+
>["node"]
|
|
12
|
+
> & { Property__c: { value: string } };
|
|
13
|
+
|
|
14
|
+
export async function getTenantProperties(userId: string): Promise<TenantProperty[]> {
|
|
15
|
+
if (!userId.trim()) return [];
|
|
16
|
+
const response = await executeGraphQL<TenantPropertiesQuery, TenantPropertiesQueryVariables>(
|
|
17
|
+
TENANT_PROPERTIES_QUERY,
|
|
18
|
+
{ userId },
|
|
19
|
+
);
|
|
20
|
+
return (response.uiapi?.query?.Tenant__c?.edges ?? [])
|
|
21
|
+
.map((edge) => edge?.node)
|
|
22
|
+
.filter((node): node is TenantProperty => Boolean(node?.Property__c?.value));
|
|
23
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import { AlertCircleIcon, CheckCircle2Icon } from 'lucide-react';
|
|
2
|
+
import { AlertCircleIcon, CheckCircle2Icon, InfoIcon } from 'lucide-react';
|
|
3
3
|
import { Alert, AlertDescription } from '../../components/ui/alert';
|
|
4
4
|
import { useId } from 'react';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ const statusAlertVariants = cva('', {
|
|
|
8
8
|
variant: {
|
|
9
9
|
error: '',
|
|
10
10
|
success: '',
|
|
11
|
+
info: 'text-blue-600 *:[svg]:text-current *:data-[slot=alert-description]:text-blue-600/90',
|
|
11
12
|
},
|
|
12
13
|
},
|
|
13
14
|
defaultVariants: {
|
|
@@ -18,11 +19,11 @@ const statusAlertVariants = cva('', {
|
|
|
18
19
|
interface StatusAlertProps extends VariantProps<typeof statusAlertVariants> {
|
|
19
20
|
children?: React.ReactNode;
|
|
20
21
|
/** Alert variant type. @default "error" */
|
|
21
|
-
variant?: 'error' | 'success';
|
|
22
|
+
variant?: 'error' | 'success' | 'info';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Status alert component for displaying error or
|
|
26
|
+
* Status alert component for displaying error, success, or info messages.
|
|
26
27
|
* Returns null if no children are provided.
|
|
27
28
|
*/
|
|
28
29
|
export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
@@ -31,6 +32,12 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
31
32
|
|
|
32
33
|
const isError = variant === 'error';
|
|
33
34
|
|
|
35
|
+
const icon = {
|
|
36
|
+
error: <AlertCircleIcon aria-hidden="true" />,
|
|
37
|
+
success: <CheckCircle2Icon aria-hidden="true" />,
|
|
38
|
+
info: <InfoIcon aria-hidden="true" />,
|
|
39
|
+
}[variant];
|
|
40
|
+
|
|
34
41
|
return (
|
|
35
42
|
<Alert
|
|
36
43
|
variant={isError ? 'destructive' : 'default'}
|
|
@@ -38,11 +45,7 @@ export function StatusAlert({ children, variant = 'error' }: StatusAlertProps) {
|
|
|
38
45
|
aria-describedby={descriptionId}
|
|
39
46
|
role={isError ? 'alert' : 'status'}
|
|
40
47
|
>
|
|
41
|
-
{
|
|
42
|
-
<AlertCircleIcon aria-hidden="true" />
|
|
43
|
-
) : (
|
|
44
|
-
<CheckCircle2Icon aria-hidden="true" />
|
|
45
|
-
)}
|
|
48
|
+
{icon}
|
|
46
49
|
<AlertDescription id={descriptionId}>{children}</AlertDescription>
|
|
47
50
|
</Alert>
|
|
48
51
|
);
|
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
|
8
8
|
type={type}
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
|
-
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
11
|
+
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground/70 placeholder:italic w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
2
2
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
3
|
-
import {
|
|
3
|
+
import { getTenantProperties, type TenantProperty } from "@/api/tenants/tenantApi";
|
|
4
4
|
|
|
5
5
|
interface TenantAccessContextType {
|
|
6
6
|
hasTenantRecord: boolean;
|
|
7
|
+
tenantProperties: TenantProperty[];
|
|
7
8
|
loading: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const TenantAccessContext = createContext<TenantAccessContextType>({
|
|
11
12
|
hasTenantRecord: false,
|
|
13
|
+
tenantProperties: [],
|
|
12
14
|
loading: true,
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
export function TenantAccessProvider({ children }: { children: ReactNode }) {
|
|
16
18
|
const { user } = useAuth();
|
|
17
|
-
const [
|
|
19
|
+
const [tenantProperties, setTenantProperties] = useState<TenantProperty[]>([]);
|
|
18
20
|
const [loading, setLoading] = useState(true);
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
// Reset state synchronously during render when user changes
|
|
23
|
+
const [prevUserId, setPrevUserId] = useState(user?.id);
|
|
24
|
+
if (prevUserId !== user?.id) {
|
|
25
|
+
setPrevUserId(user?.id);
|
|
21
26
|
const id = user?.id?.trim() ?? "";
|
|
22
27
|
if (!id) {
|
|
23
|
-
|
|
24
|
-
setLoading(false);
|
|
25
|
-
|
|
28
|
+
if (tenantProperties.length > 0) setTenantProperties([]);
|
|
29
|
+
if (loading) setLoading(false);
|
|
30
|
+
} else {
|
|
31
|
+
if (!loading) setLoading(true);
|
|
26
32
|
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const id = user?.id?.trim() ?? "";
|
|
37
|
+
if (!id) return;
|
|
27
38
|
|
|
28
39
|
let cancelled = false;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!cancelled) setHasTenantRecord(allowed);
|
|
40
|
+
getTenantProperties(id)
|
|
41
|
+
.then((props) => {
|
|
42
|
+
if (!cancelled) setTenantProperties(props);
|
|
33
43
|
})
|
|
34
44
|
.catch(() => {
|
|
35
|
-
if (!cancelled)
|
|
45
|
+
if (!cancelled) setTenantProperties([]);
|
|
36
46
|
})
|
|
37
47
|
.finally(() => {
|
|
38
48
|
if (!cancelled) setLoading(false);
|
|
@@ -43,8 +53,10 @@ export function TenantAccessProvider({ children }: { children: ReactNode }) {
|
|
|
43
53
|
};
|
|
44
54
|
}, [user?.id]);
|
|
45
55
|
|
|
56
|
+
const hasTenantRecord = tenantProperties.length > 0;
|
|
57
|
+
|
|
46
58
|
return (
|
|
47
|
-
<TenantAccessContext.Provider value={{ hasTenantRecord, loading }}>
|
|
59
|
+
<TenantAccessContext.Provider value={{ hasTenantRecord, tenantProperties, loading }}>
|
|
48
60
|
{children}
|
|
49
61
|
</TenantAccessContext.Provider>
|
|
50
62
|
);
|
|
@@ -48,7 +48,8 @@ function getUserProfileMutation(fields: string): string {
|
|
|
48
48
|
|
|
49
49
|
function throwOnGraphQLErrors(response: any): void {
|
|
50
50
|
if (response?.errors?.length) {
|
|
51
|
-
|
|
51
|
+
console.error("GraphQL request failed", response.errors);
|
|
52
|
+
throw new Error("An unexpected error occurred");
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -49,13 +49,13 @@ export const AUTH_REDIRECT_PARAM = "startUrl";
|
|
|
49
49
|
* Placeholder text constants for authentication form inputs.
|
|
50
50
|
*/
|
|
51
51
|
export const AUTH_PLACEHOLDERS = {
|
|
52
|
-
EMAIL: "
|
|
53
|
-
PASSWORD: "",
|
|
54
|
-
PASSWORD_CREATE: "",
|
|
55
|
-
PASSWORD_CONFIRM: "",
|
|
56
|
-
PASSWORD_NEW: "",
|
|
57
|
-
PASSWORD_NEW_CONFIRM: "",
|
|
58
|
-
FIRST_NAME: "
|
|
59
|
-
LAST_NAME: "
|
|
60
|
-
USERNAME: "
|
|
52
|
+
EMAIL: "e.g. name@example.com",
|
|
53
|
+
PASSWORD: "Enter your password",
|
|
54
|
+
PASSWORD_CREATE: "Create a password",
|
|
55
|
+
PASSWORD_CONFIRM: "Re-enter your password",
|
|
56
|
+
PASSWORD_NEW: "Enter new password",
|
|
57
|
+
PASSWORD_NEW_CONFIRM: "Re-enter new password",
|
|
58
|
+
FIRST_NAME: "e.g. Alex",
|
|
59
|
+
LAST_NAME: "e.g. Smith",
|
|
60
|
+
USERNAME: "e.g. asmith",
|
|
61
61
|
} as const;
|
|
@@ -35,8 +35,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
35
35
|
const userData = await getCurrentUser();
|
|
36
36
|
setUser(userData);
|
|
37
37
|
} catch (err) {
|
|
38
|
-
|
|
39
|
-
setError(
|
|
38
|
+
console.error("Authentication failed", err);
|
|
39
|
+
setError("Authentication failed");
|
|
40
40
|
setUser(null);
|
|
41
41
|
} finally {
|
|
42
42
|
setLoading(false);
|
|
@@ -53,8 +53,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
53
53
|
}, []);
|
|
54
54
|
|
|
55
55
|
useEffect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
getCurrentUser()
|
|
58
|
+
.then((userData) => {
|
|
59
|
+
if (!cancelled) setUser(userData);
|
|
60
|
+
})
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
console.error("Authentication failed", err);
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setError("Authentication failed");
|
|
65
|
+
setUser(null);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
if (!cancelled) setLoading(false);
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
58
75
|
|
|
59
76
|
const value: AuthContextType = {
|
|
60
77
|
user,
|
|
@@ -4,6 +4,7 @@ import { FooterLink } from "../footers/footer-link";
|
|
|
4
4
|
import { SubmitButton } from "./submit-button";
|
|
5
5
|
import { CardLayout } from "../../../components/layouts/card-layout";
|
|
6
6
|
import { useFormContext } from "../hooks/form";
|
|
7
|
+
import { useAuth } from "../context/AuthContext";
|
|
7
8
|
import { useId } from "react";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -15,6 +16,8 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
|
15
16
|
description: string;
|
|
16
17
|
error?: React.ReactNode;
|
|
17
18
|
success?: React.ReactNode;
|
|
19
|
+
/** Whether to show the "already logged in" alert and disable submit when authenticated. @default true */
|
|
20
|
+
showAlreadyLoggedIn?: boolean;
|
|
18
21
|
submit: {
|
|
19
22
|
text: string;
|
|
20
23
|
loadingText?: string;
|
|
@@ -32,6 +35,10 @@ interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
|
32
35
|
* Wraps the specific logic of Login/Register forms with a consistent visual frame (Card),
|
|
33
36
|
* title, and error alert placement. Extends form element props for flexibility.
|
|
34
37
|
* This ensures all auth-related pages look and behave similarly.
|
|
38
|
+
*
|
|
39
|
+
* Auth-aware behavior:
|
|
40
|
+
* - While auth state is loading, the submit button is disabled.
|
|
41
|
+
* - If the user is already authenticated, an info alert is shown and submit is disabled.
|
|
35
42
|
*/
|
|
36
43
|
export function AuthForm({
|
|
37
44
|
id: providedId,
|
|
@@ -39,18 +46,25 @@ export function AuthForm({
|
|
|
39
46
|
description,
|
|
40
47
|
error,
|
|
41
48
|
success,
|
|
49
|
+
showAlreadyLoggedIn = true,
|
|
42
50
|
children,
|
|
43
51
|
submit,
|
|
44
52
|
footer,
|
|
45
53
|
...props
|
|
46
54
|
}: AuthFormProps) {
|
|
47
55
|
const form = useFormContext();
|
|
56
|
+
const { isAuthenticated, loading } = useAuth();
|
|
48
57
|
const generatedId = useId();
|
|
49
58
|
const id = providedId ?? generatedId;
|
|
50
59
|
|
|
60
|
+
const showAuthAlert = showAlreadyLoggedIn && isAuthenticated;
|
|
61
|
+
const isSubmitDisabled = submit.disabled || showAuthAlert || loading;
|
|
62
|
+
|
|
51
63
|
return (
|
|
52
64
|
<CardLayout title={title} description={description}>
|
|
53
65
|
<div className="space-y-6">
|
|
66
|
+
{/* [Dev Note] Auth status alert for authenticated users on public pages */}
|
|
67
|
+
{showAuthAlert && <StatusAlert variant="info">You are already logged in.</StatusAlert>}
|
|
54
68
|
{/* [Dev Note] Global form error alert (e.g. "Invalid Credentials") */}
|
|
55
69
|
{error && <StatusAlert variant="error">{error}</StatusAlert>}
|
|
56
70
|
{success && <StatusAlert variant="success">{success}</StatusAlert>}
|
|
@@ -69,7 +83,7 @@ export function AuthForm({
|
|
|
69
83
|
form={id}
|
|
70
84
|
label={submit.text}
|
|
71
85
|
loadingLabel={submit.loadingText}
|
|
72
|
-
disabled={
|
|
86
|
+
disabled={isSubmitDisabled}
|
|
73
87
|
className="mt-6"
|
|
74
88
|
/>
|
|
75
89
|
</form>
|
|
@@ -40,7 +40,7 @@ function TextField({
|
|
|
40
40
|
const id = providedId ?? generatedId;
|
|
41
41
|
const descriptionId = `${id}-description`;
|
|
42
42
|
const errorId = `${id}-error`;
|
|
43
|
-
const isInvalid = field.state.meta.
|
|
43
|
+
const isInvalid = field.state.meta.isBlurred && field.state.meta.errors.length > 0;
|
|
44
44
|
|
|
45
45
|
// Deduplicate errors by message
|
|
46
46
|
const errors = field.state.meta.errors;
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { Navigate, Outlet, useLocation } from "react-router";
|
|
2
2
|
import { useAuth } from "../context/AuthContext";
|
|
3
3
|
import { AUTH_REDIRECT_PARAM, ROUTES } from "../authenticationConfig";
|
|
4
|
-
import { CardSkeleton } from "../layout/card-skeleton";
|
|
5
|
-
|
|
6
|
-
export interface PrivateRouteProps {
|
|
7
|
-
/**
|
|
8
|
-
* Whether to show a card skeleton placeholder while authentication is loading.
|
|
9
|
-
* @default false
|
|
10
|
-
*/
|
|
11
|
-
showCardSkeleton?: boolean;
|
|
12
|
-
}
|
|
13
4
|
|
|
14
5
|
/**
|
|
15
6
|
* [Dev Note] Route Guard:
|
|
@@ -17,11 +8,11 @@ export interface PrivateRouteProps {
|
|
|
17
8
|
* Otherwise, redirects to Login with a 'startUrl' parameter so the user can be
|
|
18
9
|
* returned to this page after successful login.
|
|
19
10
|
*/
|
|
20
|
-
export default function PrivateRoute(
|
|
11
|
+
export default function PrivateRoute() {
|
|
21
12
|
const { isAuthenticated, loading } = useAuth();
|
|
22
13
|
const location = useLocation();
|
|
23
14
|
|
|
24
|
-
if (loading) return
|
|
15
|
+
if (loading) return null;
|
|
25
16
|
|
|
26
17
|
if (!isAuthenticated) {
|
|
27
18
|
const searchParams = new URLSearchParams();
|
|
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
7
7
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
8
8
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
9
9
|
import { newPasswordSchema } from "../authHelpers";
|
|
10
|
-
import {
|
|
10
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
11
11
|
|
|
12
12
|
const changePasswordSchema = z
|
|
13
13
|
.object({
|
|
@@ -17,7 +17,7 @@ const changePasswordSchema = z
|
|
|
17
17
|
|
|
18
18
|
export default function ChangePassword() {
|
|
19
19
|
const [success, setSuccess] = useState(false);
|
|
20
|
-
const [submitError, setSubmitError] = useState<
|
|
20
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
21
21
|
|
|
22
22
|
const form = useAppForm({
|
|
23
23
|
defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" },
|
|
@@ -40,11 +40,26 @@ export default function ChangePassword() {
|
|
|
40
40
|
Accept: "application/json",
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
|
-
await handleApiResponse(response
|
|
43
|
+
await handleApiResponse(response);
|
|
44
44
|
setSuccess(true);
|
|
45
45
|
form.reset();
|
|
46
46
|
} catch (err) {
|
|
47
|
-
|
|
47
|
+
console.error("Password change failed", err);
|
|
48
|
+
if (err instanceof ApiError) {
|
|
49
|
+
setSubmitError(
|
|
50
|
+
err.errors.length === 1 ? (
|
|
51
|
+
err.errors[0]
|
|
52
|
+
) : (
|
|
53
|
+
<ul>
|
|
54
|
+
{err.errors.map((e, i) => (
|
|
55
|
+
<li key={i}>{e}</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
setSubmitError("Password change failed");
|
|
62
|
+
}
|
|
48
63
|
}
|
|
49
64
|
},
|
|
50
65
|
onSubmitInvalid: () => {},
|
|
@@ -56,6 +71,7 @@ export default function ChangePassword() {
|
|
|
56
71
|
<AuthForm
|
|
57
72
|
title="Change Password"
|
|
58
73
|
description="Enter your current and new password below"
|
|
74
|
+
showAlreadyLoggedIn={false}
|
|
59
75
|
error={submitError}
|
|
60
76
|
success={
|
|
61
77
|
success && (
|
|
@@ -5,7 +5,7 @@ import { AuthForm } from "../forms/auth-form";
|
|
|
5
5
|
import { useAppForm } from "../hooks/form";
|
|
6
6
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
7
7
|
import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
|
|
8
|
-
import {
|
|
8
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
9
9
|
|
|
10
10
|
const forgotPasswordSchema = z.object({
|
|
11
11
|
username: z.string().trim().toLowerCase().email("Please enter a valid username"),
|
|
@@ -13,7 +13,7 @@ const forgotPasswordSchema = z.object({
|
|
|
13
13
|
|
|
14
14
|
export default function ForgotPassword() {
|
|
15
15
|
const [success, setSuccess] = useState(false);
|
|
16
|
-
const [submitError, setSubmitError] = useState<
|
|
16
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
17
17
|
|
|
18
18
|
const form = useAppForm({
|
|
19
19
|
defaultValues: { username: "" },
|
|
@@ -33,10 +33,25 @@ export default function ForgotPassword() {
|
|
|
33
33
|
Accept: "application/json",
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
|
-
await handleApiResponse(response
|
|
36
|
+
await handleApiResponse(response);
|
|
37
37
|
setSuccess(true);
|
|
38
38
|
} catch (err) {
|
|
39
|
-
|
|
39
|
+
console.error("Failed to send reset link", err);
|
|
40
|
+
if (err instanceof ApiError) {
|
|
41
|
+
setSubmitError(
|
|
42
|
+
err.errors.length === 1 ? (
|
|
43
|
+
err.errors[0]
|
|
44
|
+
) : (
|
|
45
|
+
<ul>
|
|
46
|
+
{err.errors.map((e, i) => (
|
|
47
|
+
<li key={i}>{e}</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
setSubmitError("Failed to send reset link");
|
|
54
|
+
}
|
|
40
55
|
}
|
|
41
56
|
},
|
|
42
57
|
onSubmitInvalid: () => {},
|
|
@@ -7,7 +7,7 @@ import { useAppForm } from "../hooks/form";
|
|
|
7
7
|
import { createDataSDK } from "@salesforce/platform-sdk-data";
|
|
8
8
|
import { ROUTES } from "../authenticationConfig";
|
|
9
9
|
import { emailSchema, getStartUrl, type AuthResponse } from "../authHelpers";
|
|
10
|
-
import {
|
|
10
|
+
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
11
11
|
|
|
12
12
|
const loginSchema = z.object({
|
|
13
13
|
email: emailSchema,
|
|
@@ -17,7 +17,7 @@ const loginSchema = z.object({
|
|
|
17
17
|
export default function Login() {
|
|
18
18
|
const navigate = useNavigate();
|
|
19
19
|
const [searchParams] = useSearchParams();
|
|
20
|
-
const [submitError, setSubmitError] = useState<
|
|
20
|
+
const [submitError, setSubmitError] = useState<React.ReactNode>(null);
|
|
21
21
|
|
|
22
22
|
const form = useAppForm({
|
|
23
23
|
defaultValues: { email: "", password: "" },
|
|
@@ -43,7 +43,7 @@ export default function Login() {
|
|
|
43
43
|
Accept: "application/json",
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
|
-
const result = await handleApiResponse<AuthResponse>(response
|
|
46
|
+
const result = await handleApiResponse<AuthResponse>(response);
|
|
47
47
|
if (result?.redirectUrl) {
|
|
48
48
|
// Hard navigate to the URL which establishes the server session cookie
|
|
49
49
|
window.location.replace(result.redirectUrl);
|
|
@@ -52,7 +52,22 @@ export default function Login() {
|
|
|
52
52
|
navigate("/", { replace: true });
|
|
53
53
|
}
|
|
54
54
|
} catch (err) {
|
|
55
|
-
|
|
55
|
+
console.error("Login failed", err);
|
|
56
|
+
if (err instanceof ApiError) {
|
|
57
|
+
setSubmitError(
|
|
58
|
+
err.errors.length === 1 ? (
|
|
59
|
+
err.errors[0]
|
|
60
|
+
) : (
|
|
61
|
+
<ul>
|
|
62
|
+
{err.errors.map((e, i) => (
|
|
63
|
+
<li key={i}>{e}</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
setSubmitError("Login failed");
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
},
|
|
58
73
|
onSubmitInvalid: () => {},
|