openstack-uicore-foundation 4.2.28 → 4.2.30

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.
@@ -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,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__plugin_context-mode_context-mode__ctx_batch_execute"
5
+ ]
6
+ }
7
+ }
@@ -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`
@@ -0,0 +1,99 @@
1
+ # UploadInputV3 Premature Completion on HTTP 202 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 that triggers HTTP 202 (async server processing), the file immediately shows as "Complete" instead of waiting for polling to confirm the server has finished processing.
14
+ **Trigger:** Upload a file via UploadInputV3. Server returns HTTP 202 with `file_id`. Dropzone's `success` event fires immediately (before polling starts or completes), and V3's `handleFileCompleted` marks the file as `complete: true`.
15
+ **Root Cause:** `src/components/inputs/upload-input-v3/index.js:153` — `handleFileCompleted()` unconditionally marks the file as `complete: true` when Dropzone's `success` event fires, without checking `file._asyncProcessing`. For non-chunked 202 uploads, `_finished()` is called directly by Dropzone (`dropzone.js:2707`), bypassing the `chunksUploaded` override that defers completion for chunked uploads.
16
+
17
+ **Secondary issue:** `wrappedOnUploadComplete` at line 190 is a passthrough — it doesn't mark uploading files as complete. When polling finishes, `onUploadComplete` fires but the `uploadingFiles` entry isn't cleaned up because `complete` was never set (if the guard blocks `handleFileCompleted`).
18
+
19
+ ## Investigation
20
+
21
+ - **V3-specific bug.** V2 has zero handling for `success`/`onFileCompleted`/`complete` — it renders only from the `value` prop, which is updated after `onUploadComplete` (correctly waits for polling in the 202 case).
22
+ - **Flow for non-chunked 202:** `xhr.onload` → `file._asyncProcessing = true` → `dropzoneOnLoad(e)` → `_finishedUploading` → `!chunked` → `_finished(files, response, e)` → emits `success` → DropzoneV3 `success` handler → `onFileCompleted` → `handleFileCompleted` marks `complete: true` → file shows "Complete" immediately. Polling starts AFTER `dropzoneOnLoad` returns, but the file already appears done.
23
+ - **Chunking decision:** `file.upload.chunked = options.chunking && (options.forceChunking || file.size > options.chunkSize)`. Config sets `chunking: true` but not `forceChunking` (default `false`), and `chunkSize` defaults to 2MB. Files ≤ 2MB are uploaded as non-chunked, bypassing the `chunksUploaded` override entirely.
24
+ - **`chunksUploaded` override** at `dropzone/index.js:164` correctly defers `done()` for chunked 202 uploads, but is never called for non-chunked uploads.
25
+ - **`wrappedOnUploadComplete`** at `upload-input-v3/index.js:190` just passes through to the parent — it doesn't update `uploadingFiles` state at all.
26
+
27
+ ## Behavior Contract
28
+
29
+ **Given:** UploadInputV3 with a file upload that returns HTTP 202 (async processing), where `file._asyncProcessing` is set to `true` on the Dropzone file object
30
+ **When:** Dropzone's `success` event fires (immediately after `_finishedUploading` calls `_finished` for non-chunked uploads, or via `chunksUploaded` for chunked uploads)
31
+ **Currently (bug):** `handleFileCompleted` marks the file as `complete: true` immediately, showing "Complete" in the UI before polling confirms server processing is done
32
+ **Expected (fix):** When `file._asyncProcessing` is true, `handleFileCompleted` does NOT mark the file as complete. The file stays in "Loading" state. When polling finishes and `onUploadComplete` fires (via `wrappedOnUploadComplete`), uploading files are marked complete, enabling the existing useEffect cleanup to remove them when `value` updates.
33
+ **Anti-regression:** Synchronous uploads (HTTP 200) must still mark files as "Complete" immediately via `handleFileCompleted`. File deletion, error display, progress tracking, and the existing server-renamed-filename cleanup must remain intact.
34
+
35
+ ## Fix Approach
36
+
37
+ **Chosen:** Guard in V3 handlers
38
+ **Why:** The `_asyncProcessing` flag is already set on the Dropzone file object before `success` fires. V3 just needs to check it in `handleFileCompleted` and mark files complete in `wrappedOnUploadComplete`. No changes to DropzoneJS or DropzoneV3 — purely V3 state management.
39
+ **Alternatives considered:**
40
+ - *Guard in DropzoneV3 success handler* — would work but touches the shared wrapper file used by all upload versions, increasing regression surface.
41
+ - *Override _finishedUploading in DropzoneJS* — fixes at the Dropzone level but patches library internals, fragile across Dropzone upgrades.
42
+
43
+ **Files:**
44
+ - `src/components/inputs/upload-input-v3/index.js` (primary fix — handleFileCompleted guard + wrappedOnUploadComplete)
45
+ - `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js` (reproducing test)
46
+
47
+ **Strategy:**
48
+ 1. In `handleFileCompleted`: check `file._asyncProcessing` — if true, return early (don't mark complete)
49
+ 2. In `wrappedOnUploadComplete`: before calling the parent callback, mark all uploading files as `complete: true` via `setUploadingFiles`. This ensures cleanup works for both sync (where `handleFileCompleted` already set it) and async (where it was skipped).
50
+
51
+ **Tests:** Add test to `upload-input-v3.test.js` that simulates: file added → file completed with `_asyncProcessing: true` → verify file still shows "Loading" (not "Complete").
52
+
53
+ ## Verification Scenario
54
+
55
+ ### TS-001: Async Upload Processing State
56
+ **Preconditions:** UploadInputV3 with `maxFiles=1`, server configured to return HTTP 202 for uploads
57
+
58
+ | Step | Action | Expected Result (after fix) |
59
+ |------|--------|-----------------------------|
60
+ | 1 | Upload a file that triggers HTTP 202 async processing | File shows "Loading" with progress bar, NOT "Complete" |
61
+ | 2 | Wait for polling to return `status: 'complete'` and parent to update value | File transitions to "Complete" (from value section), no duplicate entries |
62
+
63
+ ## Progress
64
+
65
+ - [x] Task 1: Write Reproducing Test (RED)
66
+ - [x] Task 2: Implement Fix at Root Cause
67
+ - [x] Task 3: Quality Gate
68
+ **Tasks:** 3 | **Done:** 3
69
+
70
+ ## Tasks
71
+
72
+ ### Task 1: Write Reproducing Test (RED)
73
+
74
+ **Objective:** Encode the Behavior Contract as a failing test BEFORE writing any fix code.
75
+ **Files:** `src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
76
+ **Entry point:** `UploadInputV3` component (rendered with mock DropzoneV3)
77
+ **Test scenario:**
78
+ 1. Render UploadInputV3 with `maxFiles=1` and `value=[]`
79
+ 2. Simulate `onAddedFile({ name: 'video.mp4', size: 5000000 })`
80
+ 3. Simulate `onFileCompleted({ name: 'video.mp4', size: 5000000, _asyncProcessing: true })`
81
+ 4. Assert: file still shows "Loading" (not "Complete") — the `_asyncProcessing` flag should prevent marking as complete
82
+ **DoD:** Test exists, named `test('does not mark file as complete when _asyncProcessing is true')`, runs, fails because `handleFileCompleted` currently ignores `_asyncProcessing` and marks complete unconditionally.
83
+ **Verify:** `npx jest src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js --verbose`
84
+
85
+ ### Task 2: Implement Fix at Root Cause
86
+
87
+ **Objective:** Minimal change to `handleFileCompleted` and `wrappedOnUploadComplete` to prevent premature completion on HTTP 202.
88
+ **Files:** `src/components/inputs/upload-input-v3/index.js`
89
+ **Strategy:**
90
+ 1. In `handleFileCompleted` (line 153): add early return when `file._asyncProcessing` is true
91
+ 2. In `wrappedOnUploadComplete` (line 190): add `setUploadingFiles(prev => prev.map(f => ({ ...f, complete: true })))` before calling the parent callback, so all uploading files are marked complete when the server confirms processing is done
92
+ **DoD:** Reproducing test PASSES. Full test suite PASSES. Diff touches root-cause file only.
93
+ **Verify:** `npx jest --verbose`
94
+
95
+ ### Task 3: Quality Gate
96
+
97
+ **Objective:** Full suite re-run, build clean.
98
+ **DoD:** Full suite green, build succeeds, no performance regressions.
99
+ **Verify:** `npx jest --verbose && npm run build-dev`
@@ -1,2 +1,2 @@
1
- !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define("openstack-uicore-foundation",[],r):"object"==typeof exports?exports["openstack-uicore-foundation"]=r():e["openstack-uicore-foundation"]=r()}(this,(()=>(()=>{"use strict";var e={5097:(e,r,a)=>{a(1116),a(6842),a(9087),a(9558),a(2183)},3195:(e,r,a)=>{a.d(r,{AUTH_ERROR_ACCESS_TOKEN_EXPIRED:()=>o,AUTH_ERROR_LOCK_ACQUIRE_ERROR:()=>s,AUTH_ERROR_MISSING_AUTH_INFO:()=>t,AUTH_ERROR_MISSING_REFRESH_TOKEN:()=>n,AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR:()=>d,AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR:()=>i});const t="AUTH_ERROR_MISSING_AUTH_INFO",n="AUTH_ERROR_MISSING_REFRESH_TOKEN",o="AUTH_ERROR_ACCESS_TOKEN_EXPIRED",s="AUTH_ERROR_LOCK_ACQUIRE_ERROR",i="AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR",d="AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR"},2183:(e,r,a)=>{a.d(r,{getAccessToken:()=>m});var t=a(9558),n=a(5812),o=a.n(n);a(806);const s=require("browser-tabs-lock");var i=a.n(s);const d=require("js-cookie");var u=a.n(d),l=(a(8041),a(9891),a(5097),a(8853),a(3195));const Lock=new(i()),GET_TOKEN_SILENTLY_LOCK_KEY="openstackuicore.lock.getTokenSilently",c="code",p="authInfo",y="idToken",_=async(e,r)=>{if(e===c&&O()){if(!r)throw T(),Error(l.AUTH_ERROR_MISSING_REFRESH_TOKEN);let e=await(async(e,r=5,a=1e3)=>{for(let t=0;t<r;t++)try{return await e()}catch(e){if(!e.message||!e.message.startsWith(l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR)||t===r-1)throw e;const n=a*Math.pow(2,t);console.log(`retryWithBackoff retry ${t+1}/${r} in ${n}ms`),await new Promise((e=>setTimeout(e,n)))}})((()=>f(r))),{access_token:a,expires_in:t,refresh_token:n,id_token:o}=e;return void 0===n&&(n=null),E(a,t,n,o),a}throw T(),Error(l.AUTH_ERROR_ACCESS_TOKEN_EXPIRED)},R=async()=>{console.log("openstack-uicore-foundation::Security::methods::_getAccessToken");let e=g();if(!e)throw console.log("openstack-uicore-foundation::Security::methods::_getAccessToken AUTH_ERROR_MISSING_AUTH_INFO"),Error(l.AUTH_ERROR_MISSING_AUTH_INFO);let{accessToken:r,expiresIn:a,accessTokenUpdatedAt:t,refreshToken:n}=e,s=Q();const i=o()().unix();let d=i-t;return a-=60,console.log(`openstack-uicore-foundation::Security::methods::_getAccessToken now ${i} accessTokenUpdatedAt ${t} expiresIn ${a} timeElapsedSecs ${d}`),(d>=a||null==r)&&(console.log("openstack-uicore-foundation::Security::methods::_getAccessToken access token expired, refreshing it ..."),r=await _(s,n)),r},m=async()=>{if("undefined"!=typeof navigator&&navigator.locks)return await navigator.locks.request(GET_TOKEN_SILENTLY_LOCK_KEY,(async e=>(console.log("openstack-uicore-foundation::Security::methods::getAccessToken web lock api",e),await R())));if(!await(0,t.retryPromise)((()=>Lock.acquireLock(GET_TOKEN_SILENTLY_LOCK_KEY,6e3)),10))throw Error(l.AUTH_ERROR_LOCK_ACQUIRE_ERROR);try{return await R()}finally{await Lock.releaseLock(GET_TOKEN_SILENTLY_LOCK_KEY)}},f=async e=>{let r=S(),a=h();const n={grant_type:"refresh_token",client_id:encodeURI(a),refresh_token:e},o=new AbortController,s=setTimeout((()=>o.abort()),1e4);let i,d;try{i=await fetch(`${r}/oauth2/token`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(n),signal:o.signal})}catch(e){throw console.log("refreshAccessToken network error:",e.message),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: ${e.message}`)}finally{clearTimeout(s)}if(!i.ok){if(console.log(`refreshAccessToken server error: ${i.status} - ${i.statusText}`),i.status>=500||408===i.status||429===i.status)throw Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: ${i.status} - ${i.statusText}`);throw(0,t.setSessionClearingState)(!0),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR}: ${i.status} - ${i.statusText}`)}try{d=await i.json()}catch(e){throw Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: invalid JSON response from IDP`)}let{access_token:u,refresh_token:c,expires_in:p,id_token:y}=d;if(!u)throw(0,t.setSessionClearingState)(!0),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR}: missing access_token in refresh response`);return{access_token:u,refresh_token:c,expires_in:p,id_token:y}},E=(e,r,a=null,n=null)=>{let o=g(),s={accessToken:e,expiresIn:r,accessTokenUpdatedAt:Math.floor(Date.now()/1e3)};null==a&&o&&(a=o.refreshToken),null==n&&o&&(n=o.idToken),a&&(s.refreshToken=a),n?(s[y]=n,u().set(y,n,{secure:!0,sameSite:"Lax"})):u().remove(y),(0,t.putOnLocalStorage)(p,JSON.stringify(s))},g=()=>{try{let e=(0,t.getFromLocalStorage)(p,!1);return e?JSON.parse(e):null}catch(e){return null}},T=()=>{"undefined"!=typeof window&&((0,t.removeFromLocalStorage)(p),u().remove(y))},h=()=>"undefined"!=typeof window?window.OAUTH2_CLIENT_ID:null,Q=()=>"undefined"!=typeof window&&window.OAUTH2_FLOW||"token id_token",O=()=>"undefined"==typeof window||new Boolean(window.OAUTH2_USE_REFRESH_TOKEN||!0),S=()=>"undefined"!=typeof window?window.IDP_BASE_URL:null},9087:(e,r,a)=>{a.d(r,{escapeFilterValue:()=>p,fetchErrorHandler:()=>l,fetchResponseHandler:()=>c});a(2462),a(806);var t=a(8041),n=a.n(t),o=a(9236),s=a.n(o),i=a(6842),d=a.n(i);a(9558),a(5097),a(2183);n().escapeQuerySpace=!1;const u=e=>r=>({type:e,payload:r}),l=(u("RESET_LOADING"),u("START_LOADING"),u("STOP_LOADING"),e=>{let r=e.status,a=e.statusText;switch(r){case 403:s().fire("ERROR",d().translate("errors.user_not_authz"),"warning");break;case 401:s().fire("ERROR",d().translate("errors.session_expired"),"error");break;case 412:s().fire("ERROR",a,"warning");case 500:s().fire("ERROR",d().translate("errors.server_error"),"error")}}),c=e=>{if(e.ok)return e.json();throw e},p=e=>e=(e=(e=(e=(e=String(e)).replace(/\\/g,"\\\\")).replace(/,/g,"\\,")).replace(/;/g,"\\;")).replace(/\+/g,"%2B")},8853:()=>{require("spark-md5"),require("crypto-js/sha256"),require("crypto-js/enc-base64url"),require("crypto-js/enc-hex"),"undefined"!=typeof window&&(window.crypto||window.msCrypto)},9558:(e,r,a)=>{a.d(r,{buildAPIBaseUrl:()=>t,getFromLocalStorage:()=>o,putOnLocalStorage:()=>n,removeFromLocalStorage:()=>s,retryPromise:()=>d,setSessionClearingState:()=>i});a(5812),a(8041);const t=e=>"undefined"!=typeof window?`${window.API_BASE_URL}${e}`:null``,n=(e,r)=>{"undefined"!=typeof window&&window.localStorage.setItem(e,r)},o=(e,r)=>{if("undefined"!=typeof window){let a=window.localStorage.getItem(e);return r&&(console.log(`getFromLocalStorage removing key ${e}`),s(e)),a}return null},s=e=>{"undefined"!=typeof window&&window.localStorage.removeItem(e)},i=e=>{"undefined"!=typeof window&&(window.clearing_session_state=e)},d=async(e,r=3)=>{for(let a=0;a<r;a++)if(await e())return!0;return!1}},3582:(e,r,a)=>{a.d(r,{queryRegistrationCompanies:()=>y});var t=a(9087),n=a(2183),o=a(9558),s=a(7825),i=a.n(s),d=a(8041),u=a.n(d);const l=500;u().escapeQuerySpace=!1;const c=async(e,r,a={})=>fetch((0,o.buildAPIBaseUrl)(e.toString()),a).then(t.fetchResponseHandler).then((e=>{"function"==typeof r&&r(e.data)})).catch((e=>(404===e.status&&r([]),e))).catch(t.fetchErrorHandler),p=async(e,r,a={})=>{let t;try{t=await(0,n.getAccessToken)()}catch(e){return"function"==typeof r&&r(e),Promise.reject()}return e.addQuery("access_token",t),c(e,r,a)},y=(i().debounce((async(e,r,a=10)=>{let n=u()("/api/v1/members");n.addQuery("expand","tickets,rsvp,schedule_summit_events,all_affiliations"),n.addQuery("order","first_name,last_name"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`full_name@@${e},first_name@@${e},last_name@@${e},email@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/attendees`);o.addQuery("order","first_name,last_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`full_name=@${r},email=@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a=10)=>{let n=u()("/api/v1/summits/all");n.addQuery("expand","tickets,rsvp,schedule_summit_events,all_affiliations"),n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()("/api/v1/"+(e?`summits/${e}/speakers`:"speakers"));o.addQuery("expand","member,registration_request"),o.addQuery("order","first_name,last_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`full_name@@${r},first_name@@${r},last_name@@${r},email@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a,n=50)=>{let o=u()("/api/v1/"+(e?`summits/${e}/track-tag-groups/all/allowed-tags`:"tags"));e&&o.addQuery("expand","tag,track_tag_group"),o.addQuery("order","tag"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`tag@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a,n=[],o=10)=>{let s=u()(`/api/v1/summits/${e}/tracks`);s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),(null==n?void 0:n.length)>0&&s.addQuery("filter[]",`not_id==${n.join("||")}`),r&&(r=(0,t.escapeFilterValue)(r),s.addQuery("filter[]",`name@@${r}`)),p(s,a)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/track-groups`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a=!1,n,o=10)=>{let s=u()(`/api/v1/summits/${e}/events`+(a?"/published":""));s.addQuery("order","title"),s.addQuery("page",1),s.addQuery("per_page",o),r&&(r=(0,t.escapeFilterValue)(r),s.addQuery("filter[]",`title@@${r}`)),p(s,n)}),l),i().debounce((async(e,r,a,n=null,o=10)=>{let s=u()(`/api/v1/summits/${e}/event-types`);s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),r&&(r=(0,t.escapeFilterValue)(r),s.addQuery("filter[]",`name@@${r}`)),n&&(n=(0,t.escapeFilterValue)(n),s.addQuery("filter[]",`class_name==${n}`)),p(s,a)}),l),i().debounce((async(e,r,a=10)=>{let n=u()("/api/v1/groups");n.addQuery("order","title,code"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`title@@${e},code@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,a=10)=>{let n=u()("/api/v1/companies");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/registration-companies`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,a)}),l));i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/sponsors`);o.addQuery("expand","company,sponsorship,sponsorship.type"),o.addQuery("order","id"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`company_name@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/sponsors`);o.addQuery("expand","company,sponsorship,sponsorship.type"),o.addQuery("fields","id,company.name,sponsorship.type.name"),o.addQuery("relations","none,company.none,sponsorship.type.none"),o.addQuery("filter[]","badge_scans_count>0"),o.addQuery("order","+company_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`company_name@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a,n=10)=>{let o=u()(`/api/v1/summits/${e}/access-level-types`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,a)}),l),i().debounce((async(e,r,a=10)=>{let n=u()("/api/v1/organizations");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l);i().debounce((async(e,r={},a,n="v1",o=10)=>{let s=u()(`/api/${n}/summits/${e}/ticket-types`);if(s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),r.hasOwnProperty("name")){const e=(0,t.escapeFilterValue)(r.name);e&&""!=e&&s.addQuery("filter[]",`name@@${e}`)}if(r.hasOwnProperty("audience")){const e=(0,t.escapeFilterValue)(r.audience);e&&""!=e&&s.addQuery("filter[]",`audience==${e}`)}p(s,a)}),l),i().debounce((async(e,r,a=10)=>{const n=u()("/api/v1/sponsored-projects");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",a),e&&(e=(0,t.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,a,n=10,o=[])=>{let s=u()(`/api/v1/summits/${e}/promo-codes`);s.addQuery("order","code"),s.addQuery("page",1),s.addQuery("per_page",n),r&&(r=(0,t.escapeFilterValue)(r),s.addQuery("filter[]",`code@@${r}`));for(const e of o)s.addQuery("filter[]",e);p(s,a)}),l)},1116:e=>{e.exports=require("@babel/runtime/helpers/defineProperty")},2462:e=>{e.exports=require("@babel/runtime/helpers/objectWithoutProperties")},6842:e=>{e.exports=require("i18n-react/dist/i18n-react")},9891:e=>{e.exports=require("idtoken-verifier")},7825:e=>{e.exports=require("lodash")},5812:e=>{e.exports=require("moment-timezone")},806:e=>{e.exports=require("superagent/lib/client")},9236:e=>{e.exports=require("sweetalert2")},8041:e=>{e.exports=require("urijs")}},r={};function a(t){var n=r[t];if(void 0!==n)return n.exports;var o=r[t]={exports:{}};return e[t](o,o.exports,a),o.exports}(()=>{a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r}})(),(()=>{a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})}})(),(()=>{a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r)})(),(()=>{a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}})();var t={};a.r(t),a.d(t,{default:()=>f});const n=require("@babel/runtime/helpers/extends");var o=a.n(n),s=a(2462),i=a.n(s);const d=require("react");var u=a.n(d);const l=require("prop-types");var c=a.n(l);const p=require("@mui/material");var y=a(3582);const _=["summitId","isRequired","sx","onChange","id","name","label","value","error","helperText","onBlur","placeholder","options2Show","disableShrink"],R=["key"],m=e=>{let{summitId:r,isRequired:a,sx:t,onChange:n,id:s,name:d,label:l,value:c,error:m,helperText:f,onBlur:E,placeholder:g,options2Show:T,disableShrink:h}=e,Q=i()(e,_);const[O,S]=u().useState(""),[w,k]=u().useState([]);return u().useEffect((()=>{""!==O?(0,y.queryRegistrationCompanies)(r,O,(e=>{let r=[];c&&(r=[c]),e&&(r=[...r,...e]),k(r)}),T):k(c?[c]:[])}),[c,O]),u().createElement(p.Autocomplete,o()({sx:t,id:s,name:d,options:w,autoComplete:!0,freeSolo:!0,includeInputInList:!0,filterSelectedOptions:!0,value:c,onBlur:()=>{E&&E(d)},getOptionLabel:e=>"string"==typeof e?e:e.inputValue?e.inputValue:e.name,onChange:(e,r)=>{let a=(null==r?void 0:r.inputValue)||r;r&&"object"==typeof r&&r.inputValue&&(a={id:0,name:r.inputValue}),k(a?[a,...w]:w),n({target:{id:d,value:a,type:"companyinput"}})},onInputChange:(e,r)=>{S(r)},filterOptions:(e,r)=>{const{inputValue:a}=r,t=[...e],n=e.some((e=>a===e.title));return""===a||n||t.push({inputValue:a,name:`Select "${a}"`}),t},renderInput:e=>u().createElement(p.TextField,o()({},e,{label:l,placeholder:g,fullWidth:!0,required:a,helperText:f,error:m,margin:"normal",InputLabelProps:{shrink:h}})),renderOption:(e,r)=>{const{key:a}=e,t=i()(e,R);return u().createElement("li",o()({key:a},t),u().createElement(p.Typography,{variant:"body2",sx:{fontSize:"1em",color:"text.secondary"}},r.name))}},Q))};m.defaultProps={name:"GENERAL",label:"Company",options2Show:20,disableShrink:!1},m.propTypes={summitId:c().number.isRequired,value:c().oneOfType([c().string,c().object]),onChange:c().func.isRequired,isRequired:c().bool,name:c().string,error:c().bool,helperText:c().string,onBlur:c().func,options2Show:c().number,placeholder:c().string,label:c().string,disableShrink:c().bool};const f=m;return t})()));
1
+ !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define("openstack-uicore-foundation",[],r):"object"==typeof exports?exports["openstack-uicore-foundation"]=r():e["openstack-uicore-foundation"]=r()}(this,(()=>(()=>{"use strict";var e={5097:(e,r,t)=>{t(1116),t(6842),t(9087),t(9558),t(2183)},3195:(e,r,t)=>{t.d(r,{AUTH_ERROR_ACCESS_TOKEN_EXPIRED:()=>o,AUTH_ERROR_LOCK_ACQUIRE_ERROR:()=>s,AUTH_ERROR_MISSING_AUTH_INFO:()=>a,AUTH_ERROR_MISSING_REFRESH_TOKEN:()=>n,AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR:()=>d,AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR:()=>i});const a="AUTH_ERROR_MISSING_AUTH_INFO",n="AUTH_ERROR_MISSING_REFRESH_TOKEN",o="AUTH_ERROR_ACCESS_TOKEN_EXPIRED",s="AUTH_ERROR_LOCK_ACQUIRE_ERROR",i="AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR",d="AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR"},2183:(e,r,t)=>{t.d(r,{getAccessToken:()=>R});var a=t(9558),n=t(5812),o=t.n(n);t(806);const s=require("browser-tabs-lock");var i=t.n(s);const d=require("js-cookie");var u=t.n(d),l=(t(8041),t(9891),t(5097),t(8853),t(3195));const Lock=new(i()),GET_TOKEN_SILENTLY_LOCK_KEY="openstackuicore.lock.getTokenSilently",c="code",p="authInfo",y="idToken",_=async(e,r)=>{if(e===c&&O()){if(!r)throw T(),Error(l.AUTH_ERROR_MISSING_REFRESH_TOKEN);let e=await(async(e,r=5,t=1e3)=>{for(let a=0;a<r;a++)try{return await e()}catch(e){if(!e.message||!e.message.startsWith(l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR)||a===r-1)throw e;const n=t*Math.pow(2,a);console.log(`retryWithBackoff retry ${a+1}/${r} in ${n}ms`),await new Promise((e=>setTimeout(e,n)))}})((()=>f(r))),{access_token:t,expires_in:a,refresh_token:n,id_token:o}=e;return void 0===n&&(n=null),g(t,a,n,o),t}throw T(),Error(l.AUTH_ERROR_ACCESS_TOKEN_EXPIRED)},m=async()=>{console.log("openstack-uicore-foundation::Security::methods::_getAccessToken");let e=E();if(!e)throw console.log("openstack-uicore-foundation::Security::methods::_getAccessToken AUTH_ERROR_MISSING_AUTH_INFO"),Error(l.AUTH_ERROR_MISSING_AUTH_INFO);let{accessToken:r,expiresIn:t,accessTokenUpdatedAt:a,refreshToken:n}=e,s=h();const i=o()().unix();let d=i-a;return t-=60,console.log(`openstack-uicore-foundation::Security::methods::_getAccessToken now ${i} accessTokenUpdatedAt ${a} expiresIn ${t} timeElapsedSecs ${d}`),(d>=t||null==r)&&(console.log("openstack-uicore-foundation::Security::methods::_getAccessToken access token expired, refreshing it ..."),r=await _(s,n)),r},R=async()=>{if("undefined"!=typeof navigator&&navigator.locks)return await navigator.locks.request(GET_TOKEN_SILENTLY_LOCK_KEY,(async e=>(console.log("openstack-uicore-foundation::Security::methods::getAccessToken web lock api",e),await m())));if(!await(0,a.retryPromise)((()=>Lock.acquireLock(GET_TOKEN_SILENTLY_LOCK_KEY,6e3)),10))throw Error(l.AUTH_ERROR_LOCK_ACQUIRE_ERROR);try{return await m()}finally{await Lock.releaseLock(GET_TOKEN_SILENTLY_LOCK_KEY)}},f=async e=>{let r=S(),t=Q();const n={grant_type:"refresh_token",client_id:encodeURI(t),refresh_token:e},o=new AbortController,s=setTimeout((()=>o.abort()),1e4);let i,d;try{i=await fetch(`${r}/oauth2/token`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(n),signal:o.signal})}catch(e){throw console.log("refreshAccessToken network error:",e.message),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: ${e.message}`)}finally{clearTimeout(s)}if(!i.ok){if(console.log(`refreshAccessToken server error: ${i.status} - ${i.statusText}`),i.status>=500||408===i.status||429===i.status)throw Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: ${i.status} - ${i.statusText}`);throw(0,a.setSessionClearingState)(!0),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR}: ${i.status} - ${i.statusText}`)}try{d=await i.json()}catch(e){throw Error(`${l.AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR}: invalid JSON response from IDP`)}let{access_token:u,refresh_token:c,expires_in:p,id_token:y}=d;if(!u)throw(0,a.setSessionClearingState)(!0),Error(`${l.AUTH_ERROR_REFRESH_TOKEN_REQUEST_ERROR}: missing access_token in refresh response`);return{access_token:u,refresh_token:c,expires_in:p,id_token:y}},g=(e,r,t=null,n=null)=>{let o=E(),s={accessToken:e,expiresIn:r,accessTokenUpdatedAt:Math.floor(Date.now()/1e3)};null==t&&o&&(t=o.refreshToken),null==n&&o&&(n=o.idToken),t&&(s.refreshToken=t),n?(s[y]=n,u().set(y,n,{secure:!0,sameSite:"Lax"})):u().remove(y),(0,a.putOnLocalStorage)(p,JSON.stringify(s))},E=()=>{try{let e=(0,a.getFromLocalStorage)(p,!1);return e?JSON.parse(e):null}catch(e){return null}},T=()=>{"undefined"!=typeof window&&((0,a.removeFromLocalStorage)(p),u().remove(y))},Q=()=>"undefined"!=typeof window?window.OAUTH2_CLIENT_ID:null,h=()=>"undefined"!=typeof window&&window.OAUTH2_FLOW||"token id_token",O=()=>"undefined"==typeof window||new Boolean(window.OAUTH2_USE_REFRESH_TOKEN||!0),S=()=>"undefined"!=typeof window?window.IDP_BASE_URL:null},9087:(e,r,t)=>{t.d(r,{escapeFilterValue:()=>p,fetchErrorHandler:()=>l,fetchResponseHandler:()=>c});t(2462),t(806);var a=t(8041),n=t.n(a),o=t(9236),s=t.n(o),i=t(6842),d=t.n(i);t(9558),t(5097),t(2183);n().escapeQuerySpace=!1;const u=e=>r=>({type:e,payload:r}),l=(u("RESET_LOADING"),u("START_LOADING"),u("STOP_LOADING"),e=>{let r=e.status,t=e.statusText;switch(r){case 403:s().fire("ERROR",d().translate("errors.user_not_authz"),"warning");break;case 401:s().fire("ERROR",d().translate("errors.session_expired"),"error");break;case 412:s().fire("ERROR",t,"warning");case 500:s().fire("ERROR",d().translate("errors.server_error"),"error")}}),c=e=>{if(e.ok)return e.json();throw e},p=e=>e=(e=(e=(e=(e=String(e)).replace(/\\/g,"\\\\")).replace(/,/g,"\\,")).replace(/;/g,"\\;")).replace(/\+/g,"%2B")},8853:()=>{require("spark-md5"),require("crypto-js/sha256"),require("crypto-js/enc-base64url"),require("crypto-js/enc-hex"),"undefined"!=typeof window&&(window.crypto||window.msCrypto)},9558:(e,r,t)=>{t.d(r,{buildAPIBaseUrl:()=>a,getFromLocalStorage:()=>o,putOnLocalStorage:()=>n,removeFromLocalStorage:()=>s,retryPromise:()=>d,setSessionClearingState:()=>i});t(5812),t(8041);const a=e=>"undefined"!=typeof window?`${window.API_BASE_URL}${e}`:null``,n=(e,r)=>{"undefined"!=typeof window&&window.localStorage.setItem(e,r)},o=(e,r)=>{if("undefined"!=typeof window){let t=window.localStorage.getItem(e);return r&&(console.log(`getFromLocalStorage removing key ${e}`),s(e)),t}return null},s=e=>{"undefined"!=typeof window&&window.localStorage.removeItem(e)},i=e=>{"undefined"!=typeof window&&(window.clearing_session_state=e)},d=async(e,r=3)=>{for(let t=0;t<r;t++)if(await e())return!0;return!1}},3582:(e,r,t)=>{t.d(r,{queryRegistrationCompanies:()=>y});var a=t(9087),n=t(2183),o=t(9558),s=t(7825),i=t.n(s),d=t(8041),u=t.n(d);const l=500;u().escapeQuerySpace=!1;const c=async(e,r,t={})=>fetch((0,o.buildAPIBaseUrl)(e.toString()),t).then(a.fetchResponseHandler).then((e=>{"function"==typeof r&&r(e.data)})).catch((e=>(404===e.status&&r([]),e))).catch(a.fetchErrorHandler),p=async(e,r,t={})=>{let a;try{a=await(0,n.getAccessToken)()}catch(e){return"function"==typeof r&&r(e),Promise.reject()}return e.addQuery("access_token",a),c(e,r,t)},y=(i().debounce((async(e,r,t=10)=>{let n=u()("/api/v1/members");n.addQuery("expand","tickets,rsvp,schedule_summit_events,all_affiliations"),n.addQuery("order","first_name,last_name"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`full_name@@${e},first_name@@${e},last_name@@${e},email@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/attendees`);o.addQuery("order","first_name,last_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`full_name=@${r},email=@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t=10)=>{let n=u()("/api/v1/summits/all");n.addQuery("expand","tickets,rsvp,schedule_summit_events,all_affiliations"),n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()("/api/v1/"+(e?`summits/${e}/speakers`:"speakers"));o.addQuery("expand","member,registration_request"),o.addQuery("order","first_name,last_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`full_name@@${r},first_name@@${r},last_name@@${r},email@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t,n=50)=>{let o=u()("/api/v1/"+(e?`summits/${e}/track-tag-groups/all/allowed-tags`:"tags"));e&&o.addQuery("expand","tag,track_tag_group"),o.addQuery("order","tag"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`tag@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t,n=[],o=10)=>{let s=u()(`/api/v1/summits/${e}/tracks`);s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),(null==n?void 0:n.length)>0&&s.addQuery("filter[]",`not_id==${n.join("||")}`),r&&(r=(0,a.escapeFilterValue)(r),s.addQuery("filter[]",`name@@${r}`)),p(s,t)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/track-groups`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t=!1,n,o=10)=>{let s=u()(`/api/v1/summits/${e}/events`+(t?"/published":""));s.addQuery("order","title"),s.addQuery("page",1),s.addQuery("per_page",o),r&&(r=(0,a.escapeFilterValue)(r),s.addQuery("filter[]",`title@@${r}`)),p(s,n)}),l),i().debounce((async(e,r,t,n=null,o=10)=>{let s=u()(`/api/v1/summits/${e}/event-types`);s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),r&&(r=(0,a.escapeFilterValue)(r),s.addQuery("filter[]",`name@@${r}`)),n&&(n=(0,a.escapeFilterValue)(n),s.addQuery("filter[]",`class_name==${n}`)),p(s,t)}),l),i().debounce((async(e,r,t=10)=>{let n=u()("/api/v1/groups");n.addQuery("order","title,code"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`title@@${e},code@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,t=10)=>{let n=u()("/api/v1/companies");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/registration-companies`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,t)}),l));i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/sponsors`);o.addQuery("expand","company,sponsorship,sponsorship.type"),o.addQuery("order","id"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`company_name@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/sponsors`);o.addQuery("expand","company,sponsorship,sponsorship.type"),o.addQuery("fields","id,company.name,sponsorship.type.name"),o.addQuery("relations","none,company.none,sponsorship.type.none"),o.addQuery("filter[]","badge_scans_count>0"),o.addQuery("order","+company_name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`company_name@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t,n=10)=>{let o=u()(`/api/v1/summits/${e}/access-level-types`);o.addQuery("order","name"),o.addQuery("page",1),o.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),o.addQuery("filter[]",`name@@${r}`)),p(o,t)}),l),i().debounce((async(e,r,t=10)=>{let n=u()("/api/v1/organizations");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l);i().debounce((async(e,r={},t,n="v1",o=10)=>{let s=u()(`/api/${n}/summits/${e}/ticket-types`);if(s.addQuery("order","name"),s.addQuery("page",1),s.addQuery("per_page",o),r.hasOwnProperty("name")){const e=(0,a.escapeFilterValue)(r.name);e&&""!=e&&s.addQuery("filter[]",`name@@${e}`)}if(r.hasOwnProperty("audience")){const e=(0,a.escapeFilterValue)(r.audience);e&&""!=e&&s.addQuery("filter[]",`audience==${e}`)}p(s,t)}),l),i().debounce((async(e,r,t=10)=>{const n=u()("/api/v1/sponsored-projects");n.addQuery("order","name"),n.addQuery("page",1),n.addQuery("per_page",t),e&&(e=(0,a.escapeFilterValue)(e),n.addQuery("filter[]",`name@@${e}`)),p(n,r)}),l),i().debounce((async(e,r,t,n=10,o=[])=>{let s=u()(`/api/v1/summits/${e}/promo-codes`);s.addQuery("order","code"),s.addQuery("page",1),s.addQuery("per_page",n),r&&(r=(0,a.escapeFilterValue)(r),s.addQuery("filter[]",`code@@${r}`));for(const e of o)s.addQuery("filter[]",e);p(s,t)}),l)},1896:(e,r,t)=>{t.d(r,{default:()=>o});var a=t(2015),n=t.n(a);const o=e=>{const r=n().useRef(e);return n().useLayoutEffect((()=>{r.current=e})),n().useCallback(((...e)=>r.current(...e)),[])}},1116:e=>{e.exports=require("@babel/runtime/helpers/defineProperty")},2462:e=>{e.exports=require("@babel/runtime/helpers/objectWithoutProperties")},6842:e=>{e.exports=require("i18n-react/dist/i18n-react")},9891:e=>{e.exports=require("idtoken-verifier")},7825:e=>{e.exports=require("lodash")},5812:e=>{e.exports=require("moment-timezone")},2015:e=>{e.exports=require("react")},806:e=>{e.exports=require("superagent/lib/client")},9236:e=>{e.exports=require("sweetalert2")},8041:e=>{e.exports=require("urijs")}},r={};function t(a){var n=r[a];if(void 0!==n)return n.exports;var o=r[a]={exports:{}};return e[a](o,o.exports,t),o.exports}(()=>{t.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return t.d(r,{a:r}),r}})(),(()=>{t.d=(e,r)=>{for(var a in r)t.o(r,a)&&!t.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})}})(),(()=>{t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r)})(),(()=>{t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}})();var a={};t.r(a),t.d(a,{default:()=>O,findExistingByName:()=>T,isCompanyObject:()=>f,isExistingCompany:()=>g,isNewCompany:()=>E,normalizeCompanyValue:()=>Q});const n=require("@babel/runtime/helpers/extends");var o=t.n(n),s=t(2462),i=t.n(s),d=t(2015),u=t.n(d);const l=require("prop-types");var c=t.n(l);const p=require("@mui/material");var y=t(3582),_=t(1896);const m=["summitId","isRequired","sx","onChange","id","name","label","value","error","helperText","onBlur","placeholder","options2Show","disableShrink"],R=["key"],f=e=>!!e&&"object"==typeof e&&"string"==typeof e.name,g=e=>f(e)&&e.id>0,E=e=>f(e)&&0===e.id&&!!e.name.trim(),T=(e,r)=>{const t=null==r?void 0:r.trim().toLowerCase();return t&&(e||[]).find((e=>g(e)&&e.name.toLowerCase()===t))||null},Q=e=>e?"string"==typeof e?e.trim()?e:null:"object"==typeof e&&"string"==typeof e.name&&e.name.trim()?e:null:null,h=e=>{let{summitId:r,isRequired:t,sx:a,onChange:n,id:s,name:d,label:l,value:c,error:f,helperText:g,onBlur:h,placeholder:O,options2Show:S,disableShrink:w}=e,k=i()(e,m);const[b,v]=u().useState(""),[$,A]=u().useState([]),x=u().useMemo((()=>Q(c)),[c]),I=(0,_.default)((e=>{n({target:{id:d,value:e,type:"companyinput"}})}));return u().useEffect((()=>{if(""===b)return void A(x?[x]:[]);let e=!1;return(0,y.queryRegistrationCompanies)(r,b,(r=>{if(e)return;let t=[];if(x&&(t=[x]),r&&(t=[...t,...r]),A(t),E(x)){const e=T(r,x.name);e&&I(e)}}),S),()=>{e=!0}}),[x,b,r,S,I]),u().createElement(p.Autocomplete,o()({sx:a,id:s,name:d,options:$,autoComplete:!0,autoSelect:!0,freeSolo:!0,includeInputInList:!0,filterSelectedOptions:!0,value:x,onBlur:()=>{h&&h(d)},getOptionLabel:e=>"string"==typeof e?e:e.name,onChange:(e,r)=>{let t=r;if("string"==typeof t&&t.trim()){const e=t.trim();t=T($,e)||{id:0,name:e}}A(t?[t,...$.filter((e=>{var r;return(null==e?void 0:e.id)!==(null===(r=t)||void 0===r?void 0:r.id)}))]:$),n({target:{id:d,value:t,type:"companyinput"}})},onInputChange:(e,r)=>{v(r)},filterOptions:e=>e,renderInput:e=>u().createElement(p.TextField,o()({},e,{label:l,placeholder:O,fullWidth:!0,required:t,helperText:g,error:f,margin:"normal",InputLabelProps:w?{shrink:!1}:void 0})),renderOption:(e,r)=>{const{key:t}=e,a=i()(e,R),n="string"==typeof r?r:null==r?void 0:r.name;return u().createElement("li",o()({key:t},a),u().createElement(p.Typography,{variant:"body2",sx:{fontSize:"1em",color:"text.secondary",padding:"5px 0"}},n))}},k))};h.defaultProps={name:"GENERAL",label:"Company",options2Show:20,disableShrink:!1},h.propTypes={summitId:c().number.isRequired,value:c().oneOfType([c().string,c().object]),onChange:c().func.isRequired,isRequired:c().bool,name:c().string,error:c().bool,helperText:c().string,onBlur:c().func,options2Show:c().number,placeholder:c().string,label:c().string,disableShrink:c().bool};const O=h;return a})()));
2
2
  //# sourceMappingURL=company-input-v2.js.map