@jmruthers/pace-core 0.6.6 → 0.6.7
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/{scripts/audit/audit-dependencies.cjs → audit-tool/00-dependencies.cjs} +12 -13
- package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
- package/audit-tool/audits/02-project-structure.cjs +255 -0
- package/audit-tool/audits/03-architecture.cjs +196 -0
- package/audit-tool/audits/04-code-quality.cjs +149 -0
- package/audit-tool/audits/05-styling.cjs +224 -0
- package/audit-tool/audits/06-security-rbac.cjs +544 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
- package/audit-tool/audits/08-testing-documentation.cjs +202 -0
- package/audit-tool/audits/09-operations.cjs +208 -0
- package/audit-tool/index.cjs +291 -0
- package/audit-tool/utils/code-utils.cjs +218 -0
- package/audit-tool/utils/file-utils.cjs +230 -0
- package/audit-tool/utils/report-utils.cjs +241 -0
- package/cursor-rules/00-standards-overview.mdc +156 -0
- package/cursor-rules/{00-pace-core-compliance.mdc → 01-pace-core-compliance.mdc} +187 -34
- package/cursor-rules/02-project-structure.mdc +37 -5
- package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +125 -11
- package/cursor-rules/04-code-quality.mdc +419 -0
- package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +55 -10
- package/cursor-rules/{09-rbac-compliance.mdc → 06-security-rbac.mdc} +62 -6
- package/cursor-rules/07-api-tech-stack.mdc +377 -0
- package/cursor-rules/08-testing-documentation.mdc +324 -0
- package/cursor-rules/09-operations.mdc +365 -0
- package/dist/DataTable-7PMH7XN7.js +15 -0
- package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +26 -16
- package/dist/{chunk-FENMYN2U.js → chunk-5X4QLXRG.js} +1 -3
- package/dist/{chunk-4T7OBVTU.js → chunk-6F3IILHI.js} +1 -1
- package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
- package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
- package/dist/{chunk-7TYHROIV.js → chunk-BM4CQ5P3.js} +50 -8
- package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
- package/dist/{chunk-OHIK3MIO.js → chunk-GHYHJTYV.js} +2 -2
- package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
- package/dist/{chunk-LAZMKTTF.js → chunk-JGWDVX64.js} +281 -347
- package/dist/{chunk-MAGBIDNS.js → chunk-L4XMVJKY.js} +2 -2
- package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
- package/dist/{chunk-ZS5VO5JB.js → chunk-Q7Q7V5NV.js} +406 -451
- package/dist/{chunk-3O3WHILE.js → chunk-VBCS3DUA.js} +236 -60
- package/dist/{chunk-BVP2BCJF.js → chunk-ZKAWKYT4.js} +8 -8
- package/dist/components.d.ts +5 -4
- package/dist/components.js +27 -32
- package/dist/eslint-rules/index.cjs +22 -9
- package/{src/eslint-rules/rules/compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +184 -23
- package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
- package/dist/eslint-rules/rules/05-styling.cjs +61 -0
- package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +26 -10
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +18 -17
- package/dist/rbac/index.js +6 -6
- package/dist/theming/runtime.d.ts +14 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
- package/dist/{usePublicRouteParams-COZ28Mvq.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +19 -19
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +8 -8
- package/docs/README.md +1 -1
- package/docs/api/modules.md +47 -31
- package/docs/api-reference/components.md +18 -20
- package/docs/api-reference/hooks.md +80 -80
- package/docs/api-reference/types.md +1 -1
- package/docs/api-reference/utilities.md +1 -1
- package/docs/architecture/README.md +1 -1
- package/docs/core-concepts/events.md +3 -3
- package/docs/core-concepts/organisations.md +6 -6
- package/docs/core-concepts/permissions.md +6 -6
- package/docs/documentation-index.md +12 -18
- package/docs/getting-started/documentation-index.md +1 -1
- package/docs/getting-started/examples/README.md +4 -4
- package/docs/getting-started/examples/full-featured-app.md +1 -1
- package/docs/getting-started/faq.md +2 -2
- package/docs/getting-started/quick-reference.md +4 -4
- package/docs/implementation-guides/authentication.md +15 -15
- package/docs/implementation-guides/component-styling.md +1 -1
- package/docs/implementation-guides/data-tables.md +126 -33
- package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
- package/docs/implementation-guides/dynamic-colors.md +3 -3
- package/docs/implementation-guides/file-upload-storage.md +2 -2
- package/docs/implementation-guides/hierarchical-datatable.md +40 -60
- package/docs/implementation-guides/inactivity-tracking.md +3 -3
- package/docs/implementation-guides/large-datasets.md +3 -2
- package/docs/implementation-guides/organisation-security.md +2 -2
- package/docs/implementation-guides/performance.md +2 -2
- package/docs/implementation-guides/permission-enforcement.md +1 -1
- package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
- package/docs/migration/V0.4.0_rbac-migration.md +6 -6
- package/docs/rbac/README.md +5 -5
- package/docs/rbac/advanced-patterns.md +6 -6
- package/docs/rbac/api-reference.md +20 -20
- package/docs/rbac/event-based-apps.md +3 -3
- package/docs/rbac/examples.md +41 -41
- package/docs/rbac/getting-started.md +37 -37
- package/docs/rbac/performance.md +1 -1
- package/docs/rbac/quick-start.md +52 -52
- package/docs/rbac/secure-client-protection.md +1 -1
- package/docs/rbac/troubleshooting.md +1 -1
- package/docs/security/README.md +5 -5
- package/docs/standards/0-standards-overview.md +220 -0
- package/docs/standards/{00-pace-core-compliance.md → 1-pace-core-compliance-standards.md} +204 -185
- package/docs/standards/{02-project-structure.md → 2-project-structure-standards.md} +11 -47
- package/docs/standards/3-architecture-standards.md +606 -0
- package/docs/standards/4-code-quality-standards.md +728 -0
- package/docs/standards/{08-markup-quality.md → 5-styling-standards.md} +12 -9
- package/docs/standards/{09-rbac-compliance.md → 6-security-rbac-standards.md} +126 -18
- package/docs/standards/7-api-tech-stack-standards.md +662 -0
- package/docs/standards/8-testing-documentation-standards.md +401 -0
- package/docs/standards/9-operations-standards.md +1102 -0
- package/docs/standards/README.md +203 -104
- package/docs/troubleshooting/README.md +4 -4
- package/docs/troubleshooting/common-issues.md +2 -2
- package/docs/troubleshooting/debugging.md +9 -9
- package/docs/troubleshooting/migration.md +4 -4
- package/eslint-config-pace-core.cjs +21 -10
- package/package.json +6 -5
- package/scripts/install-cursor-rules.cjs +11 -243
- package/scripts/install-eslint-config.cjs +284 -0
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +10 -10
- package/src/__tests__/integration/UserProfile.test.tsx +14 -14
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
- package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
- package/src/__tests__/templates/component.test.template.tsx +18 -15
- package/src/components/Calendar/Calendar.tsx +201 -47
- package/src/components/ContextSelector/ContextSelector.tsx +137 -153
- package/src/components/DataTable/AUDIT_REPORT.md +293 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
- package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
- package/src/components/DataTable/components/DataTableLayout.tsx +5 -16
- package/src/components/DataTable/components/EditableRow.tsx +5 -7
- package/src/components/DataTable/components/EmptyState.tsx +10 -9
- package/src/components/DataTable/components/FilterRow.tsx +2 -4
- package/src/components/DataTable/components/ImportModal.tsx +124 -126
- package/src/components/DataTable/components/LoadingState.tsx +5 -6
- package/src/components/DataTable/components/SortIndicator.tsx +50 -0
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
- package/src/components/DataTable/components/index.ts +2 -1
- package/src/components/DataTable/types.ts +0 -18
- package/src/components/DataTable/utils/a11yUtils.ts +17 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
- package/src/components/DateTimeField/DateTimeField.tsx +7 -8
- package/src/components/Dialog/Dialog.test.tsx +1 -0
- package/src/components/Dialog/Dialog.tsx +25 -8
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
- package/src/components/FileUpload/FileUpload.test.tsx +52 -14
- package/src/components/FileUpload/FileUpload.tsx +112 -130
- package/src/components/Progress/Progress.tsx +2 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
- package/src/components/Select/Select.tsx +86 -77
- package/src/components/Select/types.ts +3 -0
- package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
- package/src/hooks/__tests__/hooks.integration.test.tsx +49 -49
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
- package/src/hooks/public/usePublicEvent.ts +5 -5
- package/src/hooks/public/usePublicEventLogo.ts +5 -5
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.ts +5 -5
- package/src/hooks/useAppConfig.ts +2 -2
- package/src/hooks/useEventTheme.test.ts +7 -7
- package/src/hooks/useEventTheme.ts +1 -4
- package/src/hooks/useFileDisplay.ts +2 -2
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
- package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
- package/src/providers/__tests__/EventProvider.test.tsx +61 -61
- package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
- package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +10 -10
- package/src/styles/core.css +7 -0
- package/src/theming/__tests__/parseEventColours.test.ts +9 -3
- package/src/theming/parseEventColours.ts +22 -10
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- package/src/utils/storage/README.md +1 -1
- package/cursor-rules/01-standards-compliance.mdc +0 -285
- package/cursor-rules/04-testing-standards.mdc +0 -270
- package/cursor-rules/05-bug-reports-and-features.mdc +0 -248
- package/cursor-rules/06-code-quality.mdc +0 -311
- package/cursor-rules/07-tech-stack-compliance.mdc +0 -216
- package/cursor-rules/10-error-handling-patterns.mdc +0 -179
- package/cursor-rules/11-performance-optimization.mdc +0 -169
- package/cursor-rules/12-ci-cd-integration.mdc +0 -150
- package/dist/DataTable-LRJL4IRV.js +0 -15
- package/dist/eslint-rules/rules/compliance.cjs +0 -348
- package/dist/eslint-rules/rules/components.cjs +0 -113
- package/dist/eslint-rules/rules/imports.cjs +0 -102
- package/docs/best-practices/README.md +0 -472
- package/docs/best-practices/accessibility.md +0 -604
- package/docs/best-practices/common-patterns.md +0 -516
- package/docs/best-practices/deployment.md +0 -1103
- package/docs/best-practices/performance.md +0 -1328
- package/docs/best-practices/security.md +0 -940
- package/docs/best-practices/testing.md +0 -1034
- package/docs/rbac/compliance/compliance-guide.md +0 -544
- package/docs/standards/01-standards-compliance.md +0 -188
- package/docs/standards/03-solid-principles.md +0 -39
- package/docs/standards/04-testing-standards.md +0 -36
- package/docs/standards/05-bug-reports-and-features.md +0 -27
- package/docs/standards/06-code-quality.md +0 -34
- package/docs/standards/07-tech-stack-compliance.md +0 -30
- package/docs/standards/10-error-handling-patterns.md +0 -401
- package/docs/standards/11-performance-optimization.md +0 -348
- package/docs/standards/12-ci-cd-integration.md +0 -370
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +0 -192
- package/scripts/audit/audit-compliance.cjs +0 -1295
- package/scripts/audit/audit-components.cjs +0 -260
- package/scripts/audit/audit-rbac.cjs +0 -954
- package/scripts/audit/audit-standards.cjs +0 -1268
- package/scripts/audit/index.cjs +0 -1927
- package/src/components/DataTable/components/DataTableBody.tsx +0 -478
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
- package/src/components/DataTable/components/ExpandButton.tsx +0 -113
- package/src/components/DataTable/components/GroupHeader.tsx +0 -54
- package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
- package/src/components/DataTable/core/DataTableContext.tsx +0 -216
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
- package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
- package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
- package/src/components/DataTable/utils/debugTools.ts +0 -514
- package/src/eslint-rules/index.cjs +0 -22
- package/src/eslint-rules/rules/components.cjs +0 -113
- package/src/eslint-rules/rules/imports.cjs +0 -102
- package/src/eslint-rules/rules/rbac.cjs +0 -790
- package/src/eslint-rules/utils/helpers.cjs +0 -42
- package/src/eslint-rules/utils/manifest-loader.cjs +0 -75
|
@@ -116,17 +116,19 @@ describe('[component] FileUpload', () => {
|
|
|
116
116
|
describe('Drag and Drop', () => {
|
|
117
117
|
it('shows drag state message on dragover', () => {
|
|
118
118
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
119
|
-
const dropArea = screen.
|
|
119
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
120
120
|
|
|
121
121
|
fireEvent.dragOver(dropArea);
|
|
122
|
+
// Component shows "Drop files here..." (with ellipsis)
|
|
122
123
|
expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
|
|
123
124
|
});
|
|
124
125
|
|
|
125
126
|
it('resets drag state on dragleave', () => {
|
|
126
127
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
127
|
-
const dropArea = screen.
|
|
128
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
128
129
|
|
|
129
130
|
fireEvent.dragOver(dropArea);
|
|
131
|
+
// Component shows "Drop files here..." (with ellipsis)
|
|
130
132
|
expect(screen.getByText(/Drop files here/i)).toBeInTheDocument();
|
|
131
133
|
|
|
132
134
|
fireEvent.dragLeave(dropArea);
|
|
@@ -135,24 +137,41 @@ describe('[component] FileUpload', () => {
|
|
|
135
137
|
|
|
136
138
|
it('handles file drop', async () => {
|
|
137
139
|
const onUploadSuccess = vi.fn();
|
|
140
|
+
// Ensure uploadFile mock resolves immediately
|
|
141
|
+
const mockUpload = vi.fn(async () => createMockUploadResult());
|
|
142
|
+
mockUseFileReference.mockReturnValue({
|
|
143
|
+
uploadFile: mockUpload,
|
|
144
|
+
isLoading: false,
|
|
145
|
+
error: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
138
148
|
renderWithProviders(
|
|
139
149
|
<FileUpload {...baseProps} onUploadSuccess={onUploadSuccess} showProgress />
|
|
140
150
|
);
|
|
141
151
|
|
|
142
|
-
const dropArea = screen.
|
|
152
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
143
153
|
const file = createTestFile('test.png', 'image/png');
|
|
144
154
|
|
|
155
|
+
// Use fireEvent.drop with proper dataTransfer mock
|
|
145
156
|
await act(async () => {
|
|
146
157
|
fireEvent.drop(dropArea, {
|
|
147
158
|
dataTransfer: {
|
|
148
159
|
files: [file],
|
|
149
|
-
|
|
160
|
+
items: [{
|
|
161
|
+
kind: 'file',
|
|
162
|
+
type: file.type,
|
|
163
|
+
getAsFile: () => file
|
|
164
|
+
}],
|
|
165
|
+
types: ['Files']
|
|
166
|
+
}
|
|
150
167
|
});
|
|
151
168
|
});
|
|
152
169
|
|
|
170
|
+
// Wait for upload to complete - the uploadFile is called asynchronously
|
|
153
171
|
await waitFor(() => {
|
|
172
|
+
expect(mockUpload).toHaveBeenCalled();
|
|
154
173
|
expect(onUploadSuccess).toHaveBeenCalled();
|
|
155
|
-
});
|
|
174
|
+
}, { timeout: 3000 });
|
|
156
175
|
});
|
|
157
176
|
|
|
158
177
|
it('does not handle drop when disabled', () => {
|
|
@@ -204,7 +223,7 @@ describe('[component] FileUpload', () => {
|
|
|
204
223
|
|
|
205
224
|
it('triggers file input click when drop area is clicked', () => {
|
|
206
225
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
207
|
-
const dropArea = screen.
|
|
226
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
208
227
|
const input = screen.getByTestId('file-input') as HTMLInputElement;
|
|
209
228
|
const clickSpy = vi.spyOn(input, 'click');
|
|
210
229
|
|
|
@@ -398,12 +417,18 @@ describe('[component] FileUpload', () => {
|
|
|
398
417
|
fireEvent.change(input, { target: { files: [file] } });
|
|
399
418
|
});
|
|
400
419
|
|
|
420
|
+
// Wait for upload to complete and file card to be rendered with File icon
|
|
421
|
+
// The File icon is rendered as an SVG inside CardHeader (header) inside Card (article)
|
|
422
|
+
// Since showPreview is false and it's a PDF, the File icon should be rendered
|
|
423
|
+
// In tests, lucide-react icons are mocked with data-testid="lucide-{name}"
|
|
401
424
|
await waitFor(() => {
|
|
425
|
+
// First verify the file name is present
|
|
402
426
|
expect(screen.getByText('test.pdf')).toBeInTheDocument();
|
|
403
|
-
|
|
404
|
-
|
|
427
|
+
|
|
428
|
+
// The File icon is mocked with data-testid="lucide-file" in tests
|
|
429
|
+
const fileIcon = screen.getByTestId('lucide-file');
|
|
405
430
|
expect(fileIcon).toBeInTheDocument();
|
|
406
|
-
});
|
|
431
|
+
}, { timeout: 3000 });
|
|
407
432
|
});
|
|
408
433
|
|
|
409
434
|
it('shows loading spinner when showProgress is false', async () => {
|
|
@@ -429,14 +454,27 @@ describe('[component] FileUpload', () => {
|
|
|
429
454
|
});
|
|
430
455
|
|
|
431
456
|
// Wait for spinner to appear
|
|
457
|
+
// The LoadingSpinner has role="status" but no accessible name
|
|
458
|
+
// When showProgress is false and isUploading is true, spinner appears in CardHeader
|
|
459
|
+
// The upload starts immediately when file is selected, and status changes to 'uploading'
|
|
460
|
+
// Wait for the upload state to be set to 'uploading' which triggers isUploading
|
|
461
|
+
// The spinner appears in the CardHeader (the drop area) when isUploading && !showProgress
|
|
432
462
|
await waitFor(() => {
|
|
433
|
-
|
|
434
|
-
|
|
463
|
+
// The spinner should appear when isUploading is true and showProgress is false
|
|
464
|
+
// isUploading is true when uploadStates has entries with status 'uploading' or 'processing'
|
|
465
|
+
// The spinner is in the CardHeader (the drop area)
|
|
466
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
467
|
+
const spinner = dropArea.querySelector('[role="status"]');
|
|
435
468
|
expect(spinner).toBeInTheDocument();
|
|
469
|
+
// LoadingSpinner renders a canvas element with animate-spin class directly on it
|
|
470
|
+
// So we check if the spinner element itself has the class
|
|
471
|
+
expect(spinner).toHaveClass('animate-spin');
|
|
472
|
+
}, { timeout: 2000 });
|
|
473
|
+
|
|
474
|
+
// Now resolve the upload to complete it
|
|
475
|
+
await act(async () => {
|
|
476
|
+
resolveUpload!(createMockUploadResult());
|
|
436
477
|
});
|
|
437
|
-
|
|
438
|
-
// Resolve upload to clean up
|
|
439
|
-
resolveUpload!(createMockUploadResult());
|
|
440
478
|
});
|
|
441
479
|
});
|
|
442
480
|
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
11
11
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { Check, X, File } from 'lucide-react';
|
|
12
13
|
import { FileCategory, FileUploadResult, UploadProgress } from '../../types/file-reference';
|
|
13
14
|
import { useFileReference } from '../../hooks/useFileReference';
|
|
14
15
|
import { getCurrentAppName } from '../../utils/app/appNameResolver';
|
|
15
16
|
import { getAppId } from '../../utils/app/appIdResolver';
|
|
16
17
|
import { assertAppId } from '../../types/core';
|
|
18
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../Card';
|
|
19
|
+
import { Progress } from '../Progress';
|
|
20
|
+
import { LoadingSpinner } from '../LoadingSpinner';
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Props for the FileUpload component.
|
|
@@ -138,7 +142,7 @@ export function FileUpload({
|
|
|
138
142
|
|
|
139
143
|
// Calculate isUploading and isDisabled early so they can be used in callbacks
|
|
140
144
|
const isUploading = useMemo(() => {
|
|
141
|
-
return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
|
|
145
|
+
return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
|
|
142
146
|
state.progress.status === 'uploading' || state.progress.status === 'processing'
|
|
143
147
|
);
|
|
144
148
|
}, [uploadStates]);
|
|
@@ -154,7 +158,7 @@ export function FileUpload({
|
|
|
154
158
|
resolve(null);
|
|
155
159
|
return;
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
|
|
158
162
|
const reader = new FileReader();
|
|
159
163
|
reader.onload = (e) => {
|
|
160
164
|
resolve(e.target?.result as string || null);
|
|
@@ -203,7 +207,7 @@ export function FileUpload({
|
|
|
203
207
|
if (!files || files.length === 0) return;
|
|
204
208
|
|
|
205
209
|
const fileArray = Array.from(files);
|
|
206
|
-
|
|
210
|
+
|
|
207
211
|
// Validate all files first
|
|
208
212
|
const validationErrors: string[] = [];
|
|
209
213
|
const validFiles: File[] = [];
|
|
@@ -224,11 +228,11 @@ export function FileUpload({
|
|
|
224
228
|
|
|
225
229
|
// Initialize upload states
|
|
226
230
|
const newUploadStates = new Map<string, FileUploadState>();
|
|
227
|
-
|
|
231
|
+
|
|
228
232
|
for (const file of validFiles) {
|
|
229
233
|
const fileId = `${file.name}-${file.size}-${Date.now()}`;
|
|
230
234
|
const preview = showPreview ? (await generatePreview(file)) || undefined : undefined;
|
|
231
|
-
|
|
235
|
+
|
|
232
236
|
const progress: UploadProgress = {
|
|
233
237
|
loaded: 0,
|
|
234
238
|
total: file.size,
|
|
@@ -383,7 +387,7 @@ export function FileUpload({
|
|
|
383
387
|
}
|
|
384
388
|
} catch (err) {
|
|
385
389
|
const errorMessage = err instanceof Error ? err.message : 'Upload failed';
|
|
386
|
-
|
|
390
|
+
|
|
387
391
|
setUploadStates(prev => {
|
|
388
392
|
const updated = new Map(prev);
|
|
389
393
|
const state = updated.get(fileId);
|
|
@@ -432,9 +436,9 @@ export function FileUpload({
|
|
|
432
436
|
e.preventDefault();
|
|
433
437
|
e.stopPropagation();
|
|
434
438
|
setIsDragging(false);
|
|
435
|
-
|
|
439
|
+
|
|
436
440
|
if (isDisabled) return;
|
|
437
|
-
|
|
441
|
+
|
|
438
442
|
const files = e.dataTransfer.files;
|
|
439
443
|
handleFileSelect(files);
|
|
440
444
|
}, [isDisabled, handleFileSelect]);
|
|
@@ -465,8 +469,8 @@ export function FileUpload({
|
|
|
465
469
|
const disabledClasses = isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-sec-50';
|
|
466
470
|
|
|
467
471
|
return (
|
|
468
|
-
<
|
|
469
|
-
<
|
|
472
|
+
<Card className={className}>
|
|
473
|
+
<CardHeader
|
|
470
474
|
role="button"
|
|
471
475
|
tabIndex={isDisabled ? -1 : 0}
|
|
472
476
|
aria-label="File upload area"
|
|
@@ -484,7 +488,7 @@ export function FileUpload({
|
|
|
484
488
|
} : undefined}
|
|
485
489
|
>
|
|
486
490
|
{children || (
|
|
487
|
-
|
|
491
|
+
<>
|
|
488
492
|
<input
|
|
489
493
|
ref={fileInputRef}
|
|
490
494
|
type="file"
|
|
@@ -496,146 +500,124 @@ export function FileUpload({
|
|
|
496
500
|
data-testid="file-input"
|
|
497
501
|
aria-label={accept ? `Upload file${multiple ? 's' : ''} (${accept})` : `Upload file${multiple ? 's' : ''}`}
|
|
498
502
|
/>
|
|
499
|
-
<
|
|
503
|
+
<p className="text-sec-600">
|
|
500
504
|
{isResolvingAppId ? (
|
|
501
505
|
'Resolving app configuration...'
|
|
502
506
|
) : isDragging ? (
|
|
503
507
|
'Drop files here...'
|
|
504
508
|
) : (
|
|
505
509
|
<>
|
|
506
|
-
|
|
510
|
+
Click to upload
|
|
507
511
|
{' '}or drag and drop
|
|
508
512
|
</>
|
|
509
513
|
)}
|
|
510
|
-
</
|
|
511
|
-
<
|
|
514
|
+
</p>
|
|
515
|
+
<p className="text-sm text-sec-500">
|
|
512
516
|
{!isResolvingAppId && accept !== '*/*' && `Accepted formats: ${accept}`}
|
|
513
517
|
{!isResolvingAppId && maxSize && ` • Max size: ${Math.round(maxSize / 1024 / 1024)}MB`}
|
|
514
518
|
{!isResolvingAppId && multiple && ' • Multiple files allowed'}
|
|
515
|
-
</
|
|
516
|
-
|
|
519
|
+
</p>
|
|
520
|
+
</>
|
|
517
521
|
)}
|
|
518
|
-
|
|
522
|
+
|
|
519
523
|
{isUploading && !showProgress && (
|
|
520
|
-
|
|
521
|
-
className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center"
|
|
522
|
-
role="status"
|
|
523
|
-
aria-live="polite"
|
|
524
|
-
aria-label="Uploading file"
|
|
525
|
-
>
|
|
526
|
-
<div className="animate-spin rounded-full size-8 border-b-2 border-main-500" aria-hidden="true"></div>
|
|
527
|
-
</div>
|
|
524
|
+
<LoadingSpinner size="lg" className="text-main-500" />
|
|
528
525
|
)}
|
|
529
|
-
</
|
|
526
|
+
</CardHeader>
|
|
530
527
|
|
|
531
528
|
{/* Upload Progress List */}
|
|
532
529
|
{showProgress && uploadStates.size > 0 && (
|
|
533
|
-
<
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
{
|
|
573
|
-
{
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
<div
|
|
582
|
-
className={`h-2 rounded-full transition-all duration-300 ${
|
|
583
|
-
isError ? 'bg-acc-500' : 'bg-main-500'
|
|
584
|
-
}`}
|
|
585
|
-
style={{ width: `${progress.percentage}%` }}
|
|
530
|
+
<CardContent>
|
|
531
|
+
{Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
|
|
532
|
+
const { file, progress, preview, result } = uploadState;
|
|
533
|
+
const isError = progress.status === 'error';
|
|
534
|
+
const isCompleted = progress.status === 'completed';
|
|
535
|
+
const isUploading = progress.status === 'uploading' || progress.status === 'processing';
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<Card
|
|
539
|
+
key={fileId}
|
|
540
|
+
className={`grid grid-cols-[auto_1fr_auto] items-center gap-3 ${isError
|
|
541
|
+
? 'bg-acc-50 border-acc-200'
|
|
542
|
+
: isCompleted
|
|
543
|
+
? 'bg-success-50 border-success-200'
|
|
544
|
+
: 'bg-sec-50 border-sec-200'
|
|
545
|
+
}`}
|
|
546
|
+
>
|
|
547
|
+
<CardHeader className="p-0">
|
|
548
|
+
{preview ? (
|
|
549
|
+
<img
|
|
550
|
+
src={preview}
|
|
551
|
+
alt={file.name}
|
|
552
|
+
className="size-12 object-cover rounded"
|
|
553
|
+
/>
|
|
554
|
+
) : (
|
|
555
|
+
<File className="size-12 text-sec-600" />
|
|
556
|
+
)}
|
|
557
|
+
</CardHeader>
|
|
558
|
+
|
|
559
|
+
<CardContent className="p-0 min-w-0">
|
|
560
|
+
<CardTitle className="text-base truncate">
|
|
561
|
+
{file.name}
|
|
562
|
+
</CardTitle>
|
|
563
|
+
<CardDescription>
|
|
564
|
+
{formatFileSize(file.size)}
|
|
565
|
+
{isCompleted && result && ' • Uploaded'}
|
|
566
|
+
{isError && progress.error && ` • ${progress.error}`}
|
|
567
|
+
</CardDescription>
|
|
568
|
+
|
|
569
|
+
{/* Progress Bar */}
|
|
570
|
+
{showProgress && (isUploading || isError) && (
|
|
571
|
+
<>
|
|
572
|
+
<Progress
|
|
573
|
+
value={progress.percentage}
|
|
574
|
+
max={100}
|
|
575
|
+
style={{
|
|
576
|
+
accentColor: isError ? 'var(--color-acc-500)' : 'var(--color-main-500)'
|
|
577
|
+
}}
|
|
586
578
|
/>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
)}
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
);
|
|
616
|
-
})}
|
|
617
|
-
</div>
|
|
618
|
-
)}
|
|
619
|
-
|
|
620
|
-
{appIdError && (
|
|
621
|
-
<div
|
|
622
|
-
className="p-3 bg-acc-50 border border-acc-200 rounded-lg text-sm text-acc-600"
|
|
623
|
-
role="alert"
|
|
624
|
-
aria-live="assertive"
|
|
625
|
-
>
|
|
626
|
-
{appIdError}
|
|
627
|
-
</div>
|
|
579
|
+
{isUploading && (
|
|
580
|
+
<p>
|
|
581
|
+
{progress.percentage}% • {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
|
|
582
|
+
</p>
|
|
583
|
+
)}
|
|
584
|
+
</>
|
|
585
|
+
)}
|
|
586
|
+
</CardContent>
|
|
587
|
+
|
|
588
|
+
<CardFooter className="p-0">
|
|
589
|
+
{isCompleted && (
|
|
590
|
+
<Check className="text-success-500 size-5" />
|
|
591
|
+
)}
|
|
592
|
+
{isError && (
|
|
593
|
+
<X className="text-acc-500 size-5" />
|
|
594
|
+
)}
|
|
595
|
+
{isUploading && (
|
|
596
|
+
<LoadingSpinner size="sm" className="text-main-500" />
|
|
597
|
+
)}
|
|
598
|
+
</CardFooter>
|
|
599
|
+
</Card>
|
|
600
|
+
);
|
|
601
|
+
})}
|
|
602
|
+
|
|
603
|
+
</CardContent>
|
|
628
604
|
)}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
605
|
+
|
|
606
|
+
{(appIdError || error) && (
|
|
607
|
+
<CardFooter>
|
|
608
|
+
{appIdError && (
|
|
609
|
+
<p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
|
|
610
|
+
{appIdError}
|
|
611
|
+
</p>
|
|
612
|
+
)}
|
|
613
|
+
{error && (
|
|
614
|
+
<p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
|
|
615
|
+
{error}
|
|
616
|
+
</p>
|
|
617
|
+
)}
|
|
618
|
+
</CardFooter>
|
|
637
619
|
)}
|
|
638
|
-
</
|
|
620
|
+
</Card>
|
|
639
621
|
);
|
|
640
622
|
}
|
|
641
623
|
|
|
@@ -97,10 +97,8 @@ const Progress = React.forwardRef<
|
|
|
97
97
|
<progress
|
|
98
98
|
ref={ref}
|
|
99
99
|
className={cn(
|
|
100
|
-
'appearance-none border-0 h-2 w-full rounded-full overflow-hidden transition-all accent-
|
|
101
|
-
isIndeterminate
|
|
102
|
-
? 'bg-gradient-to-r from-primary/10 via-primary/90 to-primary/10'
|
|
103
|
-
: 'bg-primary/20',
|
|
100
|
+
'appearance-none border-0 h-2 w-full rounded-full overflow-hidden transition-all accent-main-800',
|
|
101
|
+
!isIndeterminate && 'bg-sec-600/50',
|
|
104
102
|
className
|
|
105
103
|
)}
|
|
106
104
|
{...(isIndeterminate ? {} : { value })}
|
|
@@ -260,9 +260,9 @@ export function ProtectedRoute({
|
|
|
260
260
|
// Use isLoading (combined loading state) for consistency with simpler implementations
|
|
261
261
|
if (isLoading && !sessionRestoration.hasTimedOut) {
|
|
262
262
|
return loadingFallback || (
|
|
263
|
-
<
|
|
263
|
+
<main className="grid place-items-center size-full">
|
|
264
264
|
<LoadingSpinner />
|
|
265
|
-
</
|
|
265
|
+
</main>
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -294,9 +294,9 @@ export function ProtectedRoute({
|
|
|
294
294
|
const isTabVisible = typeof document !== 'undefined' && !document.hidden;
|
|
295
295
|
if (tabJustBecameVisibleRef.current || (isTabVisible && wasAuthenticatedRef.current && isLoading)) {
|
|
296
296
|
return loadingFallback || (
|
|
297
|
-
<
|
|
297
|
+
<main className="grid place-items-center size-full">
|
|
298
298
|
<LoadingSpinner />
|
|
299
|
-
</
|
|
299
|
+
</main>
|
|
300
300
|
);
|
|
301
301
|
}
|
|
302
302
|
|
|
@@ -309,9 +309,9 @@ export function ProtectedRoute({
|
|
|
309
309
|
// Show loading state while we wait for session refresh (unless we're not loading)
|
|
310
310
|
if (isLoading) {
|
|
311
311
|
return loadingFallback || (
|
|
312
|
-
<
|
|
312
|
+
<main className="grid place-items-center size-full">
|
|
313
313
|
<LoadingSpinner />
|
|
314
|
-
</
|
|
314
|
+
</main>
|
|
315
315
|
);
|
|
316
316
|
}
|
|
317
317
|
|
|
@@ -333,14 +333,14 @@ export function ProtectedRoute({
|
|
|
333
333
|
// If no events are available, show error message
|
|
334
334
|
if (!events || events.length === 0) {
|
|
335
335
|
return noEventsFallback || (
|
|
336
|
-
<
|
|
336
|
+
<main className="grid place-items-center text-center min-h-screen p-8">
|
|
337
337
|
<Alert variant="destructive" className="max-w-md">
|
|
338
338
|
<AlertTitle>No Events Available</AlertTitle>
|
|
339
339
|
<AlertDescription>
|
|
340
340
|
You don't have access to any events. Please contact your administrator if you believe this is an error.
|
|
341
341
|
</AlertDescription>
|
|
342
342
|
</Alert>
|
|
343
|
-
</
|
|
343
|
+
</main>
|
|
344
344
|
);
|
|
345
345
|
}
|
|
346
346
|
|