@jmruthers/pace-core 0.5.105 → 0.5.107
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-BE0OXZKQ.d.ts → DataTable-D5cBRca8.d.ts} +1 -1
- package/dist/{DataTable-LWHFLTEW.js → DataTable-H2WIR2DN.js} +3 -3
- package/dist/{chunk-QPCAGLUS.js → chunk-4OX5PXHX.js} +5 -2
- package/dist/chunk-4OX5PXHX.js.map +1 -0
- package/dist/{chunk-75G3NZWN.js → chunk-5JJCXTVE.js} +293 -37
- package/dist/chunk-5JJCXTVE.js.map +1 -0
- package/dist/{chunk-HBGPLSA5.js → chunk-DMNMZKWS.js} +70 -24
- package/dist/chunk-DMNMZKWS.js.map +1 -0
- package/dist/{chunk-AZFPGDCJ.js → chunk-EWKCROSF.js} +133 -49
- package/dist/chunk-EWKCROSF.js.map +1 -0
- package/dist/{chunk-4BWGRQBG.js → chunk-NFPV7MRN.js} +22 -2
- package/dist/chunk-NFPV7MRN.js.map +1 -0
- package/dist/{chunk-DWYMGSGU.js → chunk-VJ7MPS2K.js} +2 -2
- package/dist/components.d.ts +3 -3
- package/dist/components.js +4 -4
- package/dist/{formatting-BfDeV-ja.d.ts → formatting-BiEv5oEk.d.ts} +32 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +6 -6
- package/dist/{types-BDg1mAGG.d.ts → types-D4TVpDa1.d.ts} +24 -1
- package/dist/{useToast-Bm6TnSK-.d.ts → useToast-DRah6K-g.d.ts} +5 -2
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +4 -4
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +18 -18
- package/docs/api/interfaces/DataTableColumn.md +115 -10
- package/docs/api/interfaces/DataTableProps.md +38 -38
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +39 -18
- package/docs/api-reference/utilities.md +26 -3
- package/docs/implementation-guides/data-tables.md +390 -0
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.tsx +4 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +25 -10
- package/src/components/DataTable/components/EditableRow.tsx +174 -16
- package/src/components/DataTable/components/UnifiedTableBody.tsx +205 -35
- package/src/components/DataTable/types.ts +34 -4
- package/src/components/FileDisplay/FileDisplay.test.tsx +184 -201
- package/src/components/FileDisplay/FileDisplay.tsx +40 -39
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +189 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +142 -35
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +4 -4
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/public/usePublicFileDisplay.ts +25 -15
- package/src/hooks/useEventTheme.test.ts +11 -0
- package/src/hooks/useFileDisplay.ts +11 -0
- package/src/hooks/useSecureDataAccess.test.ts +22 -5
- package/src/hooks/useToast.ts +11 -2
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +67 -3
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +72 -4
- package/src/services/__tests__/OrganisationService.pagination.test.ts +10 -2
- package/src/styles/core.css +11 -0
- package/src/utils/__tests__/formatting.unit.test.ts +33 -0
- package/src/utils/file-reference.test.ts +44 -5
- package/src/utils/file-reference.ts +49 -26
- package/src/utils/formatting.ts +57 -2
- package/src/validation/__tests__/passwordSchema.unit.test.ts +3 -3
- package/dist/chunk-4BWGRQBG.js.map +0 -1
- package/dist/chunk-75G3NZWN.js.map +0 -1
- package/dist/chunk-AZFPGDCJ.js.map +0 -1
- package/dist/chunk-HBGPLSA5.js.map +0 -1
- package/dist/chunk-QPCAGLUS.js.map +0 -1
- /package/dist/{DataTable-LWHFLTEW.js.map → DataTable-H2WIR2DN.js.map} +0 -0
- /package/dist/{chunk-DWYMGSGU.js.map → chunk-VJ7MPS2K.js.map} +0 -0
|
@@ -2,9 +2,16 @@ import React from 'react';
|
|
|
2
2
|
import { renderWithProviders, screen, userEvent, act, waitFor } from '../../__tests__/helpers/test-utils';
|
|
3
3
|
import { FileDisplay } from './FileDisplay';
|
|
4
4
|
import { createMockSupabaseClient } from '../../__tests__/helpers/test-utils';
|
|
5
|
+
import { UnifiedAuthProvider } from '../../providers/services/UnifiedAuthProvider';
|
|
5
6
|
|
|
6
7
|
const supabase = createMockSupabaseClient() as any;
|
|
7
8
|
|
|
9
|
+
// Mock useIsPublicPage to return false so FileDisplay uses authenticated version
|
|
10
|
+
vi.mock('../PublicLayout/PublicPageProvider', () => ({
|
|
11
|
+
useIsPublicPage: vi.fn(() => false),
|
|
12
|
+
PublicPageContext: React.createContext(null)
|
|
13
|
+
}));
|
|
14
|
+
|
|
8
15
|
vi.mock('../../hooks/useFileReference', () => ({
|
|
9
16
|
useFileReferenceForRecord: vi.fn(),
|
|
10
17
|
useFileReference: vi.fn(() => ({
|
|
@@ -14,11 +21,32 @@ vi.mock('../../hooks/useFileReference', () => ({
|
|
|
14
21
|
})),
|
|
15
22
|
}));
|
|
16
23
|
|
|
24
|
+
// Mock useFileDisplay hook which is used by FileDisplayAuthenticated
|
|
25
|
+
vi.mock('../../hooks/useFileDisplay', () => ({
|
|
26
|
+
useFileDisplay: vi.fn()
|
|
27
|
+
}));
|
|
28
|
+
|
|
17
29
|
vi.mock('../../utils/storage/helpers', () => ({
|
|
18
30
|
getPublicUrl: vi.fn((_, path: string) => `https://example.com/${path}`),
|
|
19
31
|
getSignedUrl: vi.fn(async (_: any, path: string) => ({ url: `https://signed.example.com/${path}` })),
|
|
20
32
|
}));
|
|
21
33
|
|
|
34
|
+
// Helper to render with UnifiedAuthProvider
|
|
35
|
+
const renderWithUnifiedAuth = (ui: React.ReactElement) => {
|
|
36
|
+
return renderWithProviders(
|
|
37
|
+
<UnifiedAuthProvider
|
|
38
|
+
supabaseClient={supabase}
|
|
39
|
+
appName="test-app"
|
|
40
|
+
idleTimeoutMs={30 * 60 * 1000}
|
|
41
|
+
warnBeforeMs={60 * 1000}
|
|
42
|
+
onIdleLogout={() => {}}
|
|
43
|
+
dangerouslyDisableInactivity={true}
|
|
44
|
+
>
|
|
45
|
+
{ui}
|
|
46
|
+
</UnifiedAuthProvider>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
22
50
|
const baseProps = {
|
|
23
51
|
supabase,
|
|
24
52
|
table_name: 'person',
|
|
@@ -27,59 +55,62 @@ const baseProps = {
|
|
|
27
55
|
};
|
|
28
56
|
|
|
29
57
|
describe('[component] FileDisplay', () => {
|
|
30
|
-
beforeEach(() => {
|
|
58
|
+
beforeEach(async () => {
|
|
31
59
|
vi.clearAllMocks();
|
|
60
|
+
// Set up default mock for useFileDisplay
|
|
61
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
62
|
+
useFileDisplay.mockReturnValue({
|
|
63
|
+
isLoading: false,
|
|
64
|
+
error: null,
|
|
65
|
+
fileUrl: null,
|
|
66
|
+
fileReference: null,
|
|
67
|
+
fileReferences: [],
|
|
68
|
+
fileCount: 0,
|
|
69
|
+
fileUrls: new Map(),
|
|
70
|
+
refetch: vi.fn(),
|
|
71
|
+
});
|
|
32
72
|
});
|
|
33
73
|
|
|
34
74
|
it('renders error state and clears error on retry', async () => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
75
|
+
const refetch = vi.fn();
|
|
76
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
77
|
+
useFileDisplay.mockReturnValue({
|
|
38
78
|
isLoading: false,
|
|
39
|
-
error: 'boom',
|
|
79
|
+
error: new Error('boom'),
|
|
40
80
|
fileUrl: null,
|
|
41
81
|
fileReference: null,
|
|
42
82
|
fileReferences: [],
|
|
43
83
|
fileCount: 0,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
loadFileReferences: vi.fn(),
|
|
47
|
-
loadFileCount: vi.fn(),
|
|
48
|
-
deleteFile: vi.fn(),
|
|
49
|
-
clearError,
|
|
84
|
+
fileUrls: new Map(),
|
|
85
|
+
refetch,
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
|
|
88
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} />);
|
|
53
89
|
|
|
54
90
|
expect(screen.getByText(/Error loading file: boom/i)).toBeInTheDocument();
|
|
55
91
|
const retry = screen.getByRole('button', { name: /Try again/i });
|
|
56
92
|
await userEvent.click(retry);
|
|
57
|
-
expect(
|
|
93
|
+
expect(refetch).toHaveBeenCalled();
|
|
58
94
|
});
|
|
59
95
|
|
|
60
96
|
it('renders empty state when no files are found', async () => {
|
|
61
|
-
const
|
|
62
|
-
|
|
97
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
98
|
+
useFileDisplay.mockReturnValue({
|
|
63
99
|
isLoading: false,
|
|
64
100
|
error: null,
|
|
65
101
|
fileUrl: null,
|
|
66
102
|
fileReference: null,
|
|
67
103
|
fileReferences: [],
|
|
68
104
|
fileCount: 0,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
loadFileReferences: vi.fn(),
|
|
72
|
-
loadFileCount: vi.fn(),
|
|
73
|
-
deleteFile: vi.fn(),
|
|
74
|
-
clearError: vi.fn(),
|
|
105
|
+
fileUrls: new Map(),
|
|
106
|
+
refetch: vi.fn(),
|
|
75
107
|
});
|
|
76
108
|
|
|
77
|
-
|
|
109
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} />);
|
|
78
110
|
expect(screen.getByText(/No files found/i)).toBeInTheDocument();
|
|
79
111
|
});
|
|
80
112
|
|
|
81
113
|
it('renders a single image with delete button and handles delete', async () => {
|
|
82
|
-
const deleteFile = vi.fn(async () => true);
|
|
83
114
|
const categoryFile = {
|
|
84
115
|
id: 'fr-1',
|
|
85
116
|
table_name: 'person',
|
|
@@ -90,49 +121,36 @@ describe('[component] FileDisplay', () => {
|
|
|
90
121
|
file_metadata: { fileName: 'file.png', fileSize: 2048, fileType: 'image/png' },
|
|
91
122
|
};
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
useFileReference.mockReturnValue({
|
|
96
|
-
getFilesByCategory: vi.fn(() => Promise.resolve([categoryFile])),
|
|
97
|
-
isLoading: false,
|
|
98
|
-
error: null,
|
|
99
|
-
});
|
|
124
|
+
const fileUrlsMap = new Map<string, string>();
|
|
125
|
+
fileUrlsMap.set('fr-1', 'https://example.com/file.png');
|
|
100
126
|
|
|
101
|
-
const
|
|
102
|
-
|
|
127
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
128
|
+
useFileDisplay.mockReturnValue({
|
|
103
129
|
isLoading: false,
|
|
104
130
|
error: null,
|
|
105
|
-
fileUrl:
|
|
106
|
-
fileReference:
|
|
107
|
-
fileReferences: [],
|
|
108
|
-
fileCount:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
loadFileReferences: vi.fn(),
|
|
112
|
-
loadFileCount: vi.fn(),
|
|
113
|
-
deleteFile,
|
|
114
|
-
clearError: vi.fn(),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Mock getSignedUrl for the category file URL generation
|
|
118
|
-
const { getSignedUrl } = await import('../../utils/storage/helpers');
|
|
119
|
-
vi.spyOn(await import('../../utils/storage/helpers'), 'getSignedUrl').mockResolvedValue({
|
|
120
|
-
url: 'https://example.com/file.png'
|
|
131
|
+
fileUrl: 'https://example.com/file.png',
|
|
132
|
+
fileReference: categoryFile,
|
|
133
|
+
fileReferences: [categoryFile],
|
|
134
|
+
fileCount: 1,
|
|
135
|
+
fileUrls: fileUrlsMap,
|
|
136
|
+
refetch: vi.fn(),
|
|
121
137
|
});
|
|
122
138
|
|
|
123
139
|
// Confirm dialog needs to be accepted for deletion
|
|
124
140
|
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} showDelete category={1 as any} />);
|
|
127
143
|
|
|
128
144
|
const image = await screen.findByRole('img', { name: /file\.png/i });
|
|
129
145
|
expect(image).toBeInTheDocument();
|
|
130
146
|
|
|
131
147
|
const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
|
|
132
148
|
await userEvent.click(deleteBtn);
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
149
|
+
|
|
150
|
+
// Dialog should open
|
|
151
|
+
expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
|
|
152
|
+
const confirmDeleteBtn = screen.getByRole('button', { name: /^Delete$/i });
|
|
153
|
+
await userEvent.click(confirmDeleteBtn);
|
|
136
154
|
|
|
137
155
|
confirmSpy.mockRestore();
|
|
138
156
|
});
|
|
@@ -148,42 +166,24 @@ describe('[component] FileDisplay', () => {
|
|
|
148
166
|
file_metadata: { fileName: 'report.pdf', fileSize: 4096, fileType: 'application/pdf' },
|
|
149
167
|
};
|
|
150
168
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
useFileReference.mockReturnValue({
|
|
154
|
-
getFilesByCategory: vi.fn(() => Promise.resolve([categoryFile])),
|
|
155
|
-
isLoading: false,
|
|
156
|
-
error: null,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const useFileReferenceForRecord = (await import('../../hooks/useFileReference')).useFileReferenceForRecord as unknown as vi.Mock;
|
|
160
|
-
useFileReferenceForRecord.mockReturnValue({
|
|
169
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
170
|
+
useFileDisplay.mockReturnValue({
|
|
161
171
|
isLoading: false,
|
|
162
172
|
error: null,
|
|
163
|
-
fileUrl: null,
|
|
164
|
-
fileReference:
|
|
165
|
-
fileReferences: [],
|
|
166
|
-
fileCount:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
loadFileReferences: vi.fn(),
|
|
170
|
-
loadFileCount: vi.fn(),
|
|
171
|
-
deleteFile: vi.fn(),
|
|
172
|
-
clearError: vi.fn(),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Mock getSignedUrl for the category file URL generation
|
|
176
|
-
const { getSignedUrl } = await import('../../utils/storage/helpers');
|
|
177
|
-
vi.spyOn(await import('../../utils/storage/helpers'), 'getSignedUrl').mockResolvedValue({
|
|
178
|
-
url: null // PDFs don't need URLs for display
|
|
173
|
+
fileUrl: null, // PDFs don't need URLs for display
|
|
174
|
+
fileReference: categoryFile,
|
|
175
|
+
fileReferences: [categoryFile],
|
|
176
|
+
fileCount: 1,
|
|
177
|
+
fileUrls: new Map(),
|
|
178
|
+
refetch: vi.fn(),
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} category={1 as any} />);
|
|
182
182
|
expect(await screen.findByText('report.pdf')).toBeInTheDocument();
|
|
183
183
|
expect(screen.getByText(/application\/pdf/i)).toBeInTheDocument();
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
it
|
|
186
|
+
it('renders multiple files and shows download link for non-image files', async () => {
|
|
187
187
|
const fileRefs = [
|
|
188
188
|
{
|
|
189
189
|
id: 'f1',
|
|
@@ -205,60 +205,45 @@ describe('[component] FileDisplay', () => {
|
|
|
205
205
|
},
|
|
206
206
|
];
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
// Create a stable Map reference that won't trigger unnecessary resets
|
|
209
|
+
const fileUrlsMap = new Map<string, string>();
|
|
210
|
+
fileUrlsMap.set('f1', 'https://example.com/bucket/path/a.png');
|
|
211
|
+
fileUrlsMap.set('f2', 'https://signed.example.com/bucket/path/b.pdf');
|
|
212
|
+
|
|
213
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
214
|
+
useFileDisplay.mockReturnValue({
|
|
210
215
|
isLoading: false,
|
|
211
216
|
error: null,
|
|
212
217
|
fileUrl: null,
|
|
213
218
|
fileReference: null,
|
|
214
219
|
fileReferences: fileRefs,
|
|
215
220
|
fileCount: 2,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
loadFileReferences: vi.fn(),
|
|
219
|
-
loadFileCount: vi.fn(),
|
|
220
|
-
deleteFile: vi.fn(),
|
|
221
|
-
clearError: vi.fn(),
|
|
221
|
+
fileUrls: fileUrlsMap,
|
|
222
|
+
refetch: vi.fn(),
|
|
222
223
|
});
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
const storageHelpers = await import('../../utils/storage/helpers');
|
|
226
|
-
const getPublicUrlMock = vi.mocked(storageHelpers.getPublicUrl);
|
|
227
|
-
const getSignedUrlMock = vi.mocked(storageHelpers.getSignedUrl);
|
|
225
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} />);
|
|
228
226
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// Wait for files to render (they should appear immediately)
|
|
227
|
+
// Wait for files to render - both file names should appear
|
|
232
228
|
await waitFor(() => {
|
|
233
229
|
expect(screen.getByText('a.png')).toBeInTheDocument();
|
|
234
230
|
expect(screen.getByText('b.pdf')).toBeInTheDocument();
|
|
235
231
|
});
|
|
236
232
|
|
|
237
|
-
// The
|
|
238
|
-
//
|
|
239
|
-
// Wait for getPublicUrl to be called (it should be called immediately for the public file)
|
|
233
|
+
// The component syncs fileUrls in useEffect - wait for the sync to complete
|
|
234
|
+
// The download link appears when canDownload is true: !isImage && fileUrl (from internalFileUrls map)
|
|
240
235
|
await waitFor(() => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
'bucket/path/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const image = await screen.findByRole('img', { name: /a\.png/i }, { timeout: 5000 });
|
|
250
|
-
expect(image).toBeInTheDocument();
|
|
251
|
-
// The top-level mock returns `https://example.com/${path}`, so for 'bucket/path/a.png':
|
|
252
|
-
expect(image).toHaveAttribute('src', 'https://example.com/bucket/path/a.png');
|
|
253
|
-
|
|
254
|
-
// Wait for getSignedUrl to be called for the private PDF file
|
|
255
|
-
await waitFor(() => {
|
|
256
|
-
expect(getSignedUrlMock).toHaveBeenCalled();
|
|
257
|
-
}, { timeout: 2000 });
|
|
236
|
+
const link = screen.queryByRole('link');
|
|
237
|
+
if (link) {
|
|
238
|
+
expect(link).toHaveAttribute('href', 'bucket/path/b.pdf');
|
|
239
|
+
expect(link).toHaveAttribute('download', 'b.pdf');
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}, { timeout: 3000 });
|
|
258
244
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
const link = await screen.findByRole('link', { name: /↓/i }, { timeout: 5000 });
|
|
245
|
+
// Verify download link exists and has correct attributes
|
|
246
|
+
const link = screen.getByRole('link');
|
|
262
247
|
expect(link).toBeInTheDocument();
|
|
263
248
|
expect(link).toHaveAttribute('href', 'bucket/path/b.pdf');
|
|
264
249
|
expect(link).toHaveAttribute('download', 'b.pdf');
|
|
@@ -275,27 +260,22 @@ describe('[component] FileDisplay', () => {
|
|
|
275
260
|
file_metadata: { fileName: 'logo.png', fileSize: 1024, fileType: 'image/png' },
|
|
276
261
|
};
|
|
277
262
|
|
|
278
|
-
const
|
|
279
|
-
|
|
263
|
+
const fileUrlsMap = new Map<string, string>();
|
|
264
|
+
fileUrlsMap.set('fr-3', 'https://example.com/logo.png');
|
|
265
|
+
|
|
266
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
267
|
+
useFileDisplay.mockReturnValue({
|
|
280
268
|
isLoading: false,
|
|
281
269
|
error: null,
|
|
282
|
-
fileUrl: null,
|
|
283
|
-
fileReference: null,
|
|
270
|
+
fileUrl: null,
|
|
271
|
+
fileReference: null,
|
|
284
272
|
fileReferences: [logoFileRef],
|
|
285
273
|
fileCount: 1,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
loadFileReferences: vi.fn(),
|
|
289
|
-
loadFileCount: vi.fn(),
|
|
290
|
-
deleteFile: vi.fn(),
|
|
291
|
-
clearError: vi.fn(),
|
|
274
|
+
fileUrls: fileUrlsMap,
|
|
275
|
+
refetch: vi.fn(),
|
|
292
276
|
});
|
|
293
277
|
|
|
294
|
-
|
|
295
|
-
const { getPublicUrl } = await import('../../utils/storage/helpers');
|
|
296
|
-
vi.spyOn(await import('../../utils/storage/helpers'), 'getPublicUrl').mockReturnValue('https://example.com/logo.png');
|
|
297
|
-
|
|
298
|
-
renderWithProviders(<FileDisplay {...baseProps} displayOnly />);
|
|
278
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
|
|
299
279
|
|
|
300
280
|
// Wait for async URL generation in displayOnly mode
|
|
301
281
|
const image = await screen.findByRole('img', { name: /logo\.png/i });
|
|
@@ -310,43 +290,32 @@ describe('[component] FileDisplay', () => {
|
|
|
310
290
|
});
|
|
311
291
|
|
|
312
292
|
it('renders with wrapper when displayOnly is true but showDelete is also true', async () => {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
293
|
+
const logoFileRef = {
|
|
294
|
+
id: 'fr-4',
|
|
295
|
+
table_name: 'event',
|
|
296
|
+
record_id: 'event-1',
|
|
297
|
+
organisation_id: 'org-1',
|
|
298
|
+
file_path: 'bucket/path/logo.png',
|
|
299
|
+
is_public: true,
|
|
300
|
+
file_metadata: { fileName: 'logo.png', fileSize: 1024, fileType: 'image/png' },
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const fileUrlsMap = new Map<string, string>();
|
|
304
|
+
fileUrlsMap.set('fr-4', 'https://example.com/logo.png');
|
|
305
|
+
|
|
306
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
307
|
+
useFileDisplay.mockReturnValue({
|
|
316
308
|
isLoading: false,
|
|
317
309
|
error: null,
|
|
318
310
|
fileUrl: 'https://example.com/logo.png',
|
|
319
|
-
fileReference:
|
|
320
|
-
|
|
321
|
-
table_name: 'event',
|
|
322
|
-
record_id: 'event-1',
|
|
323
|
-
organisation_id: 'org-1',
|
|
324
|
-
file_path: 'bucket/path/logo.png',
|
|
325
|
-
is_public: true,
|
|
326
|
-
file_metadata: { fileName: 'logo.png', fileSize: 1024, fileType: 'image/png' },
|
|
327
|
-
},
|
|
328
|
-
fileReferences: [
|
|
329
|
-
{
|
|
330
|
-
id: 'fr-4',
|
|
331
|
-
table_name: 'event',
|
|
332
|
-
record_id: 'event-1',
|
|
333
|
-
organisation_id: 'org-1',
|
|
334
|
-
file_path: 'bucket/path/logo.png',
|
|
335
|
-
is_public: true,
|
|
336
|
-
file_metadata: { fileName: 'logo.png', fileSize: 1024, fileType: 'image/png' },
|
|
337
|
-
},
|
|
338
|
-
],
|
|
311
|
+
fileReference: logoFileRef,
|
|
312
|
+
fileReferences: [logoFileRef],
|
|
339
313
|
fileCount: 1,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
loadFileReferences: vi.fn(),
|
|
343
|
-
loadFileCount: vi.fn(),
|
|
344
|
-
deleteFile,
|
|
345
|
-
clearError: vi.fn(),
|
|
314
|
+
fileUrls: fileUrlsMap,
|
|
315
|
+
refetch: vi.fn(),
|
|
346
316
|
});
|
|
347
317
|
|
|
348
|
-
|
|
349
|
-
renderWithProviders(<FileDisplay {...baseProps} displayOnly showDelete />);
|
|
318
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly showDelete />);
|
|
350
319
|
|
|
351
320
|
const image = await screen.findByRole('img', { name: /logo\.png/i });
|
|
352
321
|
expect(image).toBeInTheDocument();
|
|
@@ -357,55 +326,56 @@ describe('[component] FileDisplay', () => {
|
|
|
357
326
|
|
|
358
327
|
const deleteBtn = screen.getByRole('button', { name: /Delete file/i });
|
|
359
328
|
await userEvent.click(deleteBtn);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
329
|
+
|
|
330
|
+
// Dialog should open
|
|
331
|
+
expect(await screen.findByText('Confirm Delete')).toBeInTheDocument();
|
|
332
|
+
const confirmDeleteBtn = screen.getByRole('button', { name: /^Delete$/i });
|
|
333
|
+
await userEvent.click(confirmDeleteBtn);
|
|
363
334
|
});
|
|
364
335
|
|
|
365
336
|
it('prefers image files when displayOnly is true and multiple files exist', async () => {
|
|
366
|
-
const
|
|
367
|
-
|
|
337
|
+
const docFile = {
|
|
338
|
+
id: 'fr-doc',
|
|
339
|
+
table_name: 'person',
|
|
340
|
+
record_id: 'rec-1',
|
|
341
|
+
organisation_id: 'org-1',
|
|
342
|
+
file_path: 'bucket/path/doc.pdf',
|
|
343
|
+
is_public: false,
|
|
344
|
+
file_metadata: { fileName: 'doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
|
|
345
|
+
};
|
|
346
|
+
const imageFile = {
|
|
347
|
+
id: 'fr-img',
|
|
348
|
+
table_name: 'person',
|
|
349
|
+
record_id: 'rec-1',
|
|
350
|
+
organisation_id: 'org-1',
|
|
351
|
+
file_path: 'bucket/path/image.png',
|
|
352
|
+
is_public: true,
|
|
353
|
+
file_metadata: { fileName: 'image.png', fileSize: 1024, fileType: 'image/png' },
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const fileUrlsMap = new Map<string, string>();
|
|
357
|
+
fileUrlsMap.set('fr-img', 'https://example.com/image.png');
|
|
358
|
+
|
|
359
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
360
|
+
useFileDisplay.mockReturnValue({
|
|
368
361
|
isLoading: false,
|
|
369
362
|
error: null,
|
|
370
363
|
fileUrl: null,
|
|
371
364
|
fileReference: null,
|
|
372
|
-
fileReferences: [
|
|
373
|
-
{
|
|
374
|
-
id: 'fr-doc',
|
|
375
|
-
table_name: 'person',
|
|
376
|
-
record_id: 'rec-1',
|
|
377
|
-
organisation_id: 'org-1',
|
|
378
|
-
file_path: 'bucket/path/doc.pdf',
|
|
379
|
-
is_public: false,
|
|
380
|
-
file_metadata: { fileName: 'doc.pdf', fileSize: 2048, fileType: 'application/pdf' },
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
id: 'fr-img',
|
|
384
|
-
table_name: 'person',
|
|
385
|
-
record_id: 'rec-1',
|
|
386
|
-
organisation_id: 'org-1',
|
|
387
|
-
file_path: 'bucket/path/image.png',
|
|
388
|
-
is_public: true,
|
|
389
|
-
file_metadata: { fileName: 'image.png', fileSize: 1024, fileType: 'image/png' },
|
|
390
|
-
},
|
|
391
|
-
],
|
|
365
|
+
fileReferences: [docFile, imageFile], // PDF first, but component should prefer image
|
|
392
366
|
fileCount: 2,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
loadFileReferences: vi.fn(),
|
|
396
|
-
loadFileCount: vi.fn(),
|
|
397
|
-
deleteFile: vi.fn(),
|
|
398
|
-
clearError: vi.fn(),
|
|
367
|
+
fileUrls: fileUrlsMap,
|
|
368
|
+
refetch: vi.fn(),
|
|
399
369
|
});
|
|
400
370
|
|
|
401
371
|
// This test verifies that displayOnly logic prefers images
|
|
402
372
|
// The component should select the image file over the PDF
|
|
403
|
-
|
|
373
|
+
renderWithUnifiedAuth(<FileDisplay {...baseProps} displayOnly />);
|
|
404
374
|
|
|
405
375
|
// Wait for the component to process and select the image
|
|
406
376
|
await waitFor(() => {
|
|
407
377
|
// The component should eventually show the image file
|
|
408
|
-
expect(
|
|
378
|
+
expect(screen.queryByRole('img', { name: /image\.png/i })).toBeInTheDocument();
|
|
409
379
|
}, { timeout: 3000 });
|
|
410
380
|
});
|
|
411
381
|
});
|
|
@@ -431,8 +401,20 @@ const mockUseFileReference = vi.fn();
|
|
|
431
401
|
const mockGetPublicUrl = vi.fn();
|
|
432
402
|
const mockGetSignedUrl = vi.fn();
|
|
433
403
|
|
|
434
|
-
beforeEach(() => {
|
|
404
|
+
beforeEach(async () => {
|
|
435
405
|
vi.clearAllMocks();
|
|
406
|
+
// Set up default mock for useFileDisplay
|
|
407
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
408
|
+
useFileDisplay.mockReturnValue({
|
|
409
|
+
isLoading: false,
|
|
410
|
+
error: null,
|
|
411
|
+
fileUrl: null,
|
|
412
|
+
fileReference: null,
|
|
413
|
+
fileReferences: [],
|
|
414
|
+
fileCount: 0,
|
|
415
|
+
fileUrls: new Map(),
|
|
416
|
+
refetch: vi.fn(),
|
|
417
|
+
});
|
|
436
418
|
mockUseFileReferenceForRecord.mockReturnValue({
|
|
437
419
|
isLoading: false,
|
|
438
420
|
error: null,
|
|
@@ -472,9 +454,10 @@ const defaultProps = {
|
|
|
472
454
|
};
|
|
473
455
|
|
|
474
456
|
describe('FileDisplay Component (relocated)', () => {
|
|
475
|
-
it('renders with default props', () => {
|
|
476
|
-
|
|
477
|
-
|
|
457
|
+
it('renders with default props', async () => {
|
|
458
|
+
const useFileDisplay = (await import('../../hooks/useFileDisplay')).useFileDisplay as unknown as vi.Mock;
|
|
459
|
+
renderWithUnifiedAuth(<FileDisplay {...defaultProps} />);
|
|
460
|
+
expect(useFileDisplay).toHaveBeenCalled();
|
|
478
461
|
});
|
|
479
462
|
});
|
|
480
463
|
|
|
@@ -130,21 +130,20 @@ function FileDisplayContent({
|
|
|
130
130
|
}, [fallbackSize, className]);
|
|
131
131
|
|
|
132
132
|
// Sync fileUrls prop with internal state
|
|
133
|
-
|
|
134
|
-
setInternalFileUrls(new Map(fileUrls));
|
|
135
|
-
}, [fileUrls]);
|
|
136
|
-
|
|
137
|
-
// Track file references to detect when they change
|
|
133
|
+
// Track file references to detect when they change and sync URLs
|
|
138
134
|
useEffect(() => {
|
|
139
135
|
const currentIds = fileReferences.map(f => f.id).join(',');
|
|
140
136
|
const prevIds = fileReferencesRef.current.map(f => f.id).join(',');
|
|
141
137
|
|
|
142
138
|
if (currentIds !== prevIds) {
|
|
143
139
|
fileReferencesRef.current = fileReferences;
|
|
144
|
-
// Reset internal URLs when file references change
|
|
145
|
-
setInternalFileUrls(new Map());
|
|
140
|
+
// Reset internal URLs when file references change, then immediately sync from props
|
|
141
|
+
setInternalFileUrls(new Map(fileUrls));
|
|
142
|
+
} else {
|
|
143
|
+
// If file references haven't changed, just sync URLs
|
|
144
|
+
setInternalFileUrls(new Map(fileUrls));
|
|
146
145
|
}
|
|
147
|
-
}, [fileReferences]);
|
|
146
|
+
}, [fileReferences, fileUrls]);
|
|
148
147
|
|
|
149
148
|
const handleDeleteClick = () => {
|
|
150
149
|
setDeleteDialogOpen(true);
|
|
@@ -201,6 +200,38 @@ function FileDisplayContent({
|
|
|
201
200
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
202
201
|
};
|
|
203
202
|
|
|
203
|
+
// Check for errors first - errors should be shown even if fileCount is 0
|
|
204
|
+
if (error) {
|
|
205
|
+
if (ErrorComponent) {
|
|
206
|
+
return <ErrorComponent error={error} retry={clearError} />;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show fallback if enabled
|
|
210
|
+
if (showFallback) {
|
|
211
|
+
return (
|
|
212
|
+
<div className={fallbackClasses} title="File unavailable">
|
|
213
|
+
{computedFallbackText}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div className={`p-4 bg-acc-50 border border-acc-200 rounded-lg ${className}`}>
|
|
220
|
+
<div className="text-acc-600">
|
|
221
|
+
Error loading file: {error instanceof Error ? error.message : String(error)}
|
|
222
|
+
</div>
|
|
223
|
+
{clearError && (
|
|
224
|
+
<button
|
|
225
|
+
onClick={clearError}
|
|
226
|
+
className="mt-2 text-sm text-acc-700 hover:text-acc-800 underline"
|
|
227
|
+
>
|
|
228
|
+
Try again
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
204
235
|
// Show fallback immediately if enabled and we have no files (even during loading)
|
|
205
236
|
// This provides better UX by showing fallback UI instead of a spinner when we know there are no files
|
|
206
237
|
if (fileCount === 0 && !isLoading) {
|
|
@@ -243,37 +274,6 @@ function FileDisplayContent({
|
|
|
243
274
|
);
|
|
244
275
|
}
|
|
245
276
|
|
|
246
|
-
if (error) {
|
|
247
|
-
if (ErrorComponent) {
|
|
248
|
-
return <ErrorComponent error={error} retry={clearError} />;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Show fallback if enabled
|
|
252
|
-
if (showFallback) {
|
|
253
|
-
return (
|
|
254
|
-
<div className={fallbackClasses} title="File unavailable">
|
|
255
|
-
{computedFallbackText}
|
|
256
|
-
</div>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<div className={`p-4 bg-acc-50 border border-acc-200 rounded-lg ${className}`}>
|
|
262
|
-
<div className="text-acc-600">
|
|
263
|
-
Error loading file: {error instanceof Error ? error.message : String(error)}
|
|
264
|
-
</div>
|
|
265
|
-
{clearError && (
|
|
266
|
-
<button
|
|
267
|
-
onClick={clearError}
|
|
268
|
-
className="mt-2 text-sm text-acc-700 hover:text-acc-800 underline"
|
|
269
|
-
>
|
|
270
|
-
Try again
|
|
271
|
-
</button>
|
|
272
|
-
)}
|
|
273
|
-
</div>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
277
|
// Single file display (when category or displayOnly is specified)
|
|
278
278
|
if ((category || displayOnly) && fileReference) {
|
|
279
279
|
const isImage = fileReference.file_metadata.fileType?.startsWith('image/');
|
|
@@ -697,6 +697,7 @@ function FileDisplayAuthenticated({
|
|
|
697
697
|
className={className}
|
|
698
698
|
children={children}
|
|
699
699
|
onDelete={showDelete ? handleDelete : undefined}
|
|
700
|
+
clearError={refetch}
|
|
700
701
|
organisation_id={organisation_id}
|
|
701
702
|
loadingComponent={loadingComponent}
|
|
702
703
|
errorComponent={errorComponent}
|