@jmruthers/pace-core 0.6.6 → 0.6.8
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} +227 -22
- package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
- package/audit-tool/audits/02-project-structure.cjs +240 -0
- package/audit-tool/audits/03-architecture.cjs +224 -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 +554 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +355 -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 +295 -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 +380 -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-6RMSCQJ6.js +15 -0
- package/dist/{DataTable-2N_tqbfq.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-BBH6Vqg7.d.ts → PublicPageProvider-CIGSujI2.d.ts} +40 -24
- package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
- package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
- package/dist/{chunk-MAGBIDNS.js → chunk-4DDCYDQ3.js} +8 -7
- package/dist/{chunk-BVP2BCJF.js → chunk-5W2A3DRC.js} +10 -9
- package/dist/{chunk-SD6WQY43.js → chunk-7ILTDCL2.js} +9 -1
- package/dist/{chunk-3QC3KRHK.js → chunk-A3W6LW53.js} +16 -1
- package/dist/{chunk-3O3WHILE.js → chunk-EF2UGZWY.js} +239 -63
- package/dist/{chunk-LAZMKTTF.js → chunk-EURB7QFZ.js} +341 -337
- package/dist/{chunk-2HGJFNAH.js → chunk-FEJLJNWA.js} +1 -15
- package/dist/{chunk-7TYHROIV.js → chunk-GS5672WG.js} +55 -13
- package/dist/{chunk-UIYSCEV7.js → chunk-IUBRCBSY.js} +1 -1
- package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
- package/dist/{chunk-FENMYN2U.js → chunk-MPBLMWVR.js} +3 -3
- package/dist/{chunk-ZS5VO5JB.js → chunk-NKHKXPI4.js} +408 -453
- package/dist/{chunk-A55DK444.js → chunk-OJ4SKRSV.js} +1 -7
- package/dist/{chunk-4T7OBVTU.js → chunk-S6ZQKDY6.js} +1 -1
- package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
- package/dist/{chunk-OHIK3MIO.js → chunk-Z2FNRKF3.js} +13 -13
- package/dist/components.d.ts +5 -4
- package/dist/components.js +29 -34
- 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 +346 -0
- package/dist/eslint-rules/rules/05-styling.cjs +61 -0
- package/dist/eslint-rules/rules/{rbac.cjs → 06-security-rbac.cjs} +34 -13
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +385 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -0
- package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +8 -8
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -20
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +8 -8
- package/dist/theming/runtime.d.ts +61 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/{types-B-K_5VnO.d.ts → types-DXstZpNI.d.ts} +0 -17
- package/dist/types.d.ts +2 -2
- 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 +106 -41
- 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/dependencies.md +23 -0
- 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/app-layout.md +1 -1
- package/docs/implementation-guides/authentication.md +15 -15
- package/docs/implementation-guides/component-styling.md +1 -1
- package/docs/implementation-guides/data-tables.md +127 -34
- 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} +241 -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 +50 -20
- package/package.json +50 -19
- package/scripts/eslint-audit.cjs +123 -0
- package/scripts/install-cursor-rules.cjs +11 -243
- package/scripts/install-eslint-config.cjs +349 -0
- package/scripts/validate-dependencies.cjs +248 -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 +30 -18
- 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 +10 -9
- package/src/__tests__/templates/component.test.template.tsx +18 -15
- package/src/components/AddressField/AddressField.tsx +26 -1
- package/src/components/Alert/Alert.test.tsx +86 -22
- package/src/components/Alert/Alert.tsx +19 -11
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Calendar/Calendar.tsx +201 -47
- package/src/components/Checkbox/Checkbox.test.tsx +2 -1
- package/src/components/ContextSelector/ContextSelector.tsx +108 -126
- package/src/components/DataTable/AUDIT_REPORT.md +293 -0
- package/src/components/DataTable/DataTable.tsx +1 -19
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +21 -6
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
- 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 +11 -10
- 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 +45 -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 +1 -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 +45 -16
- package/src/components/FileUpload/FileUpload.tsx +141 -130
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
- 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__/useDataTablePerformance.unit.test.ts +8 -5
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +99 -99
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
- 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 +13 -9
- package/src/hooks/useAddressAutocomplete.test.ts +18 -18
- package/src/hooks/useAppConfig.ts +2 -2
- package/src/hooks/useEventTheme.test.ts +7 -7
- package/src/hooks/useEventTheme.ts +2 -1
- package/src/hooks/useFileDisplay.ts +2 -2
- package/src/hooks/useFileUrl.ts +52 -8
- package/src/hooks/useOrganisationSecurity.test.ts +2 -1
- 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 +38 -38
- 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/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
- package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
- package/src/rbac/api.test.ts +104 -0
- package/src/rbac/engine.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +2 -2
- package/src/rbac/secureClient.ts +1 -1
- package/src/rbac/types/functions.ts +1 -1
- package/src/styles/core.css +7 -0
- package/src/theming/__tests__/parseEventColours.test.ts +118 -3
- package/src/theming/parseEventColours.ts +77 -11
- package/src/types/supabase.ts +2 -3
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
- package/src/utils/formatting/formatDate.test.ts +3 -2
- package/src/utils/formatting/formatDateTime.test.ts +2 -2
- package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
- package/src/utils/storage/README.md +1 -1
- package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
- package/src/utils/storage/helpers.test.ts +69 -3
- 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,18 +116,22 @@ describe('[component] FileUpload', () => {
|
|
|
116
116
|
describe('Drag and Drop', () => {
|
|
117
117
|
it('shows drag state message on dragover', () => {
|
|
118
118
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
119
|
-
|
|
119
|
+
// The drop area is the CardHeader with role="button", not a div
|
|
120
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
120
121
|
|
|
121
122
|
fireEvent.dragOver(dropArea);
|
|
122
|
-
|
|
123
|
+
// Component shows "Drop files here..." with ellipsis
|
|
124
|
+
expect(screen.getByText(/Drop files here\.\.\./i)).toBeInTheDocument();
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
it('resets drag state on dragleave', () => {
|
|
126
128
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
127
|
-
|
|
129
|
+
// The drop area is the CardHeader with role="button", not a div
|
|
130
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
128
131
|
|
|
129
132
|
fireEvent.dragOver(dropArea);
|
|
130
|
-
|
|
133
|
+
// Component shows "Drop files here..." with ellipsis
|
|
134
|
+
expect(screen.getByText(/Drop files here\.\.\./i)).toBeInTheDocument();
|
|
131
135
|
|
|
132
136
|
fireEvent.dragLeave(dropArea);
|
|
133
137
|
expect(screen.getByText(/Click to upload/i)).toBeInTheDocument();
|
|
@@ -135,24 +139,45 @@ describe('[component] FileUpload', () => {
|
|
|
135
139
|
|
|
136
140
|
it('handles file drop', async () => {
|
|
137
141
|
const onUploadSuccess = vi.fn();
|
|
142
|
+
const mockUploadFile = vi.fn(async () => createMockUploadResult());
|
|
143
|
+
mockUseFileReference.mockReturnValue({
|
|
144
|
+
uploadFile: mockUploadFile,
|
|
145
|
+
isLoading: false,
|
|
146
|
+
error: null,
|
|
147
|
+
});
|
|
148
|
+
|
|
138
149
|
renderWithProviders(
|
|
139
150
|
<FileUpload {...baseProps} onUploadSuccess={onUploadSuccess} showProgress />
|
|
140
151
|
);
|
|
141
152
|
|
|
142
|
-
|
|
153
|
+
// Wait for app ID resolution to complete
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(screen.queryByText(/Resolving app configuration/i)).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
143
159
|
const file = createTestFile('test.png', 'image/png');
|
|
144
160
|
|
|
161
|
+
// Use fireEvent.drop with proper dataTransfer mock
|
|
145
162
|
await act(async () => {
|
|
146
163
|
fireEvent.drop(dropArea, {
|
|
147
164
|
dataTransfer: {
|
|
148
165
|
files: [file],
|
|
149
|
-
|
|
166
|
+
items: [{
|
|
167
|
+
kind: 'file',
|
|
168
|
+
type: file.type,
|
|
169
|
+
getAsFile: () => file
|
|
170
|
+
}],
|
|
171
|
+
types: ['Files']
|
|
172
|
+
}
|
|
150
173
|
});
|
|
151
174
|
});
|
|
152
175
|
|
|
176
|
+
// Wait for upload to complete - the uploadFile is called asynchronously
|
|
153
177
|
await waitFor(() => {
|
|
178
|
+
expect(mockUploadFile).toHaveBeenCalled();
|
|
154
179
|
expect(onUploadSuccess).toHaveBeenCalled();
|
|
155
|
-
});
|
|
180
|
+
}, { timeout: 5000 });
|
|
156
181
|
});
|
|
157
182
|
|
|
158
183
|
it('does not handle drop when disabled', () => {
|
|
@@ -204,7 +229,7 @@ describe('[component] FileUpload', () => {
|
|
|
204
229
|
|
|
205
230
|
it('triggers file input click when drop area is clicked', () => {
|
|
206
231
|
renderWithProviders(<FileUpload {...baseProps} />);
|
|
207
|
-
const dropArea = screen.
|
|
232
|
+
const dropArea = screen.getByRole('button', { name: /File upload area/i });
|
|
208
233
|
const input = screen.getByTestId('file-input') as HTMLInputElement;
|
|
209
234
|
const clickSpy = vi.spyOn(input, 'click');
|
|
210
235
|
|
|
@@ -398,12 +423,17 @@ describe('[component] FileUpload', () => {
|
|
|
398
423
|
fireEvent.change(input, { target: { files: [file] } });
|
|
399
424
|
});
|
|
400
425
|
|
|
426
|
+
// Wait for upload to complete and file card to be rendered with File icon
|
|
427
|
+
// The File icon is rendered as an SVG inside CardHeader (header) inside Card (article)
|
|
428
|
+
// Since showPreview is false and it's a PDF, the File icon should be rendered
|
|
429
|
+
// In tests, lucide-react icons are mocked with data-testid="lucide-{name}"
|
|
401
430
|
await waitFor(() => {
|
|
431
|
+
// First verify the file name is present
|
|
402
432
|
expect(screen.getByText('test.pdf')).toBeInTheDocument();
|
|
403
|
-
// File icon should be shown (
|
|
404
|
-
const fileIcon =
|
|
433
|
+
// File icon should be shown (lucide-react File icon)
|
|
434
|
+
const fileIcon = screen.getByTestId('lucide-file');
|
|
405
435
|
expect(fileIcon).toBeInTheDocument();
|
|
406
|
-
});
|
|
436
|
+
}, { timeout: 3000 });
|
|
407
437
|
});
|
|
408
438
|
|
|
409
439
|
it('shows loading spinner when showProgress is false', async () => {
|
|
@@ -429,10 +459,11 @@ describe('[component] FileUpload', () => {
|
|
|
429
459
|
});
|
|
430
460
|
|
|
431
461
|
// Wait for spinner to appear
|
|
462
|
+
// LoadingSpinner uses role="status" with "Loading..." text, not "Uploading file"
|
|
432
463
|
await waitFor(() => {
|
|
433
|
-
const
|
|
434
|
-
const spinner = spinnerContainer.querySelector('.animate-spin');
|
|
464
|
+
const spinner = screen.getByRole('status');
|
|
435
465
|
expect(spinner).toBeInTheDocument();
|
|
466
|
+
expect(spinner).toHaveClass('animate-spin');
|
|
436
467
|
});
|
|
437
468
|
|
|
438
469
|
// Resolve upload to clean up
|
|
@@ -592,6 +623,4 @@ describe('[component] FileUpload', () => {
|
|
|
592
623
|
});
|
|
593
624
|
});
|
|
594
625
|
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
|
|
626
|
+
});
|
|
@@ -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.
|
|
@@ -90,8 +94,19 @@ export function FileUpload({
|
|
|
90
94
|
const [isResolvingAppId, setIsResolvingAppId] = useState(!app_id);
|
|
91
95
|
const [appIdError, setAppIdError] = useState<string | null>(null);
|
|
92
96
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
97
|
+
const progressIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
|
93
98
|
const { uploadFile, isLoading, error } = useFileReference(supabase);
|
|
94
99
|
|
|
100
|
+
// Cleanup all progress intervals on unmount
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
progressIntervalsRef.current.forEach((interval) => {
|
|
104
|
+
clearInterval(interval);
|
|
105
|
+
});
|
|
106
|
+
progressIntervalsRef.current.clear();
|
|
107
|
+
};
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
95
110
|
// Resolve app_id from app name if not provided
|
|
96
111
|
useEffect(() => {
|
|
97
112
|
if (app_id) {
|
|
@@ -138,7 +153,7 @@ export function FileUpload({
|
|
|
138
153
|
|
|
139
154
|
// Calculate isUploading and isDisabled early so they can be used in callbacks
|
|
140
155
|
const isUploading = useMemo(() => {
|
|
141
|
-
return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
|
|
156
|
+
return uploadStates.size > 0 && Array.from(uploadStates.values()).some(state =>
|
|
142
157
|
state.progress.status === 'uploading' || state.progress.status === 'processing'
|
|
143
158
|
);
|
|
144
159
|
}, [uploadStates]);
|
|
@@ -154,7 +169,7 @@ export function FileUpload({
|
|
|
154
169
|
resolve(null);
|
|
155
170
|
return;
|
|
156
171
|
}
|
|
157
|
-
|
|
172
|
+
|
|
158
173
|
const reader = new FileReader();
|
|
159
174
|
reader.onload = (e) => {
|
|
160
175
|
resolve(e.target?.result as string || null);
|
|
@@ -203,7 +218,7 @@ export function FileUpload({
|
|
|
203
218
|
if (!files || files.length === 0) return;
|
|
204
219
|
|
|
205
220
|
const fileArray = Array.from(files);
|
|
206
|
-
|
|
221
|
+
|
|
207
222
|
// Validate all files first
|
|
208
223
|
const validationErrors: string[] = [];
|
|
209
224
|
const validFiles: File[] = [];
|
|
@@ -224,11 +239,11 @@ export function FileUpload({
|
|
|
224
239
|
|
|
225
240
|
// Initialize upload states
|
|
226
241
|
const newUploadStates = new Map<string, FileUploadState>();
|
|
227
|
-
|
|
242
|
+
|
|
228
243
|
for (const file of validFiles) {
|
|
229
244
|
const fileId = `${file.name}-${file.size}-${Date.now()}`;
|
|
230
245
|
const preview = showPreview ? (await generatePreview(file)) || undefined : undefined;
|
|
231
|
-
|
|
246
|
+
|
|
232
247
|
const progress: UploadProgress = {
|
|
233
248
|
loaded: 0,
|
|
234
249
|
total: file.size,
|
|
@@ -291,16 +306,25 @@ export function FileUpload({
|
|
|
291
306
|
return updated;
|
|
292
307
|
});
|
|
293
308
|
}, 200);
|
|
309
|
+
|
|
310
|
+
// Store interval in ref for cleanup
|
|
311
|
+
progressIntervalsRef.current.set(fileId, progressInterval);
|
|
294
312
|
|
|
295
313
|
|
|
296
314
|
// Use resolved app_id
|
|
297
315
|
if (!resolvedAppId) {
|
|
316
|
+
// Clear interval before throwing error
|
|
317
|
+
clearInterval(progressInterval);
|
|
318
|
+
progressIntervalsRef.current.delete(fileId);
|
|
298
319
|
const errorMsg = appIdError || 'App ID not available. Please provide app_id prop or set app name.';
|
|
299
320
|
throw new Error(errorMsg);
|
|
300
321
|
}
|
|
301
322
|
|
|
302
323
|
// Validate pageContext before upload
|
|
303
324
|
if (!pageContext) {
|
|
325
|
+
// Clear interval before throwing error
|
|
326
|
+
clearInterval(progressInterval);
|
|
327
|
+
progressIntervalsRef.current.delete(fileId);
|
|
304
328
|
const errorMsg = 'pageContext is required for file upload. This is used for permission checks.';
|
|
305
329
|
throw new Error(errorMsg);
|
|
306
330
|
}
|
|
@@ -318,7 +342,9 @@ export function FileUpload({
|
|
|
318
342
|
is_public: isPublic
|
|
319
343
|
}, file);
|
|
320
344
|
|
|
345
|
+
// Clear interval and remove from ref
|
|
321
346
|
clearInterval(progressInterval);
|
|
347
|
+
progressIntervalsRef.current.delete(fileId);
|
|
322
348
|
|
|
323
349
|
if (result) {
|
|
324
350
|
// Update status to completed
|
|
@@ -382,8 +408,15 @@ export function FileUpload({
|
|
|
382
408
|
onUploadError?.('Upload failed', file);
|
|
383
409
|
}
|
|
384
410
|
} catch (err) {
|
|
385
|
-
|
|
411
|
+
// Clear interval on error
|
|
412
|
+
const interval = progressIntervalsRef.current.get(fileId);
|
|
413
|
+
if (interval) {
|
|
414
|
+
clearInterval(interval);
|
|
415
|
+
progressIntervalsRef.current.delete(fileId);
|
|
416
|
+
}
|
|
386
417
|
|
|
418
|
+
const errorMessage = err instanceof Error ? err.message : 'Upload failed';
|
|
419
|
+
|
|
387
420
|
setUploadStates(prev => {
|
|
388
421
|
const updated = new Map(prev);
|
|
389
422
|
const state = updated.get(fileId);
|
|
@@ -432,9 +465,9 @@ export function FileUpload({
|
|
|
432
465
|
e.preventDefault();
|
|
433
466
|
e.stopPropagation();
|
|
434
467
|
setIsDragging(false);
|
|
435
|
-
|
|
468
|
+
|
|
436
469
|
if (isDisabled) return;
|
|
437
|
-
|
|
470
|
+
|
|
438
471
|
const files = e.dataTransfer.files;
|
|
439
472
|
handleFileSelect(files);
|
|
440
473
|
}, [isDisabled, handleFileSelect]);
|
|
@@ -465,8 +498,8 @@ export function FileUpload({
|
|
|
465
498
|
const disabledClasses = isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-sec-50';
|
|
466
499
|
|
|
467
500
|
return (
|
|
468
|
-
<
|
|
469
|
-
<
|
|
501
|
+
<Card className={className}>
|
|
502
|
+
<CardHeader
|
|
470
503
|
role="button"
|
|
471
504
|
tabIndex={isDisabled ? -1 : 0}
|
|
472
505
|
aria-label="File upload area"
|
|
@@ -484,7 +517,7 @@ export function FileUpload({
|
|
|
484
517
|
} : undefined}
|
|
485
518
|
>
|
|
486
519
|
{children || (
|
|
487
|
-
|
|
520
|
+
<>
|
|
488
521
|
<input
|
|
489
522
|
ref={fileInputRef}
|
|
490
523
|
type="file"
|
|
@@ -496,146 +529,124 @@ export function FileUpload({
|
|
|
496
529
|
data-testid="file-input"
|
|
497
530
|
aria-label={accept ? `Upload file${multiple ? 's' : ''} (${accept})` : `Upload file${multiple ? 's' : ''}`}
|
|
498
531
|
/>
|
|
499
|
-
<
|
|
532
|
+
<p className="text-sec-600">
|
|
500
533
|
{isResolvingAppId ? (
|
|
501
534
|
'Resolving app configuration...'
|
|
502
535
|
) : isDragging ? (
|
|
503
536
|
'Drop files here...'
|
|
504
537
|
) : (
|
|
505
538
|
<>
|
|
506
|
-
|
|
539
|
+
Click to upload
|
|
507
540
|
{' '}or drag and drop
|
|
508
541
|
</>
|
|
509
542
|
)}
|
|
510
|
-
</
|
|
511
|
-
<
|
|
543
|
+
</p>
|
|
544
|
+
<p className="text-sm text-sec-500">
|
|
512
545
|
{!isResolvingAppId && accept !== '*/*' && `Accepted formats: ${accept}`}
|
|
513
546
|
{!isResolvingAppId && maxSize && ` • Max size: ${Math.round(maxSize / 1024 / 1024)}MB`}
|
|
514
547
|
{!isResolvingAppId && multiple && ' • Multiple files allowed'}
|
|
515
|
-
</
|
|
516
|
-
|
|
548
|
+
</p>
|
|
549
|
+
</>
|
|
517
550
|
)}
|
|
518
|
-
|
|
551
|
+
|
|
519
552
|
{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>
|
|
553
|
+
<LoadingSpinner size="lg" className="text-main-500" />
|
|
528
554
|
)}
|
|
529
|
-
</
|
|
555
|
+
</CardHeader>
|
|
530
556
|
|
|
531
557
|
{/* Upload Progress List */}
|
|
532
558
|
{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}%` }}
|
|
559
|
+
<CardContent>
|
|
560
|
+
{Array.from(uploadStates.entries()).map(([fileId, uploadState]) => {
|
|
561
|
+
const { file, progress, preview, result } = uploadState;
|
|
562
|
+
const isError = progress.status === 'error';
|
|
563
|
+
const isCompleted = progress.status === 'completed';
|
|
564
|
+
const isUploading = progress.status === 'uploading' || progress.status === 'processing';
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<Card
|
|
568
|
+
key={fileId}
|
|
569
|
+
className={`grid grid-cols-[auto_1fr_auto] items-center gap-3 ${isError
|
|
570
|
+
? 'bg-acc-50 border-acc-200'
|
|
571
|
+
: isCompleted
|
|
572
|
+
? 'bg-success-50 border-success-200'
|
|
573
|
+
: 'bg-sec-50 border-sec-200'
|
|
574
|
+
}`}
|
|
575
|
+
>
|
|
576
|
+
<CardHeader className="p-0">
|
|
577
|
+
{preview ? (
|
|
578
|
+
<img
|
|
579
|
+
src={preview}
|
|
580
|
+
alt={file.name}
|
|
581
|
+
className="size-12 object-cover rounded"
|
|
582
|
+
/>
|
|
583
|
+
) : (
|
|
584
|
+
<File className="size-12 text-sec-600" />
|
|
585
|
+
)}
|
|
586
|
+
</CardHeader>
|
|
587
|
+
|
|
588
|
+
<CardContent className="p-0 min-w-0">
|
|
589
|
+
<CardTitle className="text-base truncate">
|
|
590
|
+
{file.name}
|
|
591
|
+
</CardTitle>
|
|
592
|
+
<CardDescription>
|
|
593
|
+
{formatFileSize(file.size)}
|
|
594
|
+
{isCompleted && result && ' • Uploaded'}
|
|
595
|
+
{isError && progress.error && ` • ${progress.error}`}
|
|
596
|
+
</CardDescription>
|
|
597
|
+
|
|
598
|
+
{/* Progress Bar */}
|
|
599
|
+
{showProgress && (isUploading || isError) && (
|
|
600
|
+
<>
|
|
601
|
+
<Progress
|
|
602
|
+
value={progress.percentage}
|
|
603
|
+
max={100}
|
|
604
|
+
style={{
|
|
605
|
+
accentColor: isError ? 'var(--color-acc-500)' : 'var(--color-main-500)'
|
|
606
|
+
}}
|
|
586
607
|
/>
|
|
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>
|
|
608
|
+
{isUploading && (
|
|
609
|
+
<p>
|
|
610
|
+
{progress.percentage}% • {formatFileSize(progress.loaded)} / {formatFileSize(progress.total)}
|
|
611
|
+
</p>
|
|
612
|
+
)}
|
|
613
|
+
</>
|
|
614
|
+
)}
|
|
615
|
+
</CardContent>
|
|
616
|
+
|
|
617
|
+
<CardFooter className="p-0">
|
|
618
|
+
{isCompleted && (
|
|
619
|
+
<Check className="text-success-500 size-5" />
|
|
620
|
+
)}
|
|
621
|
+
{isError && (
|
|
622
|
+
<X className="text-acc-500 size-5" />
|
|
623
|
+
)}
|
|
624
|
+
{isUploading && (
|
|
625
|
+
<LoadingSpinner size="sm" className="text-main-500" />
|
|
626
|
+
)}
|
|
627
|
+
</CardFooter>
|
|
628
|
+
</Card>
|
|
629
|
+
);
|
|
630
|
+
})}
|
|
631
|
+
|
|
632
|
+
</CardContent>
|
|
628
633
|
)}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
634
|
+
|
|
635
|
+
{(appIdError || error) && (
|
|
636
|
+
<CardFooter>
|
|
637
|
+
{appIdError && (
|
|
638
|
+
<p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
|
|
639
|
+
{appIdError}
|
|
640
|
+
</p>
|
|
641
|
+
)}
|
|
642
|
+
{error && (
|
|
643
|
+
<p className="grid place-items-center text-center size-full" role="alert" aria-live="assertive">
|
|
644
|
+
{error}
|
|
645
|
+
</p>
|
|
646
|
+
)}
|
|
647
|
+
</CardFooter>
|
|
637
648
|
)}
|
|
638
|
-
</
|
|
649
|
+
</Card>
|
|
639
650
|
);
|
|
640
651
|
}
|
|
641
652
|
|