@jmruthers/pace-core 0.6.3 → 0.6.5
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/{DataTable-THFPBKTP.js → DataTable-AOVNCPTX.js} +8 -8
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
- package/dist/{UnifiedAuthProvider-KAGUYQ4J.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
- package/dist/{api-IAGWF3ZG.js → api-O6HTBX5Y.js} +3 -3
- package/dist/{chunk-ZNIWI3UC.js → chunk-6COVEUS7.js} +141 -107
- package/dist/chunk-6COVEUS7.js.map +1 -0
- package/dist/{chunk-QRPVRXYT.js → chunk-AFVQODI2.js} +38 -1
- package/dist/{chunk-QRPVRXYT.js.map → chunk-AFVQODI2.js.map} +1 -1
- package/dist/{chunk-RWEBCB47.js → chunk-EFN2EIMK.js} +2 -2
- package/dist/{chunk-CNCQDFLN.js → chunk-G7QEZTYQ.js} +31 -31
- package/dist/{chunk-CNCQDFLN.js.map → chunk-G7QEZTYQ.js.map} +1 -1
- package/dist/{chunk-YDQHOZNA.js → chunk-HU2C6SSC.js} +29 -18
- package/dist/chunk-HU2C6SSC.js.map +1 -0
- package/dist/{chunk-DWUBLJJM.js → chunk-IHB5DR3H.js} +184 -53
- package/dist/chunk-IHB5DR3H.js.map +1 -0
- package/dist/{chunk-PQBSKX33.js → chunk-IVOFDYWT.js} +364 -208
- package/dist/chunk-IVOFDYWT.js.map +1 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
- package/dist/chunk-JGRYX5UX.js.map +1 -0
- package/dist/{chunk-6Z7LTB3D.js → chunk-NTM7ZSB6.js} +4 -4
- package/dist/chunk-NTM7ZSB6.js.map +1 -0
- package/dist/{chunk-HFZBI76P.js → chunk-RGAWHO7N.js} +4 -4
- package/dist/chunk-RGAWHO7N.js.map +1 -0
- package/dist/{chunk-2T2IG7T7.js → chunk-UPPMRMYG.js} +3 -3
- package/dist/{chunk-2T2IG7T7.js.map → chunk-UPPMRMYG.js.map} +1 -1
- package/dist/components.d.ts +2 -3
- package/dist/components.js +24 -28
- package/dist/components.js.map +1 -1
- package/dist/{contextValidator-3JNZKUTX.js → contextValidator-5OGXSPKS.js} +2 -2
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +41 -139
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +27 -18
- package/dist/index.js +41 -50
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -3
- package/dist/rbac/index.d.ts +16 -9
- package/dist/rbac/index.js +6 -6
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +210 -100
- package/package.json +8 -4
- package/scripts/audit/core/checks/dependencies.cjs +9 -0
- package/scripts/validate-master.js +1 -1
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.tsx +17 -19
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +248 -228
- package/src/components/Dialog/Dialog.tsx +455 -325
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
- package/src/components/FileDisplay/FileDisplay.tsx +5 -5
- package/src/components/Form/Form.test.tsx +3 -2
- package/src/components/Form/Form.tsx +4 -5
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +54 -42
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +36 -34
- package/src/components/index.ts +3 -4
- package/src/hooks/useEventTheme.ts +4 -4
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +4 -4
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/index.ts +11 -1
- package/src/rbac/README.md +20 -20
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
- package/src/rbac/hooks/useResourcePermissions.ts +57 -29
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +36 -0
- package/src/services/AuthService.ts +2 -5
- package/src/services/EventService.ts +99 -2
- package/src/services/InactivityService.ts +139 -58
- package/src/styles/core.css +4 -0
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-6Z7LTB3D.js.map +0 -1
- package/dist/chunk-DWUBLJJM.js.map +0 -1
- package/dist/chunk-HFZBI76P.js.map +0 -1
- package/dist/chunk-PQBSKX33.js.map +0 -1
- package/dist/chunk-YDQHOZNA.js.map +0 -1
- package/dist/chunk-ZNIWI3UC.js.map +0 -1
- /package/dist/{DataTable-THFPBKTP.js.map → DataTable-AOVNCPTX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KAGUYQ4J.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
- /package/dist/{api-IAGWF3ZG.js.map → api-O6HTBX5Y.js.map} +0 -0
- /package/dist/{chunk-RWEBCB47.js.map → chunk-EFN2EIMK.js.map} +0 -0
- /package/dist/{contextValidator-3JNZKUTX.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
|
@@ -46,7 +46,7 @@ import { useOrganisations } from '../../hooks/useOrganisations';
|
|
|
46
46
|
import { useEvents } from '../../hooks/useEvents';
|
|
47
47
|
import { useResolvedScope } from './useResolvedScope';
|
|
48
48
|
import { useCan } from './usePermissions';
|
|
49
|
-
import type { Scope } from '../types';
|
|
49
|
+
import type { Scope, Permission } from '../types';
|
|
50
50
|
|
|
51
51
|
export interface UseResourcePermissionsOptions {
|
|
52
52
|
/** Whether to check read permissions (default: false) */
|
|
@@ -80,15 +80,22 @@ export interface ResourcePermissions {
|
|
|
80
80
|
* and provides a simple API for permission checking.
|
|
81
81
|
*
|
|
82
82
|
* **Page Permission Support:**
|
|
83
|
-
* When an `appId` is available in the resolved scope, the
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
83
|
+
* When an `appId` is available in the resolved scope, the hook automatically:
|
|
84
|
+
* 1. Waits for scope resolution to complete (including `appId` being set)
|
|
85
|
+
* 2. Constructs permission strings with the `page.` prefix (e.g., `create:page.planning`)
|
|
86
|
+
* 3. Passes the resource name as `pageId` to enable page-based permission checks
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* This ensures permission strings match the format returned by `rbac_permissions_get`
|
|
89
|
+
* (e.g., `create:page.planning`) rather than resource-based format (e.g., `create:planning`).
|
|
90
|
+
*
|
|
91
|
+
* **Scope Resolution Timing:**
|
|
92
|
+
* The hook waits for scope resolution to complete before constructing permission strings.
|
|
93
|
+
* This prevents timing issues where permission checks use the wrong format (e.g., `delete:planning`
|
|
94
|
+
* instead of `delete:page.planning`) when `appId` is not yet available in the scope.
|
|
95
|
+
*
|
|
96
|
+
* The RPC function `rbac_check_permission_simplified` will resolve the page name to a page ID
|
|
97
|
+
* and check page permissions if the resource matches a registered page in `rbac_app_pages`.
|
|
98
|
+
* If the resource is not a registered page, it will fall back to resource-based permission checking.
|
|
92
99
|
*
|
|
93
100
|
* @param resource - The resource name (e.g., 'contacts', 'risks', 'planning')
|
|
94
101
|
* Can be a resource name or a page name registered in rbac_app_pages
|
|
@@ -157,26 +164,42 @@ export function useResourcePermissions(
|
|
|
157
164
|
selectedEventId: selectedEvent?.event_id || null
|
|
158
165
|
});
|
|
159
166
|
|
|
160
|
-
//
|
|
167
|
+
// CRITICAL FIX: Only use resolvedScope when it's available (not during loading)
|
|
168
|
+
// This ensures we wait for appId to be resolved before constructing permission strings
|
|
169
|
+
// If resolvedScope is null (still loading), we can't determine if we should use page permissions
|
|
170
|
+
// so we must wait for scope resolution to complete
|
|
161
171
|
const scope: Scope = resolvedScope || {
|
|
162
172
|
organisationId: selectedOrganisation?.id || '',
|
|
163
173
|
eventId: selectedEvent?.event_id || undefined,
|
|
164
174
|
appId: undefined
|
|
165
175
|
};
|
|
166
176
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// This
|
|
171
|
-
const
|
|
177
|
+
// CRITICAL FIX: Only use page permissions when appId is actually available in resolvedScope
|
|
178
|
+
// If scope is still loading (resolvedScope is null), we can't know if appId will be available
|
|
179
|
+
// so we must wait for scope resolution before constructing page permission strings
|
|
180
|
+
// This prevents using wrong permission format (delete:planning instead of delete:page.planning)
|
|
181
|
+
const hasAppId = !!resolvedScope?.appId;
|
|
182
|
+
const pageId = hasAppId ? resource : undefined;
|
|
183
|
+
|
|
184
|
+
// When appId is available in resolved scope, construct permission strings with page. prefix
|
|
185
|
+
// This matches the format that rbac_permissions_get returns (e.g., 'create:page.planning')
|
|
186
|
+
// and ensures consistent permission checking for page-based resources
|
|
187
|
+
// IMPORTANT: Only use page format when appId is actually resolved, not during loading
|
|
188
|
+
const isPagePermission = hasAppId && !!pageId;
|
|
189
|
+
const createPermission = isPagePermission ? `create:page.${resource}` : `create:${resource}`;
|
|
190
|
+
const updatePermission = isPagePermission ? `update:page.${resource}` : `update:${resource}`;
|
|
191
|
+
const deletePermission = isPagePermission ? `delete:page.${resource}` : `delete:${resource}`;
|
|
192
|
+
const readPermission = isPagePermission ? `read:page.${resource}` : `read:${resource}`;
|
|
172
193
|
|
|
173
194
|
// Permission checks for create, update, delete
|
|
174
195
|
// Pass null for super admin status (not checked yet - hook will check if needed)
|
|
175
196
|
// PERFORMANCE: These hooks will each check super admin separately - could be optimized in future
|
|
197
|
+
// CRITICAL: useCan will wait for appId when pageId is provided (it checks needsAppIdForPageName)
|
|
198
|
+
// But we must ensure permission strings are correct before calling useCan
|
|
176
199
|
const { can: canCreateResult, isLoading: createLoading, error: createError } = useCan(
|
|
177
200
|
user?.id || '',
|
|
178
201
|
scope,
|
|
179
|
-
|
|
202
|
+
createPermission as Permission,
|
|
180
203
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
181
204
|
true, // useCache
|
|
182
205
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -186,7 +209,7 @@ export function useResourcePermissions(
|
|
|
186
209
|
const { can: canUpdateResult, isLoading: updateLoading, error: updateError } = useCan(
|
|
187
210
|
user?.id || '',
|
|
188
211
|
scope,
|
|
189
|
-
|
|
212
|
+
updatePermission as Permission,
|
|
190
213
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
191
214
|
true, // useCache
|
|
192
215
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -196,7 +219,7 @@ export function useResourcePermissions(
|
|
|
196
219
|
const { can: canDeleteResult, isLoading: deleteLoading, error: deleteError } = useCan(
|
|
197
220
|
user?.id || '',
|
|
198
221
|
scope,
|
|
199
|
-
|
|
222
|
+
deletePermission as Permission,
|
|
200
223
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
201
224
|
true, // useCache
|
|
202
225
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -207,7 +230,7 @@ export function useResourcePermissions(
|
|
|
207
230
|
const { can: canReadResult, isLoading: readLoading, error: readError } = useCan(
|
|
208
231
|
user?.id || '',
|
|
209
232
|
scope,
|
|
210
|
-
|
|
233
|
+
readPermission as Permission,
|
|
211
234
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
212
235
|
true, // useCache
|
|
213
236
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -215,9 +238,14 @@ export function useResourcePermissions(
|
|
|
215
238
|
);
|
|
216
239
|
|
|
217
240
|
// Aggregate loading states - any permission check or scope resolution loading
|
|
241
|
+
// CRITICAL: When requireScope is true, we must wait for scope resolution to complete
|
|
242
|
+
// so we can determine the correct permission format (page vs resource permissions)
|
|
243
|
+
// This prevents using wrong permission format (delete:planning instead of delete:page.planning)
|
|
218
244
|
const isLoading = useMemo(() => {
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
// If scope resolution is required, wait for it to complete
|
|
246
|
+
const waitingForScope = requireScope && scopeLoading;
|
|
247
|
+
return waitingForScope || createLoading || updateLoading || deleteLoading || (enableRead && readLoading);
|
|
248
|
+
}, [scopeLoading, requireScope, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
|
|
221
249
|
|
|
222
250
|
// Aggregate errors - prefer scope error, then any permission error
|
|
223
251
|
const error = useMemo(() => {
|
|
@@ -239,19 +267,19 @@ export function useResourcePermissions(
|
|
|
239
267
|
if (res !== resource) {
|
|
240
268
|
return false;
|
|
241
269
|
}
|
|
242
|
-
return canCreateResult;
|
|
270
|
+
return canCreateResult; // canCreateResult is already the boolean 'can' value from useCan
|
|
243
271
|
},
|
|
244
272
|
canUpdate: (res: string) => {
|
|
245
273
|
if (res !== resource) {
|
|
246
274
|
return false;
|
|
247
275
|
}
|
|
248
|
-
return canUpdateResult;
|
|
276
|
+
return canUpdateResult; // canUpdateResult is already the boolean 'can' value from useCan
|
|
249
277
|
},
|
|
250
278
|
canDelete: (res: string) => {
|
|
251
279
|
if (res !== resource) {
|
|
252
280
|
return false;
|
|
253
281
|
}
|
|
254
|
-
return canDeleteResult;
|
|
282
|
+
return canDeleteResult; // canDeleteResult is already the boolean 'can' value from useCan
|
|
255
283
|
},
|
|
256
284
|
canRead: (res: string) => {
|
|
257
285
|
if (!enableRead) {
|
|
@@ -260,17 +288,17 @@ export function useResourcePermissions(
|
|
|
260
288
|
if (res !== resource) {
|
|
261
289
|
return false;
|
|
262
290
|
}
|
|
263
|
-
return canReadResult;
|
|
291
|
+
return canReadResult; // canReadResult is already the boolean 'can' value from useCan
|
|
264
292
|
},
|
|
265
293
|
scope,
|
|
266
294
|
isLoading,
|
|
267
295
|
error
|
|
268
296
|
}), [
|
|
269
297
|
resource,
|
|
270
|
-
canCreateResult,
|
|
271
|
-
canUpdateResult,
|
|
272
|
-
canDeleteResult,
|
|
273
|
-
canReadResult,
|
|
298
|
+
canCreateResult, // This is already the boolean 'can' value
|
|
299
|
+
canUpdateResult, // This is already the boolean 'can' value
|
|
300
|
+
canDeleteResult, // This is already the boolean 'can' value
|
|
301
|
+
canReadResult, // This is already the boolean 'can' value
|
|
274
302
|
enableRead,
|
|
275
303
|
scope,
|
|
276
304
|
isLoading,
|
package/src/rbac/permissions.ts
CHANGED
|
@@ -106,35 +106,35 @@ export const EVENT_APP_PERMISSIONS = {
|
|
|
106
106
|
// ============================================================================
|
|
107
107
|
|
|
108
108
|
export const PAGE_PERMISSIONS = {
|
|
109
|
-
// General page access
|
|
109
|
+
// General page access (generic - used for wildcard checks)
|
|
110
110
|
READ_PAGE: 'read:page' as Permission,
|
|
111
111
|
CREATE_PAGE: 'create:page' as Permission,
|
|
112
112
|
UPDATE_PAGE: 'update:page' as Permission,
|
|
113
113
|
DELETE_PAGE: 'delete:page' as Permission,
|
|
114
114
|
|
|
115
115
|
// Admin pages
|
|
116
|
-
READ_ADMIN: 'read:admin' as Permission,
|
|
117
|
-
CREATE_ADMIN: 'create:admin' as Permission,
|
|
118
|
-
UPDATE_ADMIN: 'update:admin' as Permission,
|
|
119
|
-
DELETE_ADMIN: 'delete:admin' as Permission,
|
|
116
|
+
READ_ADMIN: 'read:page.admin' as Permission,
|
|
117
|
+
CREATE_ADMIN: 'create:page.admin' as Permission,
|
|
118
|
+
UPDATE_ADMIN: 'update:page.admin' as Permission,
|
|
119
|
+
DELETE_ADMIN: 'delete:page.admin' as Permission,
|
|
120
120
|
|
|
121
121
|
// Dashboard pages
|
|
122
|
-
READ_DASHBOARD: 'read:dashboard' as Permission,
|
|
123
|
-
CREATE_DASHBOARD: 'create:dashboard' as Permission,
|
|
124
|
-
UPDATE_DASHBOARD: 'update:dashboard' as Permission,
|
|
125
|
-
DELETE_DASHBOARD: 'delete:dashboard' as Permission,
|
|
122
|
+
READ_DASHBOARD: 'read:page.dashboard' as Permission,
|
|
123
|
+
CREATE_DASHBOARD: 'create:page.dashboard' as Permission,
|
|
124
|
+
UPDATE_DASHBOARD: 'update:page.dashboard' as Permission,
|
|
125
|
+
DELETE_DASHBOARD: 'delete:page.dashboard' as Permission,
|
|
126
126
|
|
|
127
127
|
// Settings pages
|
|
128
|
-
READ_SETTINGS: 'read:settings' as Permission,
|
|
129
|
-
CREATE_SETTINGS: 'create:settings' as Permission,
|
|
130
|
-
UPDATE_SETTINGS: 'update:settings' as Permission,
|
|
131
|
-
DELETE_SETTINGS: 'delete:settings' as Permission,
|
|
128
|
+
READ_SETTINGS: 'read:page.settings' as Permission,
|
|
129
|
+
CREATE_SETTINGS: 'create:page.settings' as Permission,
|
|
130
|
+
UPDATE_SETTINGS: 'update:page.settings' as Permission,
|
|
131
|
+
DELETE_SETTINGS: 'delete:page.settings' as Permission,
|
|
132
132
|
|
|
133
133
|
// Reports pages
|
|
134
|
-
READ_REPORTS: 'read:reports' as Permission,
|
|
135
|
-
CREATE_REPORTS: 'create:reports' as Permission,
|
|
136
|
-
UPDATE_REPORTS: 'update:reports' as Permission,
|
|
137
|
-
DELETE_REPORTS: 'delete:reports' as Permission,
|
|
134
|
+
READ_REPORTS: 'read:page.reports' as Permission,
|
|
135
|
+
CREATE_REPORTS: 'create:page.reports' as Permission,
|
|
136
|
+
UPDATE_REPORTS: 'update:page.reports' as Permission,
|
|
137
|
+
DELETE_REPORTS: 'delete:page.reports' as Permission,
|
|
138
138
|
} as const;
|
|
139
139
|
|
|
140
140
|
// ============================================================================
|
|
@@ -89,7 +89,19 @@ export class ContextValidator {
|
|
|
89
89
|
if (effectiveScopeType === 'both') {
|
|
90
90
|
// For 'both' pages, we need at least one context (org or event)
|
|
91
91
|
// Both will be checked during permission evaluation
|
|
92
|
+
// For PORTAL/ADMIN apps, both contexts are optional
|
|
92
93
|
if (!scope.organisationId && !scope.eventId) {
|
|
94
|
+
if (allowsOptionalContexts(appName)) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: true,
|
|
97
|
+
resolvedScope: {
|
|
98
|
+
organisationId: undefined,
|
|
99
|
+
eventId: undefined,
|
|
100
|
+
appId: scope.appId
|
|
101
|
+
},
|
|
102
|
+
error: null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
93
105
|
return {
|
|
94
106
|
isValid: false,
|
|
95
107
|
resolvedScope: null,
|
|
@@ -123,6 +135,18 @@ export class ContextValidator {
|
|
|
123
135
|
// Handle 'event' scope - requires event context
|
|
124
136
|
if (effectiveScopeType === 'event') {
|
|
125
137
|
if (!scope.eventId) {
|
|
138
|
+
// For PORTAL/ADMIN apps, event context is optional
|
|
139
|
+
if (allowsOptionalContexts(appName)) {
|
|
140
|
+
return {
|
|
141
|
+
isValid: true,
|
|
142
|
+
resolvedScope: {
|
|
143
|
+
organisationId: scope.organisationId,
|
|
144
|
+
eventId: undefined,
|
|
145
|
+
appId: scope.appId
|
|
146
|
+
},
|
|
147
|
+
error: null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
126
150
|
return {
|
|
127
151
|
isValid: false,
|
|
128
152
|
resolvedScope: null,
|
|
@@ -167,6 +191,18 @@ export class ContextValidator {
|
|
|
167
191
|
// Handle 'organisation' scope - requires organisation context
|
|
168
192
|
if (effectiveScopeType === 'organisation') {
|
|
169
193
|
if (!scope.organisationId) {
|
|
194
|
+
// For PORTAL/ADMIN apps, organisation context is optional
|
|
195
|
+
if (allowsOptionalContexts(appName)) {
|
|
196
|
+
return {
|
|
197
|
+
isValid: true,
|
|
198
|
+
resolvedScope: {
|
|
199
|
+
organisationId: undefined,
|
|
200
|
+
eventId: scope.eventId,
|
|
201
|
+
appId: scope.appId
|
|
202
|
+
},
|
|
203
|
+
error: null
|
|
204
|
+
};
|
|
205
|
+
}
|
|
170
206
|
return {
|
|
171
207
|
isValid: false,
|
|
172
208
|
resolvedScope: null,
|
|
@@ -57,11 +57,8 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
57
57
|
|
|
58
58
|
// Auth state getters
|
|
59
59
|
getUser(): User | null {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else {
|
|
63
|
-
logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning null`);
|
|
64
|
-
}
|
|
60
|
+
// Removed debug logging - getUser() is called frequently and state changes
|
|
61
|
+
// are already logged in the auth state change handler
|
|
65
62
|
return this.user;
|
|
66
63
|
}
|
|
67
64
|
|
|
@@ -108,6 +108,9 @@ export class EventService extends BaseService implements IEventService {
|
|
|
108
108
|
this.resetInitialization();
|
|
109
109
|
this.isInitializedRef = false;
|
|
110
110
|
this.isFetchingRef = false;
|
|
111
|
+
// Reset user cleared flag when new user logs in - allows auto-selection for new user
|
|
112
|
+
this.userClearedEventRef = false;
|
|
113
|
+
this.hasAutoSelectedRef = false;
|
|
111
114
|
|
|
112
115
|
logger.debug('EventService', `User changed [ID:${this.instanceId}]`, {
|
|
113
116
|
previousUserId,
|
|
@@ -177,15 +180,40 @@ export class EventService extends BaseService implements IEventService {
|
|
|
177
180
|
// Do not clear events for super admins when organisation context is removed
|
|
178
181
|
const shouldClearEvents = !this.isSuperAdmin;
|
|
179
182
|
|
|
183
|
+
// Determine if this is the first time an org is being set (from null/undefined to a value)
|
|
184
|
+
const isFirstOrgSet = (previousOrgId === null || previousOrgId === undefined) && newOrgId !== null && newOrgId !== undefined;
|
|
185
|
+
|
|
180
186
|
// Clear events ONLY when switching between different organisations (not when org first becomes available)
|
|
181
|
-
|
|
187
|
+
// IMPORTANT: Check isFirstOrgSet FIRST to prevent clearing when org is first set
|
|
188
|
+
if (isFirstOrgSet) {
|
|
189
|
+
// Organisation first becomes available - DO NOT clear the event, preserve it
|
|
190
|
+
// The event will be validated when events are re-fetched with org context
|
|
191
|
+
// If it's no longer valid, it will be cleared without setting userClearedEventRef = true
|
|
192
|
+
const hadAutoSelectedEvent = this.hasAutoSelectedRef && !!this.selectedEvent;
|
|
193
|
+
this.userClearedEventRef = false;
|
|
194
|
+
// Don't reset hasAutoSelectedRef if we had an auto-selected event - preserve it
|
|
195
|
+
// This ensures the event remains selected when org is first set
|
|
196
|
+
if (!hadAutoSelectedEvent) {
|
|
197
|
+
this.hasAutoSelectedRef = false;
|
|
198
|
+
}
|
|
199
|
+
logger.debug('EventService', 'Organisation first set - preserving event and resetting auto-selection flags', {
|
|
200
|
+
organisationId: newOrgId,
|
|
201
|
+
hasSelectedEvent: !!this.selectedEvent,
|
|
202
|
+
selectedEventId: this.selectedEvent?.event_id,
|
|
203
|
+
hadAutoSelectedEvent,
|
|
204
|
+
preservingEvent: hadAutoSelectedEvent,
|
|
205
|
+
previousOrgId,
|
|
206
|
+
newOrgId
|
|
207
|
+
});
|
|
208
|
+
} else if (previousOrgId !== null && previousOrgId !== undefined && newOrgId !== null && newOrgId !== undefined && previousOrgId !== newOrgId) {
|
|
209
|
+
// Switching between different organisations - clear events
|
|
182
210
|
if (shouldClearEvents) {
|
|
183
211
|
this.events = [];
|
|
184
212
|
// Use setSelectedEvent(null) to preserve userClearedEventRef flag if user explicitly cleared
|
|
185
213
|
// This prevents auto-selection from re-selecting the event after org switch
|
|
186
214
|
this.setSelectedEvent(null);
|
|
187
215
|
}
|
|
188
|
-
} else if (previousOrgId !== null && newOrgId === null) {
|
|
216
|
+
} else if (previousOrgId !== null && previousOrgId !== undefined && newOrgId === null) {
|
|
189
217
|
// Organisation was removed - clear events if not super admin
|
|
190
218
|
if (shouldClearEvents) {
|
|
191
219
|
this.events = [];
|
|
@@ -232,7 +260,13 @@ export class EventService extends BaseService implements IEventService {
|
|
|
232
260
|
});
|
|
233
261
|
// Reset the user cleared flag when selecting an event
|
|
234
262
|
this.userClearedEventRef = false;
|
|
263
|
+
logger.debug('EventService', 'Event selected', {
|
|
264
|
+
eventId: event.event_id,
|
|
265
|
+
eventName: event.event_name,
|
|
266
|
+
userClearedEventRef: this.userClearedEventRef
|
|
267
|
+
});
|
|
235
268
|
} else {
|
|
269
|
+
const previousEventId = this.selectedEvent?.event_id;
|
|
236
270
|
this.selectedEvent = null;
|
|
237
271
|
this.setSelectedEventId?.(null);
|
|
238
272
|
// Clear from secure storage (don't await to avoid blocking)
|
|
@@ -243,6 +277,11 @@ export class EventService extends BaseService implements IEventService {
|
|
|
243
277
|
this.hasAutoSelectedRef = false;
|
|
244
278
|
// Mark that user explicitly cleared the event to prevent auto-selection
|
|
245
279
|
this.userClearedEventRef = true;
|
|
280
|
+
logger.debug('EventService', 'Event cleared via setSelectedEvent(null)', {
|
|
281
|
+
previousEventId,
|
|
282
|
+
userClearedEventRef: this.userClearedEventRef,
|
|
283
|
+
stackTrace: new Error().stack
|
|
284
|
+
});
|
|
246
285
|
}
|
|
247
286
|
this.notify();
|
|
248
287
|
}
|
|
@@ -617,6 +656,28 @@ export class EventService extends BaseService implements IEventService {
|
|
|
617
656
|
this.events = sortedEvents;
|
|
618
657
|
this.error = null;
|
|
619
658
|
|
|
659
|
+
// Validate selected event - if it's no longer in the events list, clear it
|
|
660
|
+
// This can happen when org context changes or events are refreshed
|
|
661
|
+
// Don't set userClearedEventRef to true in this case - it's an automatic clear, not user-initiated
|
|
662
|
+
if (this.selectedEvent) {
|
|
663
|
+
const selectedEventId = this.selectedEvent.event_id;
|
|
664
|
+
const eventStillExists = transformedEvents.some(
|
|
665
|
+
e => e.event_id === selectedEventId
|
|
666
|
+
);
|
|
667
|
+
if (!eventStillExists) {
|
|
668
|
+
// Event no longer available - clear it but don't mark as user-cleared
|
|
669
|
+
const previousUserClearedRef = this.userClearedEventRef;
|
|
670
|
+
this.selectedEvent = null;
|
|
671
|
+
this.setSelectedEventId?.(null);
|
|
672
|
+
// Restore the previous userClearedEventRef value - this was an automatic clear, not user-initiated
|
|
673
|
+
this.userClearedEventRef = previousUserClearedRef;
|
|
674
|
+
logger.debug('EventService', 'Cleared selected event - no longer in events list', {
|
|
675
|
+
previousEventId: selectedEventId,
|
|
676
|
+
eventsCount: transformedEvents.length
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
620
681
|
// Reset auto-selection ref for new events
|
|
621
682
|
this.hasAutoSelectedRef = false;
|
|
622
683
|
|
|
@@ -624,26 +685,62 @@ export class EventService extends BaseService implements IEventService {
|
|
|
624
685
|
if (!skipLoadPersisted) {
|
|
625
686
|
const persistedEventLoaded = await this.loadPersistedEvent(transformedEvents);
|
|
626
687
|
|
|
688
|
+
logger.debug('EventService', 'Event selection check', {
|
|
689
|
+
persistedEventLoaded,
|
|
690
|
+
userClearedEventRef: this.userClearedEventRef,
|
|
691
|
+
eventsCount: transformedEvents.length,
|
|
692
|
+
hasSelectedEvent: !!this.selectedEvent
|
|
693
|
+
});
|
|
694
|
+
|
|
627
695
|
// If no persisted event was loaded and user hasn't explicitly cleared an event, auto-select the next event
|
|
628
696
|
if (!persistedEventLoaded && !this.userClearedEventRef) {
|
|
629
697
|
const nextEvent = this.getNextEventByDate(transformedEvents);
|
|
698
|
+
logger.debug('EventService', 'Auto-selection attempt', {
|
|
699
|
+
nextEventFound: !!nextEvent,
|
|
700
|
+
nextEventId: nextEvent?.event_id,
|
|
701
|
+
nextEventDate: nextEvent?.event_date
|
|
702
|
+
});
|
|
630
703
|
if (nextEvent) {
|
|
631
704
|
this.hasAutoSelectedRef = true;
|
|
632
705
|
// Use setSelectedEvent() to ensure consistent behavior
|
|
633
706
|
// Theme will be applied by useEventTheme() hook
|
|
634
707
|
this.setSelectedEvent(nextEvent);
|
|
708
|
+
logger.debug('EventService', 'Auto-selected next event', {
|
|
709
|
+
eventId: nextEvent.event_id,
|
|
710
|
+
eventName: nextEvent.event_name,
|
|
711
|
+
eventDate: nextEvent.event_date
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
logger.debug('EventService', 'No next event found for auto-selection', {
|
|
715
|
+
eventsCount: transformedEvents.length,
|
|
716
|
+
eventsWithDates: transformedEvents.filter(e => e.event_date).length
|
|
717
|
+
});
|
|
635
718
|
}
|
|
719
|
+
} else if (persistedEventLoaded) {
|
|
720
|
+
logger.debug('EventService', 'Skipped auto-selection - persisted event loaded');
|
|
721
|
+
} else if (this.userClearedEventRef) {
|
|
722
|
+
logger.debug('EventService', 'Skipped auto-selection - user explicitly cleared event');
|
|
636
723
|
}
|
|
637
724
|
} else {
|
|
638
725
|
// If skipping persisted event load, still do auto-selection for new users
|
|
639
726
|
if (!this.userClearedEventRef) {
|
|
640
727
|
const nextEvent = this.getNextEventByDate(transformedEvents);
|
|
728
|
+
logger.debug('EventService', 'Auto-selection attempt (skip persisted)', {
|
|
729
|
+
nextEventFound: !!nextEvent,
|
|
730
|
+
nextEventId: nextEvent?.event_id
|
|
731
|
+
});
|
|
641
732
|
if (nextEvent) {
|
|
642
733
|
this.hasAutoSelectedRef = true;
|
|
643
734
|
// Use setSelectedEvent() to ensure consistent behavior
|
|
644
735
|
// Theme will be applied by useEventTheme() hook
|
|
645
736
|
this.setSelectedEvent(nextEvent);
|
|
737
|
+
logger.debug('EventService', 'Auto-selected next event (skip persisted)', {
|
|
738
|
+
eventId: nextEvent.event_id,
|
|
739
|
+
eventName: nextEvent.event_name
|
|
740
|
+
});
|
|
646
741
|
}
|
|
742
|
+
} else {
|
|
743
|
+
logger.debug('EventService', 'Skipped auto-selection (skip persisted) - user explicitly cleared event');
|
|
647
744
|
}
|
|
648
745
|
}
|
|
649
746
|
}
|