openstack-uicore-foundation 5.0.17-beta.3 → 5.0.17
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/.claude/rules/openstack-uicore-foundation-components.md +62 -0
- package/.claude/rules/openstack-uicore-foundation-project.md +64 -0
- package/.claude/rules/openstack-uicore-foundation-security.md +39 -0
- package/.codegraph/config.json +140 -0
- package/docs/plans/2026-04-09-showconfirmdialog-react16-compat.md +83 -0
- package/docs/plans/2026-04-22-dropzone-pooling-ux.md +129 -0
- package/docs/plans/2026-04-23-uploadv3-duplicate-file-max-reached.md +109 -0
- package/docs/plans/2026-04-23-uploadv3-premature-complete-on-202.md +99 -0
- package/lib/components/index.js +1 -1
- package/lib/components/index.js.map +1 -1
- package/lib/components/mui/form-item-table.js +1 -1
- package/lib/components/mui/form-item-table.js.map +1 -1
- package/lib/components/mui/formik-inputs/additional-input-list.js +1 -1
- package/lib/components/mui/formik-inputs/additional-input-list.js.map +1 -1
- package/lib/components/mui/formik-inputs/additional-input.js +1 -1
- package/lib/components/mui/formik-inputs/additional-input.js.map +1 -1
- package/lib/components/mui/formik-inputs/select.js +1 -1
- package/lib/components/mui/formik-inputs/select.js.map +1 -1
- package/lib/components/mui/formik-inputs/upload.js +1 -1
- package/lib/components/mui/formik-inputs/upload.js.map +1 -1
- package/lib/components/mui/search-input.js +1 -1
- package/lib/components/mui/search-input.js.map +1 -1
- package/lib/components/mui/table/extra-rows.js +1 -1
- package/lib/components/mui/table/extra-rows.js.map +1 -1
- package/lib/i18n.js +1 -1
- package/lib/i18n.js.map +1 -1
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -13
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Component Patterns
|
|
2
|
+
|
|
3
|
+
## Upload Components (3 generations)
|
|
4
|
+
|
|
5
|
+
| Version | Location | Rendering | Use Case |
|
|
6
|
+
|---------|----------|-----------|----------|
|
|
7
|
+
| V1 | `inputs/upload-input/` | Legacy, basic file list | Simple single-file uploads |
|
|
8
|
+
| V2 | `inputs/upload-input-v2/` | Custom React UI | Multi-file with progress |
|
|
9
|
+
| V3 | `inputs/upload-input-v3/` | MUI-based UI | Chunked uploads, async processing |
|
|
10
|
+
|
|
11
|
+
**All versions wrap `inputs/dropzone/index.js`** (DropzoneJS class) or `dropzone-v3.js` (DropzoneV3 wrapper). The underlying Dropzone library is `dropzone@5.7.2`.
|
|
12
|
+
|
|
13
|
+
### Chunked Upload Architecture
|
|
14
|
+
|
|
15
|
+
- `DropzoneJS` manages a chunk queue with `maxConcurrentChunks` (default: 6)
|
|
16
|
+
- `setupChunkThrottle()` wraps Dropzone's `_uploadData` to enforce concurrency
|
|
17
|
+
- Progress tracking uses `_completedBytes` floor to prevent oscillation during parallel chunk uploads
|
|
18
|
+
- `options.uploadprogress` override in `getDjsConfig()` centralizes progress correction (guards against `NaN` bytesSent from queued chunks)
|
|
19
|
+
- HTTP 202 responses trigger async polling via `_asyncProcessing` flag
|
|
20
|
+
|
|
21
|
+
### Upload Gotchas
|
|
22
|
+
|
|
23
|
+
- **Progress oscillation:** Dropzone creates chunk entries with `progress:0` and `bytesSent:undefined` upfront. Raw progress average across chunks causes backsliding — always apply `_completedBytes` floor.
|
|
24
|
+
- **Cancel cleanup:** `xhr.abort()` fires `onabort`, not `onload`/`onerror`. Cancelled files must be filtered from `chunkQueue` and `chunksInFlight` decremented in `removedfile` handler.
|
|
25
|
+
- **NaN guard:** `Math.max(bytesSent || 0, ...)` — bytesSent can be `undefined` for queued chunks.
|
|
26
|
+
|
|
27
|
+
## MUI Components
|
|
28
|
+
|
|
29
|
+
- Located in `src/components/mui/`
|
|
30
|
+
- Follow pattern: component in `index.js`, styles in `.module.less` or `.module.scss`
|
|
31
|
+
- Formik integration components in `mui/formik-inputs/` — prefix `mui-formik-*`
|
|
32
|
+
- All MUI components use MUI v6 APIs
|
|
33
|
+
|
|
34
|
+
## Redux Pattern
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// Action creator factory
|
|
38
|
+
export const createAction = type => payload => ({ type, payload });
|
|
39
|
+
|
|
40
|
+
// Async thunk pattern
|
|
41
|
+
export const myAction = (params) => async (dispatch, getState) => {
|
|
42
|
+
dispatch(startLoading());
|
|
43
|
+
return getRequest(
|
|
44
|
+
createAction(REQUEST_TYPE),
|
|
45
|
+
createAction(RECEIVE_TYPE),
|
|
46
|
+
buildAPIBaseUrl('/api/v1/endpoint'),
|
|
47
|
+
authErrorHandler
|
|
48
|
+
)(params)(dispatch, getState);
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Actions: constants + thunks in same file
|
|
53
|
+
- Reducers: switch/case on action types
|
|
54
|
+
- State shape: `loggedUserState` for auth, domain-specific reducers in consumers
|
|
55
|
+
|
|
56
|
+
## Testing Conventions
|
|
57
|
+
|
|
58
|
+
- Test files: `__tests__/` directories adjacent to components
|
|
59
|
+
- Framework: Jest + @testing-library/react
|
|
60
|
+
- Mock pattern: `jest.mock('module')` at top of file
|
|
61
|
+
- Component tests render with `@testing-library/react`, assert on DOM
|
|
62
|
+
- Dropzone tests mock the Dropzone constructor to capture options and simulate events
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Project: openstack-uicore-foundation
|
|
2
|
+
|
|
3
|
+
**Last Updated:** 2026-04-23
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Shared React component library for OpenStack marketing sites and summit management apps. Published as an npm package — consumers import from `lib/`.
|
|
8
|
+
|
|
9
|
+
## Technology Stack
|
|
10
|
+
|
|
11
|
+
- **Language:** JavaScript (ES6+, Babel-transpiled)
|
|
12
|
+
- **UI:** React 17, MUI 6, Formik, React-Redux 5, Redux 3
|
|
13
|
+
- **HTTP:** superagent
|
|
14
|
+
- **Auth:** OAuth2 PKCE + OIDC (idtoken-verifier, browser-tabs-lock)
|
|
15
|
+
- **Build:** Webpack 5, Babel 7 (babel.config.json)
|
|
16
|
+
- **Test:** Jest 28, @testing-library/react 12, jsdom
|
|
17
|
+
- **Styles:** LESS, SCSS, CSS Modules
|
|
18
|
+
- **Node:** 18.15.0 (.nvmrc)
|
|
19
|
+
- **Indentation:** 4 spaces (all file types — .editorconfig)
|
|
20
|
+
|
|
21
|
+
## Directory Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
├── components/
|
|
26
|
+
│ ├── inputs/ # Form inputs (dropzone, upload V1/V2/V3, text, dropdown, etc.)
|
|
27
|
+
│ ├── mui/ # MUI-based components (formik-inputs/, tables, dialogs, etc.)
|
|
28
|
+
│ ├── security/ # OAuth/PKCE auth, session management, reducers
|
|
29
|
+
│ ├── forms/ # SimpleForm, RsvpForm
|
|
30
|
+
│ ├── sections/ # Panel
|
|
31
|
+
│ ├── index.js # Barrel re-exports for all components
|
|
32
|
+
│ └── ... # Individual components (clock, video-stream, etc.)
|
|
33
|
+
├── models/ # Data models (summit-event)
|
|
34
|
+
├── utils/ # Actions, reducers, methods, constants, crypto, money
|
|
35
|
+
└── i18n/ # en.json, es.json, zh.json
|
|
36
|
+
lib/ # Built output (gitignored, npm-published)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Key Files
|
|
40
|
+
|
|
41
|
+
- **Entry/Exports:** `src/components/index.js` — barrel file with all public exports
|
|
42
|
+
- **Webpack entries:** `webpack.common.js` — individual entry points per component (no single bundle)
|
|
43
|
+
- **Auth core:** `src/components/security/methods.js` — OAuth PKCE, token refresh, session management
|
|
44
|
+
- **API utils:** `src/utils/actions.js` — HTTP request helpers, error handlers, Redux action creators
|
|
45
|
+
- **Constants:** `src/utils/constants.js` — shared numeric/string constants
|
|
46
|
+
|
|
47
|
+
## Development Commands
|
|
48
|
+
|
|
49
|
+
| Task | Command |
|
|
50
|
+
|------|---------|
|
|
51
|
+
| Install | `yarn install` |
|
|
52
|
+
| Build (dev) | `yarn build-dev` |
|
|
53
|
+
| Build (prod) | `yarn build` |
|
|
54
|
+
| Test | `jest` (or `npx jest`) |
|
|
55
|
+
| Clean rebuild | `yarn clean` |
|
|
56
|
+
|
|
57
|
+
## Architecture Notes
|
|
58
|
+
|
|
59
|
+
- **Multi-entry Webpack build:** Each component is a separate webpack entry point — consumers can import individual components without pulling the full bundle.
|
|
60
|
+
- **Peer dependencies model:** React, MUI, Redux, and most libs are peer deps — consumers provide them.
|
|
61
|
+
- **Redux pattern:** `createAction(TYPE)` factory → thunk actions → `getRequest`/`putRequest`/`postRequest`/`deleteRequest` helpers in `utils/actions.js`. Error handling via `authErrorHandler` with auto-redirect on 401.
|
|
62
|
+
- **i18n:** `i18n-react` with JSON locale files. Language detected from browser, fallback to English.
|
|
63
|
+
- **No barrel imports from `lib/`:** Components are imported directly from their paths (e.g., `openstack-uicore-foundation/lib/components/inputs/upload-input-v3`).
|
|
64
|
+
- **Commented-out exports** in `src/components/index.js` are components with heavy 3rd-party deps (Stripe, react-beautiful-dnd) — imported separately by consumers.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Security & Auth Conventions
|
|
2
|
+
|
|
3
|
+
## OAuth2 PKCE Flow
|
|
4
|
+
|
|
5
|
+
Auth lives in `src/components/security/`. The library implements OAuth2 Authorization Code + PKCE against an OpenStack IDP.
|
|
6
|
+
|
|
7
|
+
### Key Modules
|
|
8
|
+
|
|
9
|
+
| File | Purpose |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| `methods.js` | Core auth: `doLogin`, `initLogOut`, `getAccessToken`, `storeAuthInfo`, token refresh with mutex (SuperTokensLock) |
|
|
12
|
+
| `actions.js` | Redux actions: `onUserAuth`, `getUserInfo`, `doLogout` |
|
|
13
|
+
| `reducers.js` | Auth state: `loggedUserState` with `member`, `sessionState` |
|
|
14
|
+
| `constants.js` | Error code constants (`AUTH_ERROR_*`) |
|
|
15
|
+
| `abstract-auth-callback-route.js` | Route component for OAuth callback handling (v1) |
|
|
16
|
+
| `abstract-auth-callback-route-v2.js` | OAuth callback with PKCE verifier exchange (v2) |
|
|
17
|
+
| `session-checker/op-session-checker.js` | Session iframe checker for silent re-auth |
|
|
18
|
+
|
|
19
|
+
### Token Management
|
|
20
|
+
|
|
21
|
+
- Access tokens stored via `storeAuthInfo()` in localStorage
|
|
22
|
+
- Refresh uses `SuperTokensLock` (browser-tabs-lock) for cross-tab mutex
|
|
23
|
+
- ID token validated with `idtoken-verifier`
|
|
24
|
+
- PKCE: SHA256 code challenge via `src/utils/crypto.js` (`getSHA256`, `getRandomBytes`)
|
|
25
|
+
|
|
26
|
+
### Auth Error Handling
|
|
27
|
+
|
|
28
|
+
`authErrorHandler` in `utils/actions.js` handles HTTP errors:
|
|
29
|
+
- **401:** Triggers `initLogin()` → redirects to IDP (with session-clearing guard to prevent loops)
|
|
30
|
+
- **403:** Shows "not authorized" message
|
|
31
|
+
- **412:** Shows server-provided validation errors
|
|
32
|
+
- **Other:** Generic error with optional `res.body.message`
|
|
33
|
+
|
|
34
|
+
### Conventions
|
|
35
|
+
|
|
36
|
+
- Auth methods imported from `security/methods` — never call IDP directly
|
|
37
|
+
- Access token passed as query param `access_token` (not Bearer header) in `getRequest`/`putRequest` helpers
|
|
38
|
+
- `getAllowedUserGroups()` reads from env/config to restrict access by group membership
|
|
39
|
+
- Consumer apps must provide `loggedUserState` in their Redux store
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"include": [
|
|
4
|
+
"**/*.ts",
|
|
5
|
+
"**/*.tsx",
|
|
6
|
+
"**/*.js",
|
|
7
|
+
"**/*.jsx",
|
|
8
|
+
"**/*.py",
|
|
9
|
+
"**/*.go",
|
|
10
|
+
"**/*.rs",
|
|
11
|
+
"**/*.java",
|
|
12
|
+
"**/*.c",
|
|
13
|
+
"**/*.h",
|
|
14
|
+
"**/*.cpp",
|
|
15
|
+
"**/*.hpp",
|
|
16
|
+
"**/*.cc",
|
|
17
|
+
"**/*.cxx",
|
|
18
|
+
"**/*.cs",
|
|
19
|
+
"**/*.php",
|
|
20
|
+
"**/*.rb",
|
|
21
|
+
"**/*.swift",
|
|
22
|
+
"**/*.kt",
|
|
23
|
+
"**/*.kts",
|
|
24
|
+
"**/*.dart",
|
|
25
|
+
"**/*.svelte",
|
|
26
|
+
"**/*.liquid",
|
|
27
|
+
"**/*.pas",
|
|
28
|
+
"**/*.dpr",
|
|
29
|
+
"**/*.dpk",
|
|
30
|
+
"**/*.lpr",
|
|
31
|
+
"**/*.dfm",
|
|
32
|
+
"**/*.fmx"
|
|
33
|
+
],
|
|
34
|
+
"exclude": [
|
|
35
|
+
"**/.git/**",
|
|
36
|
+
"**/node_modules/**",
|
|
37
|
+
"**/vendor/**",
|
|
38
|
+
"**/Pods/**",
|
|
39
|
+
"**/dist/**",
|
|
40
|
+
"**/build/**",
|
|
41
|
+
"**/out/**",
|
|
42
|
+
"**/bin/**",
|
|
43
|
+
"**/obj/**",
|
|
44
|
+
"**/target/**",
|
|
45
|
+
"**/*.min.js",
|
|
46
|
+
"**/*.bundle.js",
|
|
47
|
+
"**/.next/**",
|
|
48
|
+
"**/.nuxt/**",
|
|
49
|
+
"**/.svelte-kit/**",
|
|
50
|
+
"**/.output/**",
|
|
51
|
+
"**/.turbo/**",
|
|
52
|
+
"**/.cache/**",
|
|
53
|
+
"**/.parcel-cache/**",
|
|
54
|
+
"**/.vite/**",
|
|
55
|
+
"**/.astro/**",
|
|
56
|
+
"**/.docusaurus/**",
|
|
57
|
+
"**/.gatsby/**",
|
|
58
|
+
"**/.webpack/**",
|
|
59
|
+
"**/.nx/**",
|
|
60
|
+
"**/.yarn/cache/**",
|
|
61
|
+
"**/.pnpm-store/**",
|
|
62
|
+
"**/storybook-static/**",
|
|
63
|
+
"**/.expo/**",
|
|
64
|
+
"**/web-build/**",
|
|
65
|
+
"**/ios/Pods/**",
|
|
66
|
+
"**/ios/build/**",
|
|
67
|
+
"**/android/build/**",
|
|
68
|
+
"**/android/.gradle/**",
|
|
69
|
+
"**/__pycache__/**",
|
|
70
|
+
"**/.venv/**",
|
|
71
|
+
"**/venv/**",
|
|
72
|
+
"**/site-packages/**",
|
|
73
|
+
"**/dist-packages/**",
|
|
74
|
+
"**/.pytest_cache/**",
|
|
75
|
+
"**/.mypy_cache/**",
|
|
76
|
+
"**/.ruff_cache/**",
|
|
77
|
+
"**/.tox/**",
|
|
78
|
+
"**/.nox/**",
|
|
79
|
+
"**/*.egg-info/**",
|
|
80
|
+
"**/.eggs/**",
|
|
81
|
+
"**/go/pkg/mod/**",
|
|
82
|
+
"**/target/debug/**",
|
|
83
|
+
"**/target/release/**",
|
|
84
|
+
"**/.gradle/**",
|
|
85
|
+
"**/.m2/**",
|
|
86
|
+
"**/generated-sources/**",
|
|
87
|
+
"**/.kotlin/**",
|
|
88
|
+
"**/.dart_tool/**",
|
|
89
|
+
"**/.vs/**",
|
|
90
|
+
"**/.nuget/**",
|
|
91
|
+
"**/artifacts/**",
|
|
92
|
+
"**/publish/**",
|
|
93
|
+
"**/cmake-build-*/**",
|
|
94
|
+
"**/CMakeFiles/**",
|
|
95
|
+
"**/bazel-*/**",
|
|
96
|
+
"**/vcpkg_installed/**",
|
|
97
|
+
"**/.conan/**",
|
|
98
|
+
"**/Debug/**",
|
|
99
|
+
"**/Release/**",
|
|
100
|
+
"**/x64/**",
|
|
101
|
+
"**/release/**",
|
|
102
|
+
"**/*.app/**",
|
|
103
|
+
"**/*.asar",
|
|
104
|
+
"**/DerivedData/**",
|
|
105
|
+
"**/.build/**",
|
|
106
|
+
"**/.swiftpm/**",
|
|
107
|
+
"**/xcuserdata/**",
|
|
108
|
+
"**/Carthage/Build/**",
|
|
109
|
+
"**/SourcePackages/**",
|
|
110
|
+
"**/__history/**",
|
|
111
|
+
"**/__recovery/**",
|
|
112
|
+
"**/*.dcu",
|
|
113
|
+
"**/.composer/**",
|
|
114
|
+
"**/storage/framework/**",
|
|
115
|
+
"**/bootstrap/cache/**",
|
|
116
|
+
"**/.bundle/**",
|
|
117
|
+
"**/tmp/cache/**",
|
|
118
|
+
"**/public/assets/**",
|
|
119
|
+
"**/public/packs/**",
|
|
120
|
+
"**/.yardoc/**",
|
|
121
|
+
"**/coverage/**",
|
|
122
|
+
"**/htmlcov/**",
|
|
123
|
+
"**/.nyc_output/**",
|
|
124
|
+
"**/test-results/**",
|
|
125
|
+
"**/.coverage/**",
|
|
126
|
+
"**/.idea/**",
|
|
127
|
+
"**/logs/**",
|
|
128
|
+
"**/tmp/**",
|
|
129
|
+
"**/temp/**",
|
|
130
|
+
"**/_build/**",
|
|
131
|
+
"**/docs/_build/**",
|
|
132
|
+
"**/site/**"
|
|
133
|
+
],
|
|
134
|
+
"languages": [],
|
|
135
|
+
"frameworks": [],
|
|
136
|
+
"maxFileSize": 1048576,
|
|
137
|
+
"extractDocstrings": true,
|
|
138
|
+
"trackCallSites": true,
|
|
139
|
+
"enableEmbeddings": true
|
|
140
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# showConfirmDialog React 16 Compatibility Fix Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-04-09
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Status: VERIFIED
|
|
6
|
+
Approved: Yes
|
|
7
|
+
Iterations: 0
|
|
8
|
+
Worktree: No
|
|
9
|
+
Type: Bugfix
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
**Symptom:** `showConfirmDialog` causes "Module not found: Error: Can't resolve 'react-dom/client'" build error on React 16 projects, despite a `/* webpackIgnore: true */` dynamic import guard.
|
|
14
|
+
|
|
15
|
+
**Trigger:** Any consuming project on React 16 that bundles `openstack-uicore-foundation` hits the error during webpack build.
|
|
16
|
+
|
|
17
|
+
**Root Cause:** `src/components/mui/showConfirmDialog.js:27` — The `import(/* webpackIgnore: true */ "react-dom/client")` compiles into the UMD output as a bare `import("react-dom/client")` (the magic comment is stripped during this library's build). When a consuming project's webpack processes the compiled bundle, it encounters the bare dynamic import, tries to resolve `react-dom/client`, and fails because React 16 has no such module path.
|
|
18
|
+
|
|
19
|
+
## Investigation
|
|
20
|
+
|
|
21
|
+
- **Evolution:** Original code (cd39cf3) used only `ReactDOM.render()`. PR #214 (094c6b9) added `require("react-dom/client")` in try/catch for React 18+ support — caused the same build error. Commit 6c95b33 replaced `require()` with `import(/* webpackIgnore: true */ ...)` — the magic comment works for THIS library's webpack but is stripped from the compiled UMD output.
|
|
22
|
+
- **Confirmed in built output:** `lib/components/mui/show-confirm-dialog.js` contains literal `await import("react-dom/client")` with no webpack annotation — any consuming webpack resolves it at build time.
|
|
23
|
+
- **Key insight:** The `webpackIgnore` comment is a build-time directive that only affects the webpack instance that processes the source file. It does not survive into the output and cannot protect consuming projects.
|
|
24
|
+
- **Internal callers** (5 files: mui-table, mui-table-sortable, mui-table-editable, meta-field-values, additional-input-list) all use `showConfirmDialog` as a plain imperative function — `const isConfirmed = await showConfirmDialog({...})`.
|
|
25
|
+
|
|
26
|
+
## Fix Approach
|
|
27
|
+
|
|
28
|
+
**Chosen:** Bridge Pattern + Context Provider
|
|
29
|
+
|
|
30
|
+
**Why:** Completely eliminates all references to `react-dom/client` from the source and compiled output. The dialog renders inside the existing React tree (managed by whatever React version the app uses), so no version-specific rendering API is needed. Preserves the imperative `showConfirmDialog()` API unchanged.
|
|
31
|
+
|
|
32
|
+
**Alternatives considered:**
|
|
33
|
+
- *Configuration injection* (`configureConfirmDialog({ createRoot })`) — simpler but less ergonomic; consumer must handle version detection and import themselves.
|
|
34
|
+
- *Remove createRoot entirely* (pure `ReactDOM.render`) — works on 16/17/18 but breaks React 19 where `ReactDOM.render` was removed.
|
|
35
|
+
|
|
36
|
+
**How it works:**
|
|
37
|
+
1. A new `ConfirmDialogProvider` component manages dialog state (open/closed, options, resolve callback) and renders `ConfirmDialog` within the existing React tree.
|
|
38
|
+
2. On mount, the provider registers a "bridge" callback in a module-level variable inside `showConfirmDialog.js`.
|
|
39
|
+
3. When `showConfirmDialog()` is called, it delegates to the bridge if registered. If no provider is mounted, it falls back to `ReactDOM.render()` with a console warning (backward compat for React 16/17/18 consumers who haven't added the provider yet).
|
|
40
|
+
4. Zero references to `react-dom/client` remain in the codebase.
|
|
41
|
+
|
|
42
|
+
**Files:**
|
|
43
|
+
- `src/components/mui/showConfirmDialog.js` — Add bridge registration, fallback logic
|
|
44
|
+
- `src/components/mui/ConfirmDialogProvider.js` — NEW: Provider component
|
|
45
|
+
- `src/components/index.js` — Export the new provider
|
|
46
|
+
- `webpack.common.js` — Add entry point for the new provider
|
|
47
|
+
|
|
48
|
+
**Tests:**
|
|
49
|
+
- `src/components/mui/__tests__/show-confirm-dialog.test.js` — Update for bridge-based flow
|
|
50
|
+
- `src/components/mui/__tests__/confirm-dialog-provider.test.js` — NEW: Provider tests
|
|
51
|
+
|
|
52
|
+
**Defense-in-depth:** Not applicable — this is a build-time resolution error, not a data flow issue.
|
|
53
|
+
|
|
54
|
+
## Progress
|
|
55
|
+
|
|
56
|
+
- [x] Task 1: Implement bridge pattern + provider
|
|
57
|
+
- [x] Task 2: Verify
|
|
58
|
+
**Tasks:** 2 | **Done:** 2
|
|
59
|
+
|
|
60
|
+
## Tasks
|
|
61
|
+
|
|
62
|
+
### Task 1: Implement bridge pattern + provider
|
|
63
|
+
|
|
64
|
+
**Objective:** Replace the `react-dom/client` dynamic import with a bridge pattern. Create `ConfirmDialogProvider`, modify `showConfirmDialog` to use bridge with ReactDOM.render fallback, export the new provider.
|
|
65
|
+
|
|
66
|
+
**Files:**
|
|
67
|
+
- `src/components/mui/showConfirmDialog.js` — Remove `getCreateRoot()`, add bridge registration exports (`_registerBridge`, `_unregisterBridge`). When bridge is registered, delegate. When not, fall back to `ReactDOM.render()` with `console.warn` suggesting provider migration.
|
|
68
|
+
- `src/components/mui/ConfirmDialogProvider.js` — NEW: Functional component using `useState` + `useEffect`. On mount, registers bridge via `_registerBridge`. Bridge callback sets dialog state and returns a Promise. Renders `<ConfirmDialog>` when dialog state is active. On unmount, calls `_unregisterBridge`.
|
|
69
|
+
- `src/components/index.js` — Add export: `export { default as ConfirmDialogProvider } from './mui/ConfirmDialogProvider'`
|
|
70
|
+
- `webpack.common.js` — Add entry: `'components/mui/confirm-dialog-provider': './src/components/mui/ConfirmDialogProvider.js'`
|
|
71
|
+
|
|
72
|
+
**TDD:**
|
|
73
|
+
1. Write regression test: import `showConfirmDialog` without mocking `react-dom/client` — verify no reference to `react-dom/client` exists in the module
|
|
74
|
+
2. Write provider test: render `ConfirmDialogProvider`, call `showConfirmDialog`, verify dialog appears and resolves correctly
|
|
75
|
+
3. Write fallback test: call `showConfirmDialog` without provider mounted — verify `ReactDOM.render` is called with console warning
|
|
76
|
+
4. Verify all existing tests still pass (callers mock `showConfirmDialog` so they are unaffected)
|
|
77
|
+
|
|
78
|
+
**Verify:** `npx jest --no-cache`
|
|
79
|
+
|
|
80
|
+
### Task 2: Verify
|
|
81
|
+
|
|
82
|
+
**Objective:** Full test suite passes, no regressions across all components that use `showConfirmDialog`.
|
|
83
|
+
**Verify:** `npx jest --no-cache && grep -r "react-dom/client" src/ && echo "FAIL: react-dom/client still referenced" || echo "PASS: no react-dom/client references"`
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Dropzone Polling UX Fix Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-04-22
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Status: VERIFIED
|
|
6
|
+
Approved: Yes
|
|
7
|
+
Iterations: 2
|
|
8
|
+
Worktree: No
|
|
9
|
+
Type: Bugfix
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
**Symptom:** After the last chunk is uploaded and the server returns HTTP 202 (async processing), both UploadInputV2 and UploadInputV3 immediately show the file as "Complete" / "success" — even though server-side processing hasn't finished and polling has just started.
|
|
14
|
+
**Trigger:** Upload a file where the server returns HTTP 202 on the last chunk (async processing path). The file appears fully uploaded while `pollUploadStatus` is still polling in the background.
|
|
15
|
+
**Root Cause:** `src/components/inputs/dropzone/index.js:313` — In the `sending` handler's custom `xhr.onload`, `dropzoneOnLoad(e)` is called unconditionally before checking the HTTP status code. For the 202 path, Dropzone.js processes the response through `_finishedUploading` → `finishedChunkUpload` → `chunksUploaded(file, done)` → `done()` → `_finished` → emits `success` event. This makes both V2 (Dropzone's default UI) and V3 (DropzoneV3 → UploadInputV3) mark the file as complete prematurely.
|
|
16
|
+
|
|
17
|
+
## Investigation
|
|
18
|
+
|
|
19
|
+
- Dropzone.js v5.7.2 has a `chunksUploaded(file, done)` option (line 563 in dist/dropzone.js). It's called when all chunks have been uploaded successfully. The default implementation calls `done()` immediately, which triggers `_finished` → `success` event.
|
|
20
|
+
- The `success` event sets `file.status = Dropzone.SUCCESS` and updates the file preview in V2's default template. In V3, DropzoneV3 maps `success` → `onFileCompleted` → UploadInputV3 sets `{complete: true}` → shows "Complete" with green check.
|
|
21
|
+
- The polling code (`pollUploadStatus`) was added in commit `6bad1f7` but didn't account for the fact that Dropzone.js fires `success` on any 2xx response for the final chunk.
|
|
22
|
+
- The `sending` event fires for EVERY chunk. The `xhr.onload` override is per-chunk. For intermediate chunks (200 without `name` field), Dropzone tracks chunk completion. Only when the last chunk response arrives does `finishedChunkUpload` trigger `chunksUploaded`.
|
|
23
|
+
|
|
24
|
+
## Behavior Contract
|
|
25
|
+
|
|
26
|
+
**Given:** A file is being uploaded in chunks and the server returns HTTP 202 on the last chunk (indicating async server-side processing is needed)
|
|
27
|
+
**When:** The last chunk's XHR response is received and processed
|
|
28
|
+
**Currently (bug):** Dropzone.js immediately fires the `success` event, causing both V2 and V3 UIs to show the file as "Complete" / "success" while polling is still running
|
|
29
|
+
**Expected (fix):** The `success` event should NOT fire until polling confirms server-side processing is complete (`data.status === 'complete'`). During the polling phase, the file should remain in its uploading/in-progress visual state.
|
|
30
|
+
**Anti-regression:** HTTP 200 uploads (synchronous path where the response contains `name` field) must continue to fire `success` immediately as before. Component unmount cleanup must still clear polling intervals. Error handling during polling must still work.
|
|
31
|
+
|
|
32
|
+
## Fix Approach
|
|
33
|
+
|
|
34
|
+
**Chosen:** Defer `chunksUploaded` done callback for 202 responses
|
|
35
|
+
**Why:** Uses Dropzone.js's public `chunksUploaded` option to control when `_finished` (and thus `success`) fires. Single-file change in `dropzone/index.js` that fixes the UX for both V2 and V3 consumers. No internal API hacking.
|
|
36
|
+
|
|
37
|
+
**Alternatives considered:**
|
|
38
|
+
- *Suppress success via internal `_callbacks` manipulation:* Would require accessing Dropzone's private `_callbacks` object — fragile and version-dependent. Rejected.
|
|
39
|
+
- *Add "Processing" state to V3 with new callback:* Better UX (shows "Processing..." text with indeterminate progress) but requires changes across 3 files. Can be done as a follow-up enhancement.
|
|
40
|
+
- *Skip `dropzoneOnLoad` for 202:* Would break Dropzone's internal state (chunk tracking, file counters, queue advancement). Rejected.
|
|
41
|
+
|
|
42
|
+
**Files:** `src/components/inputs/dropzone/index.js`
|
|
43
|
+
**Strategy:**
|
|
44
|
+
1. In `getDjsConfig()`: Override `chunksUploaded` option with a conditional callback that checks `file._asyncProcessing`. If the flag is set, store the `done` callback on the file object instead of calling it — this prevents `_finished` and the `success` event.
|
|
45
|
+
2. In `sending` handler's `xhr.onload`: Set `file._asyncProcessing = true` BEFORE calling `dropzoneOnLoad(e)` when `xhr.status == 202`. This ensures the `chunksUploaded` callback sees the flag.
|
|
46
|
+
3. In `pollUploadStatus`: Accept the `file` parameter. When polling returns `status: 'complete'`, call the stored `done` callback (`file._chunksUploadedDone()`) to complete the Dropzone lifecycle — this fires `success`, updating both V2 and V3 UIs.
|
|
47
|
+
|
|
48
|
+
**Tests:** `src/components/inputs/dropzone/__tests__/dropzone.test.js` (new file)
|
|
49
|
+
|
|
50
|
+
## Verification Scenario
|
|
51
|
+
|
|
52
|
+
### TS-001: File Upload with 202 Async Processing
|
|
53
|
+
**Preconditions:** App is running, upload endpoint configured to return 202 for async processing
|
|
54
|
+
|
|
55
|
+
| Step | Action | Expected Result (after fix) |
|
|
56
|
+
|------|--------|-----------------------------|
|
|
57
|
+
| 1 | Upload a file that triggers 202 response | File stays in uploading/progress state — NOT shown as "Complete" — while server processes |
|
|
58
|
+
| 2 | Wait for polling to complete (server returns `status: 'complete'`) | File transitions to "Complete" / success state only now |
|
|
59
|
+
| 3 | Upload a file that triggers 200 response (sync) | File immediately shows "Complete" / success (no regression) |
|
|
60
|
+
|
|
61
|
+
## Progress
|
|
62
|
+
|
|
63
|
+
- [x] Task 1: Write Reproducing Test (RED)
|
|
64
|
+
- [x] Task 2: Implement Fix at Root Cause
|
|
65
|
+
- [x] Task 3: Quality Gate
|
|
66
|
+
**Tasks:** 3 | **Done:** 3
|
|
67
|
+
|
|
68
|
+
**Note:** Build fails with pre-existing PostCSS/SCSS error in schedule-print/styles.module.scss (unrelated to dropzone fix). All 97 tests pass.
|
|
69
|
+
|
|
70
|
+
**Iteration 1 Fix:** Added polling guard (`file._pollingActive`) to prevent multiple intervals when server returns 202 for every chunk (not just the final one). Without this, each 202 chunk spawned a separate polling loop, causing request flood.
|
|
71
|
+
|
|
72
|
+
**Iteration 2 Fix:** Fixed request flood (216 HTTP 200 requests for single file) and resource leak on upload cancellation:
|
|
73
|
+
1. **Root cause**: `file._asyncProcessing = true` was being set for EVERY 202 response. Server behavior: intermediate chunks return HTTP 200, final chunk returns HTTP 202 with `file_id`. The original Iteration 1 code set the flag at line 340 (before the `else if(xhr?.status == 202)` check), causing the flag to be set even for 200 responses, which deferred `chunksUploaded` for all uploads.
|
|
74
|
+
2. **Fix**: Move `file._asyncProcessing = true` inside the `else if(xhr?.status == 202)` branch AND guard it with `if (fileId)` check. Now the flag is only set when we receive the final 202 chunk with `file_id`.
|
|
75
|
+
3. **XHR tracking**: Added `this.activeXHRs` Map to track all active XHR requests per file.
|
|
76
|
+
4. **Cancellation support**: Cancel pending XHRs when file is removed or component unmounts, preventing resource waste.
|
|
77
|
+
5. **Chunk throttling**: Added `maxConcurrentChunks` prop (default: 3) to DropzoneJS, V2, and V3. Wraps Dropzone's `_uploadData` with a concurrency-limited queue. When `parallelChunkUploads: true`, only N chunks upload concurrently instead of all at once. Queue drains as chunks complete via `onChunkComplete()` callback in xhr.onload/onerror.
|
|
78
|
+
|
|
79
|
+
## Tasks
|
|
80
|
+
|
|
81
|
+
### Task 1: Write Reproducing Test (RED)
|
|
82
|
+
|
|
83
|
+
**Objective:** Encode the Behavior Contract as a failing test BEFORE writing any fix code. Create test file for DropzoneJS component.
|
|
84
|
+
**Files:** `src/components/inputs/dropzone/__tests__/dropzone.test.js` (new)
|
|
85
|
+
**Entry point:** `DropzoneJS` component — specifically the `chunksUploaded` behavior when HTTP 202 is returned on the last chunk.
|
|
86
|
+
**Test cases:**
|
|
87
|
+
1. `test_dropzone_202_response_should_not_fire_success_immediately` — Simulate a chunked upload where the last chunk returns 202. Verify that the Dropzone `success` event does NOT fire immediately (the `onFileCompleted` callback in the consumer should not be triggered).
|
|
88
|
+
2. `test_dropzone_202_polling_complete_fires_success` — After 202 triggers polling and polling returns `status: 'complete'`, verify that the `success` event fires and `onUploadComplete` is called.
|
|
89
|
+
3. `test_dropzone_200_response_fires_success_immediately` — (Anti-regression) Simulate a synchronous upload where the last chunk returns 200 with `name` field. Verify `success` fires immediately as before.
|
|
90
|
+
**DoD:** Tests exist, run, and FAIL because the current code fires `success` immediately for 202.
|
|
91
|
+
**Verify:** `npx jest src/components/inputs/dropzone/__tests__/dropzone.test.js`
|
|
92
|
+
|
|
93
|
+
### Task 2: Implement Fix at Root Cause
|
|
94
|
+
|
|
95
|
+
**Objective:** Minimal change in `dropzone/index.js` that defers the `success` event for 202 responses until polling confirms completion.
|
|
96
|
+
**Files:** `src/components/inputs/dropzone/index.js`
|
|
97
|
+
**Strategy:**
|
|
98
|
+
1. In `getDjsConfig()`, after the `options.accept` assignment, add:
|
|
99
|
+
```javascript
|
|
100
|
+
options.chunksUploaded = (file, done) => {
|
|
101
|
+
if (file._asyncProcessing) {
|
|
102
|
+
file._chunksUploadedDone = done;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
done();
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
2. In `setupEvents()` → `sending` handler → `xhr.onload`, add before `dropzoneOnLoad(e)`:
|
|
109
|
+
```javascript
|
|
110
|
+
if (xhr?.status == 202) {
|
|
111
|
+
file._asyncProcessing = true;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
3. In `pollUploadStatus`, add `file` parameter. When `data.status === 'complete'`:
|
|
115
|
+
```javascript
|
|
116
|
+
if (file?._chunksUploadedDone) {
|
|
117
|
+
file._chunksUploadedDone();
|
|
118
|
+
}
|
|
119
|
+
this.onUploadComplete(data);
|
|
120
|
+
```
|
|
121
|
+
4. Update the 202 branch in `xhr.onload` to pass `file` to `pollUploadStatus`.
|
|
122
|
+
**DoD:** Reproducing test PASSES. Full test suite PASSES. Diff touches only `src/components/inputs/dropzone/index.js`.
|
|
123
|
+
**Verify:** `npx jest src/components/inputs/dropzone/__tests__/dropzone.test.js && npx jest`
|
|
124
|
+
|
|
125
|
+
### Task 3: Quality Gate
|
|
126
|
+
|
|
127
|
+
**Objective:** Lint + build clean, full suite re-run.
|
|
128
|
+
**DoD:** Lint clean, build green, full suite green. No performance regressions (polling interval unchanged, no new allocations on hot paths).
|
|
129
|
+
**Verify:** `npx jest && npm run build`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# UploadInputV3 Duplicate File / Max Files Reached After Upload Fix Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-04-23
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Status: VERIFIED
|
|
6
|
+
Approved: Yes
|
|
7
|
+
Iterations: 0
|
|
8
|
+
Worktree: No
|
|
9
|
+
Type: Bugfix
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
**Symptom:** After uploading a file via UploadInputV3, the component shows "Maximum number of files has been reached" error and displays the file twice — once with the original filename (`image.png`) and once with the server-renamed/hashed filename (`image_e2eed7fb3bc6bd1b16f77beb0f375b82.png`).
|
|
14
|
+
**Trigger:** Upload a single file when `maxFiles=1`. The server renames the file (adds hash). The component fails to clean up the transitional `uploadingFiles` entry because the renamed filename doesn't match.
|
|
15
|
+
**Root Cause:** `src/components/inputs/upload-input-v3/index.js:164` — The `useEffect` cleanup compares `v.filename === f.name` to match completed `uploadingFiles` entries against `value` entries. When the server renames the file, this comparison fails, the stale completed entry persists, and the file appears twice.
|
|
16
|
+
|
|
17
|
+
**Secondary issue:** `src/components/inputs/dropzone/index.js:156` — Missing `return` after `done('Max files reached.')` causes `done()` to be called a second time (without error), potentially accepting files that should be rejected.
|
|
18
|
+
|
|
19
|
+
## Investigation
|
|
20
|
+
|
|
21
|
+
- **V3-specific bug.** V2 does not maintain `uploadingFiles` state — it relies on Dropzone's built-in preview and only renders from the `value` prop. V3 introduced `uploadingFiles` state for custom progress UI, with a `useEffect` that cleans up completed entries when `value` updates. This cleanup fails on server-renamed files.
|
|
22
|
+
- **Flow:** `handleAddedFile` → `handleFileCompleted` (marks `complete:true` with original name) → parent updates `value` (with server-renamed filename) → `useEffect` compares `v.filename === f.name` → mismatch → stale entry persists.
|
|
23
|
+
- **The `accept` callback** at `dropzone/index.js:155-159` has a missing `return` after rejecting with `done('Max files reached.')`, causing a fallthrough to `done()`.
|
|
24
|
+
|
|
25
|
+
## Behavior Contract
|
|
26
|
+
|
|
27
|
+
**Given:** UploadInputV3 with `maxFiles=1`, a file is uploaded and the server renames it (response contains a different filename than the original)
|
|
28
|
+
**When:** The parent updates the `value` prop with the server-returned file entry (containing the renamed filename)
|
|
29
|
+
**Currently (bug):** The completed `uploadingFiles` entry (with original name) persists because `v.filename !== f.name`, causing the file to appear twice — once from `uploadingFiles` and once from `value`
|
|
30
|
+
**Expected (fix):** When `value` updates with new entries, all completed `uploadingFiles` entries are removed regardless of filename, since `value` is the source of truth for uploaded files. The file appears exactly once (from `value`).
|
|
31
|
+
**Anti-regression:** Files still uploading (not yet complete) must NOT be removed from `uploadingFiles` when `value` changes. Error file display, progress tracking, and file deletion functionality must remain intact.
|
|
32
|
+
|
|
33
|
+
## Fix Approach
|
|
34
|
+
|
|
35
|
+
**Chosen:** Remove all completed entries on value change
|
|
36
|
+
**Why:** The `value` prop is the authoritative source of truth for uploaded files. Once it changes to include new entries, any completed `uploadingFiles` entries are transitional and redundant — they exist only to bridge the visual gap between upload completion and parent state update. Removing all completed entries when `value` has entries is simple, handles server renames, and is semantically correct.
|
|
37
|
+
**Alternatives considered:**
|
|
38
|
+
- *Track server name through upload flow* — precise but complex; requires heuristic matching in `wrappedOnUploadComplete` to find which `uploadingFiles` entry corresponds to the response. Fragile for concurrent uploads.
|
|
39
|
+
- *Fuzzy filename matching* — check if value entry's filename contains the base name; fragile, depends on server naming convention.
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- `src/components/inputs/upload-input-v3/index.js` (primary fix — useEffect cleanup)
|
|
43
|
+
- `src/components/inputs/dropzone/index.js` (secondary fix — missing return)
|
|
44
|
+
- `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js` (reproducing test)
|
|
45
|
+
|
|
46
|
+
**Strategy:** Change the `useEffect` at line 160-166 to remove all completed entries from `uploadingFiles` when `value` has entries, instead of matching by exact filename. Add missing `return` after `done('Max files reached.')` in the `accept` callback.
|
|
47
|
+
|
|
48
|
+
**Tests:** Add test to `upload-input-v3.test.js` that simulates: file added → file completed → value updated with different filename → verify only one file entry visible (from value, not from uploadingFiles).
|
|
49
|
+
|
|
50
|
+
## Verification Scenario
|
|
51
|
+
|
|
52
|
+
### TS-001: Server-Renamed File Upload
|
|
53
|
+
**Preconditions:** UploadInputV3 with maxFiles=1, server configured to rename uploaded files
|
|
54
|
+
|
|
55
|
+
| Step | Action | Expected Result (after fix) |
|
|
56
|
+
|------|--------|-----------------------------|
|
|
57
|
+
| 1 | Upload a single file via UploadInputV3 | File appears as "Loading" then "Complete" |
|
|
58
|
+
| 2 | Parent updates value with server-renamed file | File appears exactly once (from value), no duplicate entry, no "max files" error |
|
|
59
|
+
|
|
60
|
+
## Progress
|
|
61
|
+
|
|
62
|
+
- [x] Task 1: Write Reproducing Test (RED)
|
|
63
|
+
- [x] Task 2: Implement Fix at Root Cause
|
|
64
|
+
- [x] Task 3: Quality Gate
|
|
65
|
+
**Tasks:** 3 | **Done:** 3
|
|
66
|
+
|
|
67
|
+
## Tasks
|
|
68
|
+
|
|
69
|
+
### Task 1: Write Reproducing Test (RED)
|
|
70
|
+
|
|
71
|
+
**Objective:** Encode the Behavior Contract as a failing test BEFORE writing any fix code.
|
|
72
|
+
**Files:** `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
|
|
73
|
+
**Entry point:** `UploadInputV3` component (rendered with mock DropzoneV3)
|
|
74
|
+
**Test scenario:**
|
|
75
|
+
1. Render UploadInputV3 with `maxFiles=1` and `value=[]`
|
|
76
|
+
2. Simulate `onAddedFile({ name: 'image.png', size: 75000 })`
|
|
77
|
+
3. Simulate `onFileCompleted({ name: 'image.png', size: 75000 })`
|
|
78
|
+
4. Re-render with `value=[{ filename: 'image_abc123.png', size: 75000 }]` (server-renamed)
|
|
79
|
+
5. Assert: only ONE file entry with text `image_abc123.png` is visible; original `image.png` is NOT visible
|
|
80
|
+
**DoD:** Test exists, named `test('cleans up completed uploading file when value updates with server-renamed filename')`, runs, fails because the stale `image.png` entry persists alongside `image_abc123.png`.
|
|
81
|
+
**Verify:** `npx jest src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js --verbose`
|
|
82
|
+
|
|
83
|
+
### Task 2: Implement Fix at Root Cause
|
|
84
|
+
|
|
85
|
+
**Objective:** Minimal change to fix the useEffect cleanup and the missing return.
|
|
86
|
+
**Files:**
|
|
87
|
+
- `src/components/inputs/upload-input-v3/index.js` — change useEffect at line 160-166 to remove all completed entries when value has entries
|
|
88
|
+
- `src/components/inputs/dropzone/index.js` — add `return` after `done('Max files reached.')` at line 156
|
|
89
|
+
**Strategy:**
|
|
90
|
+
1. In `upload-input-v3/index.js`, change the useEffect cleanup from:
|
|
91
|
+
```javascript
|
|
92
|
+
setUploadingFiles(prev => prev.filter(f => {
|
|
93
|
+
if (!f.complete) return true;
|
|
94
|
+
return !value.some(v => v.filename === f.name);
|
|
95
|
+
}));
|
|
96
|
+
```
|
|
97
|
+
To:
|
|
98
|
+
```javascript
|
|
99
|
+
setUploadingFiles(prev => prev.filter(f => !f.complete));
|
|
100
|
+
```
|
|
101
|
+
2. In `dropzone/index.js`, add `return;` after `done('Max files reached.');` at line 156.
|
|
102
|
+
**DoD:** Reproducing test PASSES. Full test suite PASSES. Diff touches root-cause files.
|
|
103
|
+
**Verify:** `npx jest --verbose`
|
|
104
|
+
|
|
105
|
+
### Task 3: Quality Gate
|
|
106
|
+
|
|
107
|
+
**Objective:** Lint + build clean, full suite re-run.
|
|
108
|
+
**DoD:** Full suite green, no lint errors, build succeeds.
|
|
109
|
+
**Verify:** `npx jest --verbose && npm run build-dev`
|