openstack-uicore-foundation 5.0.46-beta.5 → 5.0.46
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 +67 -0
- package/.claude/rules/openstack-uicore-foundation-project.md +69 -0
- package/.claude/rules/openstack-uicore-foundation-security.md +39 -0
- package/.claude/settings.local.json +1 -7
- package/.codegraph/.gitignore +5 -0
- package/.github/workflows/build_test.yml +1 -1
- package/.github/workflows/jest.yml +1 -1
- 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/docs/plans/2026-07-06-file-size-field-unit-prop.md +20 -0
- package/docs/plans/2026-07-30-node-22-migration.md +139 -0
- package/docs/plans/clickup-barrel-mui-dragalong.md +43 -0
- package/docs/plans/clickup-order-ledger-extraction.md +44 -0
- package/lib/components/clock-context.js.map +1 -1
- package/lib/components/clock.js.map +1 -1
- package/lib/components/extra-questions-mui.js.map +1 -1
- package/lib/components/extra-questions.js.map +1 -1
- package/lib/components/free-text-search.js.map +1 -1
- package/lib/components/index.js +1 -1
- package/lib/components/index.js.map +1 -1
- package/lib/components/inputs/access-levels-input.js.map +1 -1
- package/lib/components/inputs/attendee-input.js.map +1 -1
- package/lib/components/inputs/company-input-v2.js.map +1 -1
- package/lib/components/inputs/company-input.js.map +1 -1
- package/lib/components/inputs/country-dropdown.js.map +1 -1
- package/lib/components/inputs/country-input.js.map +1 -1
- package/lib/components/inputs/editor-input-v2.js.map +1 -1
- package/lib/components/inputs/event-input.js.map +1 -1
- package/lib/components/inputs/group-input.js.map +1 -1
- package/lib/components/inputs/language-input.js.map +1 -1
- package/lib/components/inputs/member-input.js.map +1 -1
- package/lib/components/inputs/organization-input.js.map +1 -1
- package/lib/components/inputs/promocode-input.js.map +1 -1
- package/lib/components/inputs/registration-company-input.js.map +1 -1
- package/lib/components/inputs/speaker-input.js.map +1 -1
- package/lib/components/inputs/sponsor-input.js +1 -1
- package/lib/components/inputs/sponsor-input.js.map +1 -1
- package/lib/components/inputs/summit-input.js.map +1 -1
- package/lib/components/inputs/tag-input.js.map +1 -1
- package/lib/components/inputs/textarea-input.js +1 -1
- package/lib/components/inputs/textarea-input.js.map +1 -1
- package/lib/components/inputs/ticket-types-input.js.map +1 -1
- package/lib/components/inputs/upload-input-v2.js +1 -1
- package/lib/components/inputs/upload-input-v2.js.map +1 -1
- package/lib/components/inputs/upload-input-v3.js +1 -1
- package/lib/components/inputs/upload-input-v3.js.map +1 -1
- package/lib/components/mui/addon-type-select.js +2 -0
- package/lib/components/mui/addon-type-select.js.map +1 -0
- package/lib/components/mui/auth-button.js +1 -1
- package/lib/components/mui/auth-button.js.map +1 -1
- package/lib/components/mui/drag-n-drop-list.js +1 -1
- package/lib/components/mui/drag-n-drop-list.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-v2.js +2 -0
- package/lib/components/mui/formik-inputs/additional-input-list-v2.js.map +1 -0
- 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-v2.js +2 -0
- package/lib/components/mui/formik-inputs/additional-input-v2.js.map +1 -0
- 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/addon-type-select.js +2 -0
- package/lib/components/mui/formik-inputs/addon-type-select.js.map +1 -0
- package/lib/components/mui/formik-inputs/company-input.js.map +1 -1
- package/lib/components/mui/formik-inputs/sponsor-input.js.map +1 -1
- package/lib/components/mui/formik-inputs/sponsorship-input.js.map +1 -1
- package/lib/components/mui/formik-inputs/sponsorship-summit-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/grid-filter.js.map +1 -1
- package/lib/components/mui/nav-bar.js +1 -1
- package/lib/components/mui/nav-bar.js.map +1 -1
- package/lib/components/mui/order-summary.js.map +1 -1
- package/lib/components/mui/sortable-table-v2.js +2 -0
- package/lib/components/mui/sortable-table-v2.js.map +1 -0
- package/lib/components/mui/sponsor-addon-select.js.map +1 -1
- package/lib/components/mui/sponsor-order-grid.js +1 -1
- package/lib/components/mui/sponsor-order-grid.js.map +1 -1
- package/lib/components/mui/stripe-payment.js.map +1 -1
- package/lib/components/mui/summits-dropdown.js.map +1 -1
- package/lib/components/mui/table/extra-rows.js.map +1 -1
- package/lib/components/mui/upload-dialog.js +1 -1
- package/lib/components/mui/upload-dialog.js.map +1 -1
- package/lib/components/order-invoice-pdf.js +1 -1
- package/lib/components/order-invoice-pdf.js.map +1 -1
- package/lib/components/schedule-builder-view.js +1 -1
- package/lib/components/schedule-builder-view.js.map +1 -1
- package/lib/components/sponsored-project-input.js.map +1 -1
- package/lib/components/summit-dropdown.js +1 -1
- package/lib/components/summit-dropdown.js.map +1 -1
- package/lib/components/table-editable.js +1 -1
- package/lib/components/table-editable.js.map +1 -1
- package/lib/components/table-selectable.js +1 -1
- package/lib/components/table-selectable.js.map +1 -1
- package/lib/components/table-sortable.js +1 -1
- package/lib/components/table-sortable.js.map +1 -1
- package/lib/css/components/extra-questions.css +1 -1
- package/lib/css/components/extra-questions.css.map +1 -1
- package/lib/css/components/free-text-search.css +1 -1
- package/lib/css/components/free-text-search.css.map +1 -1
- package/lib/css/components/index.css +1 -2
- package/lib/css/components/index.css.map +1 -1
- package/lib/css/components/mui/sortable-table-v2.css +2 -0
- package/lib/css/components/mui/sortable-table-v2.css.map +1 -0
- package/lib/i18n.js +1 -1
- package/lib/i18n.js.map +1 -1
- package/lib/security/actions.js +1 -1
- package/lib/security/actions.js.map +1 -1
- package/lib/utils/actions.js +1 -1
- package/lib/utils/actions.js.map +1 -1
- package/lib/utils/money.js +1 -1
- package/lib/utils/money.js.map +1 -1
- package/lib/utils/query-actions.js +1 -1
- package/lib/utils/query-actions.js.map +1 -1
- package/lib/utils/questions-set.js.map +1 -1
- package/package.json +9 -10
- package/lib/components/mui/formik-inputs/summit-addon-select.js +0 -2
- package/lib/components/mui/formik-inputs/summit-addon-select.js.map +0 -1
- package/lib/components/mui/summit-addon-select.js +0 -2
- package/lib/components/mui/summit-addon-select.js.map +0 -1
|
@@ -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`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MuiFormikFileSizeField — Selectable Storage Unit (Bytes/KB) Implementation Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-07-06
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Agent: Claude Code
|
|
6
|
+
Status: PENDING
|
|
7
|
+
Approved: No
|
|
8
|
+
Iterations: 0
|
|
9
|
+
Worktree: No
|
|
10
|
+
Type: Feature
|
|
11
|
+
|
|
12
|
+
> Planning in progress...
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
**Goal:** Add a prop to `MuiFormikFileSizeField` so the stored (Formik field) value can be expressed in Bytes or KB, while the input keeps displaying MB — allowing the same control to back APIs that use either unit.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
_Exploring codebase and gathering requirements..._
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Node 22 Migration Implementation Plan
|
|
2
|
+
|
|
3
|
+
Created: 2026-07-30
|
|
4
|
+
Author: smarcet@gmail.com
|
|
5
|
+
Agent: Claude Code
|
|
6
|
+
Status: VERIFIED
|
|
7
|
+
Approved: Yes
|
|
8
|
+
Iterations: 0
|
|
9
|
+
Worktree: No
|
|
10
|
+
Type: Feature
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
**Goal:** openstack-uicore-foundation builds, tests, and runs cleanly on Node 22 LTS (22.22.1), extrapolating the Node 18→22 migration from `fntechgit/summit-admin` PR #844 / ClickUp ticket 86b8zyqnf.
|
|
15
|
+
|
|
16
|
+
## Out of Scope
|
|
17
|
+
|
|
18
|
+
- **Literal SCSS/sass-loader removal.** The ticket tells summit-admin to delete `node-sass`, `sass-loader`, and all SCSS webpack/jest rules because their `.scss` was dead code. This lib's `.scss` is live (6 files: `circle-button`, `progressive-img`, `extra-questions`, `mui/NavBar`, `mui/AuthButton`, `schedule-print`), so instead of deleting SCSS support, `node-sass` is swapped for dart-sass (`sass`) — see Approach.
|
|
19
|
+
- **`fs`, `react-hot-loader` removal, `superagent`/`idtoken-verifier` version bumps.** None of these apply: this repo's `package.json` never had `fs` or `react-hot-loader`, and `superagent` (8.0.9) / `idtoken-verifier` (^2.2.2) are already the target versions the ticket asks summit-admin to bump *to* (this lib is the upstream source of those peer-dep versions).
|
|
20
|
+
- **webpack-cli v4→v5 bump, `webpack.dev.js` HTTPS devServer config, `.prettierrc` key rename.** This lib has no `serve` script (only `build-dev`/`build`, which invoke `webpack` directly, not `webpack serve`), so `webpack-cli`/devServer config is inert here — confirmed webpack-cli 4.9.2 already builds successfully on Node 22. No `.prettierrc` exists in this repo (prettier isn't installed).
|
|
21
|
+
- **ESLint upgrade.** Not applicable — this repo has no `eslint` dependency at all and no `lint` script (unlike summit-admin, which has both and defers its 8→9 migration to a separate ticket).
|
|
22
|
+
- **React 16→18, Redux 3→4 upgrades.** Explicitly out of scope in the source ticket too (separate tickets).
|
|
23
|
+
|
|
24
|
+
## Approach
|
|
25
|
+
|
|
26
|
+
**Chosen:** Direct config/dependency edits to `.nvmrc`, the two `.github/workflows/*.yml` files, `package.json`, and `webpack.common.js` — swapping `node-sass` for dart-sass rather than deleting SCSS support.
|
|
27
|
+
**Why:** Matches the upstream Node 22 migration's intent (clean dependency tree, no build tooling incompatible with Node 22) without regressing this library's actively-used SCSS components. Confirmed by reproducing the real blocker: `node-sass` 7.0.1 ships no Node 22 binary, so `sass-loader` silently returns `undefined` CSS and `webpack --config webpack.dev.js` fails with a PostCSS "received undefined instead of CSS string" error on `schedule-print/styles.module.scss`. Installing dart-sass (`sass@^1.77.0`) fixes this: `sass-loader` auto-prefers `sass` over `node-sass` with no config changes, and dart-sass's legacy `renderSync` API (what `sass-loader@12` uses) was verified against `circle-button/index.module.scss` directly — it renders correctly with no deprecation warnings at this version.
|
|
28
|
+
|
|
29
|
+
## Context for Implementer
|
|
30
|
+
|
|
31
|
+
Two Babel configs exist, same as noted in the source ticket: `babel.config.json` (used by Jest/babel-jest — already uses `@babel/plugin-transform-runtime`, unaffected) and inline `options.plugins` inside the `babel-loader` rule in `webpack.common.js:217-220` (used only by webpack). The `proposal`→`transform` plugin renames apply to `package.json` **and** `webpack.common.js` — missing either leaves a mismatched package name that webpack/babel can't resolve.
|
|
32
|
+
|
|
33
|
+
## Assumptions
|
|
34
|
+
|
|
35
|
+
- `sass@^1.77.0` (dart-sass) renders this repo's existing `.scss` files without syntax changes — verified directly against `circle-button/index.module.scss` via `sass.renderSync()`, the same legacy API `sass-loader@12.6.0` invokes by default. Task 2 depends on this holding for the other 5 `.scss` files too (all are equally simple: plain selectors, nesting, `&` — no `@import`, no Sass-specific at-rules that differ between node-sass and dart-sass). **Confirmed correct in Task 4**: `yarn build-dev` compiled all 6 `.scss` files to correct CSS (verified generated class names/rules in `lib/css/components/circle-button.css`, `mui/nav-bar.css`, etc.). One discovered difference from the isolated pre-approval test: `yarn.lock` resolved `sass` to `1.102.0` (latest satisfying `^1.77.0`, not the `1.77.8` tested directly), and going through `sass-loader`'s actual invocation (vs. calling `sass.renderSync()` raw) surfaced a `Deprecation The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0` warning during every build. Cosmetic (build exit code and CSS output were both correct), but resolved as a same-day follow-up: bumped `sass-loader` `^12.6.0` → `^14.2.1` (requires Node ≥18.12, satisfied) and added `api: 'modern'` to both SCSS loader rules in `webpack.common.js` (`.module.scss` and `.scss`). Re-verified: `yarn build-dev`/`yarn build` both compile with zero sass deprecation warnings, generated CSS is byte-identical in content to the legacy-API build (same class rules/counts across all 6 `.scss` files), and the full suite still passes 104/104 suites, 876/876 tests.
|
|
36
|
+
- Bumping `jest`/`jest-environment-jsdom` 28→29.7.0 together is compatible with `babel-jest`, `identity-obj-proxy`, `jest-transform-stub`, and `redux-mock-store` as currently configured — no snapshot tests exist in this repo (`find src -name '*.snap'` = 0 results), which removes the most common jest-major-bump breakage source. Task 3 depends on this; the full 876-test suite re-run in Task 4 is the check.
|
|
37
|
+
|
|
38
|
+
## Progress Tracking
|
|
39
|
+
|
|
40
|
+
- [x] Task 1: Bump Node version pins to 22.22.1 (.nvmrc, CI workflows)
|
|
41
|
+
- [x] Task 2: Migrate dependencies for Node 22 (babel plugin renames, node-sass → dart-sass, remove babel-cli)
|
|
42
|
+
- [x] Task 3: Bump jest + jest-environment-jsdom to 29.7.0
|
|
43
|
+
- [x] Task 4: Reinstall on Node 22 and verify full build/test pipeline (punycode warning not eliminated — documented, structural jest-29 limitation)
|
|
44
|
+
|
|
45
|
+
## Implementation Tasks
|
|
46
|
+
|
|
47
|
+
### Task 1: Bump Node version pins to 22.22.1
|
|
48
|
+
|
|
49
|
+
**Objective:** Pin the toolchain to Node 22 LTS (22.22.1, the version named in ClickUp ticket 86b8zyqnf) across the local dev environment and CI, mirroring the `node-version: 18` → `22` change in PR #844's three GitHub Actions workflows (this repo only has two of those three workflows).
|
|
50
|
+
|
|
51
|
+
**Files:**
|
|
52
|
+
|
|
53
|
+
- Modify: `.nvmrc`
|
|
54
|
+
- Modify: `.github/workflows/build_test.yml`
|
|
55
|
+
- Modify: `.github/workflows/jest.yml`
|
|
56
|
+
|
|
57
|
+
**Key Decisions / Notes:**
|
|
58
|
+
|
|
59
|
+
- `.nvmrc` currently reads `18.15.0` (`.nvmrc:1`) → `22.22.1`, matching the exact LTS patch the ticket pins for summit-admin.
|
|
60
|
+
- Both workflow files set `node-version: 18` under the `actions/setup-node@v3` step (`build_test.yml:13`, `jest.yml:13`) → `node-version: 22`, matching PR #844's style (major version only, not a full patch pin, for the `setup-node` action).
|
|
61
|
+
- `Trivial:` three one-line version-string edits, no logic change; covered by Task 4's full pipeline re-run (`.nvmrc`/workflow correctness can't be unit-tested directly — CI executing on Node 22 is the verification).
|
|
62
|
+
|
|
63
|
+
**Definition of Done:**
|
|
64
|
+
|
|
65
|
+
- [ ] `.nvmrc` contains `22.22.1`
|
|
66
|
+
- [ ] Both `.github/workflows/*.yml` files specify `node-version: 22`
|
|
67
|
+
- [ ] Verify: `grep -c "node-version: 22" .github/workflows/build_test.yml .github/workflows/jest.yml` → each file reports 1
|
|
68
|
+
|
|
69
|
+
### Task 2: Migrate dependencies for Node 22 (babel renames, node-sass → dart-sass, remove babel-cli)
|
|
70
|
+
|
|
71
|
+
**Objective:** Replace the Node-22-incompatible `node-sass` with dart-sass, rename the deprecated `@babel/plugin-proposal-*` packages to their `@babel/plugin-transform-*` equivalents in both places they're declared, and drop the unused `babel-cli` devDependency — the three dependency-level blockers/deprecations PR #844 addresses that actually apply to this repo.
|
|
72
|
+
|
|
73
|
+
**Files:**
|
|
74
|
+
|
|
75
|
+
- Modify: `package.json`
|
|
76
|
+
- Modify: `webpack.common.js`
|
|
77
|
+
|
|
78
|
+
**Key Decisions / Notes:**
|
|
79
|
+
|
|
80
|
+
- `package.json:78` — remove `"node-sass": "^7.0.1"`; add `"sass": "^1.77.0"` to `devDependencies` (alphabetically, near `redux-thunk`/`regenerator-runtime`). `sass-loader` (`node_modules/sass-loader/dist/utils.js:27-47`) auto-detects and prefers `sass` over `node-sass` when both/either are present — no `sass-loader` option changes needed.
|
|
81
|
+
- `package.json:18-20` — rename `@babel/plugin-proposal-class-properties` → `@babel/plugin-transform-class-properties`, `@babel/plugin-proposal-object-rest-spread` → `@babel/plugin-transform-object-rest-spread`, `@babel/plugin-proposal-optional-chaining` → `@babel/plugin-transform-optional-chaining` (same `^7.16.7`/`^7.17.3` version ranges — these are drop-in renames, confirmed via `npm view <proposal-pkg> deprecated` pointing at the `transform-*` replacement). This repo doesn't declare `@babel/plugin-proposal-nullish-coalescing-operator` (summit-admin's 4th renamed plugin), so there's nothing to rename for that one.
|
|
82
|
+
- `webpack.common.js:218-220` — same three renames inside the `babel-loader` rule's `options.plugins` array (mirrors `package.json`; see Context for Implementer above).
|
|
83
|
+
- `package.json:43` — remove `"babel-cli": "^6.26.0"` from `devDependencies`. Confirmed unused: no `babel-cli`/`babel-node` reference anywhere outside this one line (`grep -rn "babel-cli\|babel-node" --exclude-dir=node_modules --exclude-dir=lib .`).
|
|
84
|
+
- Do NOT touch `webpack.common.js:118-123` (the `.module.scss`/`.scss` loader rules) or the `package.json` `jest.moduleNameMapper`/`jest.transform` scss entries (`package.json:180`, `:190`) — see Out of Scope.
|
|
85
|
+
|
|
86
|
+
**Definition of Done:**
|
|
87
|
+
|
|
88
|
+
- [ ] `package.json` has no `node-sass` or `babel-cli` entries; has `"sass": "^1.77.0"` in devDependencies
|
|
89
|
+
- [ ] No `@babel/plugin-proposal-*` package name appears anywhere in `package.json` or `webpack.common.js`
|
|
90
|
+
- [ ] Verify: `grep -rn "plugin-proposal\|node-sass\|babel-cli" package.json webpack.common.js` returns no matches
|
|
91
|
+
|
|
92
|
+
### Task 3: Bump jest + jest-environment-jsdom to 29.7.0
|
|
93
|
+
|
|
94
|
+
**Objective:** Upgrade `jest` and `jest-environment-jsdom` together from `^28.1.0` to `^29.7.0` so their internal API versions stay paired, eliminating the `punycode` module deprecation warning that Node 22 emits when running the test suite under jest 28's older jsdom (^19.0.0 → ^20.0.0).
|
|
95
|
+
|
|
96
|
+
**Files:**
|
|
97
|
+
|
|
98
|
+
- Modify: `package.json`
|
|
99
|
+
|
|
100
|
+
**Key Decisions / Notes:**
|
|
101
|
+
|
|
102
|
+
- `package.json:63-64` — `"jest": "^28.1.0"` → `"^29.7.0"`, `"jest-environment-jsdom": "^28.1.0"` → `"^29.7.0"`. Bumping only `jest-environment-jsdom` (the ticket's literal instruction, written for summit-admin where jest core was already 29) would pair a jest-28 core with a jest-29 environment package — an unsupported combination since `jest-environment-jsdom` peer-requires matching `@jest/environment`/`@jest/types` major versions.
|
|
103
|
+
- No snapshot tests exist in this repo (verified: `find src -name '*.snap'` → 0 results), which is the most common jest-major-bump breakage source — reduces migration risk.
|
|
104
|
+
- `package.json:70-71` also declares standalone `jsdom": "^19.0.0"` and `jsdom-global": "^3.0.2"` devDependencies, separate from the jsdom that `jest-environment-jsdom` pulls in transitively. Confirmed unused: no `require('jsdom')`/`require('jsdom-global')` anywhere in `src` (only `@jest-environment jsdom` docblocks, which reference jest's environment config, not these packages directly) — leave both untouched; they're pre-existing dead deps outside this migration's lineage, not a version-mismatch risk.
|
|
105
|
+
- **Before bumping**, capture a baseline for the known-flaky `src/components/mui/__tests__/mui-sponsor-input.test.js` "debounces API calls" fake-timer test on jest 28: run it isolated 3x with `--runInBand` and note the failure mode when it flakes (expected: `toHaveBeenCalledWith("Spo", ...)` receives `"Sp"` — a keystroke-timing race, not a timer-count or assertion-shape error). Jest 29 changed its fake-timers internals (`@sinonjs/fake-timers`), so a post-bump flake must be compared against this baseline failure mode, not assumed to be "the same pre-existing flake" by default. **Result:** 3/3 isolated `--runInBand` runs passed cleanly on jest 28 — confirms the flake is worker-parallelism contention, not deterministic.
|
|
106
|
+
- `Trivial:` two version-string bumps in `devDependencies`; the behavioral verification is Task 4's full suite re-run, not a new unit test.
|
|
107
|
+
- **Found during changes-review (verification phase):** `babel-jest` (`package.json:43`) was left at `^28.1.0` while `jest`/`jest-environment-jsdom` bumped to `^29.7.0` — a version skew flagged because `jest@^29.7.0` needs `babel-jest@^29.7.0` internally, and `yarn.lock` was resolving both `babel-jest@28.1.0` (our stale direct pin) and `babel-jest@29.7.0` (jest's transitive need) simultaneously. Tests passed either way (876/876), but bumped `babel-jest` to `^29.7.0` for consistency with the rest of the jest-family bump and to remove the duplicate install.
|
|
108
|
+
- **Objective partially not achievable within this task's scope, discovered during Task 4 verification:** the punycode warning is NOT eliminated by this bump. Root cause traced with `node --trace-deprecation`: the warning originates from `tr46@3.0.0` (`node_modules/tr46/index.js`, `require('punycode')`), pulled in by `whatwg-url@^11.0.0`, which is what `jsdom@^20.0.0` depends on. `jest-environment-jsdom@29.7.0` — the latest 29.x release, confirmed via `npm view jest-environment-jsdom versions` — permanently pins `jsdom: ^20.0.0` (verified via `npm view jest-environment-jsdom@29.7.0 dependencies.jsdom`); that pin never changes within the 29.x line. `tr46` only stops requiring Node's deprecated built-in `punycode` module (switching to the npm `punycode` package instead) starting at `tr46@5` / `whatwg-url@14`, which first ships with `jsdom@26.1.0` — pulled in only by `jest-environment-jsdom@30.x`, which requires jest 30 core. **This means the punycode warning cannot be eliminated while staying on jest 29** — only a jest 30 upgrade (paired jest + jest-environment-jsdom + jsdom major bump, materially larger and riskier than what this plan's approach question covered) would fix it. Not implemented here: it's a bigger, unreviewed scope expansion beyond the approved "bump jest+jest-environment-jsdom to 29.7.0 together" decision. Flagged as a follow-up for a separate jest 30 migration plan.
|
|
109
|
+
|
|
110
|
+
**Definition of Done:**
|
|
111
|
+
|
|
112
|
+
- [ ] `package.json` declares `jest` and `jest-environment-jsdom` both at `^29.7.0`
|
|
113
|
+
- [ ] Isolated baseline run of `mui-sponsor-input.test.js` on jest 28 recorded (pass, or flake with the `"Sp"` vs `"Spo"` failure mode) before the bump
|
|
114
|
+
- [ ] Verify: `grep -E '"jest(-environment-jsdom)?": "\^29\.7\.0"' package.json` matches both lines
|
|
115
|
+
|
|
116
|
+
### Task 4: Reinstall on Node 22 and verify full build/test pipeline
|
|
117
|
+
|
|
118
|
+
**Objective:** Reinstall dependencies from scratch under Node 22.22.1 (regenerating `yarn.lock`) and confirm the full build and test pipeline is clean — no `node-sass`/PostCSS build errors, and the complete test suite passes — closing out the migration end to end, mirroring PR #844's final "verify full pipeline on Node 22" step. (The `punycode` warning is NOT eliminated — see Task 3's Key Decisions for the traced root cause and why it's out of this plan's scope.)
|
|
119
|
+
|
|
120
|
+
**Files:**
|
|
121
|
+
|
|
122
|
+
- Modify: `yarn.lock` (regenerated by `yarn install`, not hand-edited)
|
|
123
|
+
|
|
124
|
+
**Key Decisions / Notes:**
|
|
125
|
+
|
|
126
|
+
- Use Node 22.22.1 for this verification (matches the `.nvmrc` pin from Task 1): `export PATH="$HOME/.nvm/versions/node/v22.22.1/bin:$PATH"` (already available locally via nvm in this environment).
|
|
127
|
+
- `rm -rf node_modules && yarn install` — clean reinstall so `yarn.lock` reflects the Task 2/3 dependency changes (added `sass`, removed `node-sass`/`babel-cli`, bumped babel/jest packages) rather than a stale partial resolution.
|
|
128
|
+
- This repo has no `lint` or `serve` script (unlike summit-admin), so the equivalent verification surface is `build-dev`, `build`, and `test` only.
|
|
129
|
+
- A pre-existing flaky test (`src/components/mui/__tests__/mui-sponsor-input.test.js`, the `debounces API calls` fake-timer test) intermittently fails under jest's default parallel workers on **both** Node 18 and Node 22 — reproduced independently of this migration. Not caused by or fixed as part of this plan; if it flakes during verification, re-run with `--runInBand` to confirm it's the pre-existing flake and not a real regression.
|
|
130
|
+
- The "876/876 passing" figure in the DoD below is the pre-migration baseline captured during planning: `yarn jest --runInBand` on both Node 18.15.0 and Node 22.22.1 (pre-Task-2/3 dependency state) reported "Test Suites: 104 passed, 104 total / Tests: 876 passed, 876 total". Post-migration, compare against this baseline rather than treating 876 as a hardcoded target — a suite/test count drift (not just a pass/fail count) would indicate a collection-level regression (e.g. a test file failing to load under jest 29) that a bare pass-count comparison could mask.
|
|
131
|
+
- **Deviation found and fixed during this task's verification:** the first post-migration full-suite run failed to *collect* `src/utils/__tests__/actions.test.js` — "Cannot find module 'request'" — a suite/test-count drift exactly matching the scenario the note above warns about. Root cause: `node-sass@^7.0.1` (removed in Task 2) transitively depended on the npm package `request@^2.88.0` (used by its binary-downloader install script); nothing in `src` or this test file ever imports that package (confirmed: `grep` for `require('request')`/`from 'request'` across `src` returns nothing). The test file's `jest.mock('request')` (`actions.test.js:10`, now removed) was pre-existing dead code that only avoided "Cannot find module" by accident, because `request` happened to be resolvable via node-sass's transitive tree — it was never functionally connected to the test (the test's real HTTP client is `superagent/lib/client`, imported under the unrelated local name `request`). Removed the stale `jest.mock('request')` line at the root cause; not a jest 28→29 behavior change.
|
|
132
|
+
|
|
133
|
+
**Definition of Done:**
|
|
134
|
+
|
|
135
|
+
- [x] `yarn install` completes with exit 0 on Node 22.22.1 (no `node-sass` build/postinstall errors)
|
|
136
|
+
- [x] `yarn build-dev` and `yarn build` both complete with exit 0 and no PostCSS "received undefined instead of CSS string" errors
|
|
137
|
+
- [x] `yarn test` reports 876/876 passing (0 failures) — matches the pre-migration baseline exactly (no suite/test count drift)
|
|
138
|
+
- [~] `punycode` deprecation warning: NOT eliminated (root cause traced and documented in Task 3 — structurally impossible on jest 29; needs a separate jest 30 migration). Original DoD wording ("no punycode warning") is not met; superseded by the Task 3 note above.
|
|
139
|
+
- [x] Verify (run): `node -v` (22.22.1) `&& rm -rf node_modules && yarn install && yarn build-dev && yarn build && yarn test --runInBand 2>&1 | tee /tmp/node22-verify.log` → `yarn install` exit 0, both builds exit 0 with no PostCSS "undefined" error, `104 passed, 104 total` suites / `876 passed, 876 total` tests. One regression found and fixed mid-verification: `src/utils/__tests__/actions.test.js` failed to load ("Cannot find module 'request'") because `node-sass` (removed in Task 2) was the transitive source of the `request` npm package that the test's stale, unused `jest.mock('request')` call depended on existing — removed the dead mock line at the root cause (see Task 4 Key Decisions above); suite is green after the fix.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
GOAL
|
|
2
|
+
|
|
3
|
+
Current state: The components barrel (src/components/index.js) exports UploadInputV3 (which imports @mui/material and @mui/icons-material) uncommented, along with 70 additional Mui* components. Any consumer importing from openstack-uicore-foundation/lib/components (the barrel) is forced to resolve @mui/material, @mui/icons-material, @mui/x-date-pickers, spark-md5, dropzone and react-dropzone as installed peerDependencies, even if it uses no MUI component at all. This broke track-chairs build when bumping from 4.2.24 to 4.2.34 (UploadInputV3 was added in 4.2.25), and the same pattern is present on main (v5.x), made worse because the barrel now has 70 uncommented Mui exports (0 in the version right before 4.2.25).
|
|
4
|
+
|
|
5
|
+
Target state: Consumers that don't use MUI (like track-chairs) can update the library without being forced to install the full MUI + spark-md5 + dropzone stack, following the convention already documented in this repo (No barrel imports from lib/) and the pattern already applied to TextEditorV3, CompanyInputV2, MuiStripePayment, etc. (commented out of the barrel but with a standalone webpack entry).
|
|
6
|
+
|
|
7
|
+
TASKS
|
|
8
|
+
|
|
9
|
+
- Audit which of summit-admin's 584 library imports use the barrel (openstack-uicore-foundation/lib/components) vs individual paths, and confirm the 13 barrel imports found don't depend on UploadInputV3/Mui* staying exposed there (summit-admin already uses MUI anyway, low risk for that consumer)
|
|
10
|
+
- Comment out the UploadInputV3 export from src/components/index.js, following the exact same pattern already used for TextEditorV3 (move it under the "// these include 3rd party deps" block); keep its standalone webpack entry (webpack.common.js) untouched so direct imports keep working
|
|
11
|
+
- Fix the 2 summit-admin files that import UploadInputV3 from the barrel before/alongside the release: src/components/upload-dialog/index.js line 26, and src/pages/companies/components/company-dialog.js line 36 (used at lines 481 and 502) — switch both to import from openstack-uicore-foundation/lib/components/inputs/upload-input-v3 directly
|
|
12
|
+
- Update .claude/rules/openstack-uicore-foundation-project.md and the README if the recommended import convention changes
|
|
13
|
+
- Communicate the correct import path to consumers (track-chairs, summit-admin, etc.) to avoid the MUI/spark-md5/dropzone drag-along
|
|
14
|
+
|
|
15
|
+
ACCEPTANCE CRITERIA
|
|
16
|
+
|
|
17
|
+
- A consumer that only imports non-MUI components from openstack-uicore-foundation/lib/components/(individual path) can run yarn install and build without having @mui/material, @mui/icons-material, @mui/x-date-pickers, spark-md5, dropzone or react-dropzone installed
|
|
18
|
+
- The library's webpack build still generates individual entries for each component, including upload-input-v3, without breaking consumers that import it directly (summit-admin)
|
|
19
|
+
- The import convention is documented consistently across README, CLAUDE.md and the actual code
|
|
20
|
+
- No regression in existing tests (jest) or in the library build (yarn build)
|
|
21
|
+
|
|
22
|
+
DEVELOPMENT NOTES
|
|
23
|
+
|
|
24
|
+
Key files:
|
|
25
|
+
src/components/index.js, the main barrel, contains the 70 uncommented Mui* exports and the UploadInputV3 export on line 8
|
|
26
|
+
webpack.common.js, multi-entry build, check the upload-input-v3 and editor-input-v3 entries as reference for the pattern already in use
|
|
27
|
+
src/utils/crypto.js, imports spark-md5 unconditionally, used by dropzone/index.js and by security/methods.js via getSHA256/getRandomBytes even though methods.js never calls getMD5
|
|
28
|
+
package.json, @mui/material, @mui/icons-material, @mui/x-date-pickers, spark-md5, dropzone and react-dropzone are in peerDependencies, not dependencies, consistent with the project's peer deps model
|
|
29
|
+
|
|
30
|
+
Gotchas:
|
|
31
|
+
spark-md5 is not new to UploadInputV3, it was added in v4.2.17 (commit f4474fa) via dropzone/index.js for V1/V2. The real regression in 4.2.25 was that UploadInputV3 was the first component to actually reference @mui/material from the barrel (previously declared as a peer dep but not imported by anything reachable from there) and to add @mui/icons-material as a new peer dep
|
|
32
|
+
On v5.x main the barrel already has 70 uncommented Mui* exports vs 0 before 4.2.25 (all pre-existing, out of scope here), so commenting out UploadInputV3 removes the spark-md5/icons-material-specific drag-along this ticket targets, but a fully MUI-free consumer importing anything else from the barrel still needs MUI because of those 70 exports — tracked separately if it becomes a problem
|
|
33
|
+
summit-admin (5.0.40) already uses MUI extensively, out of 584 imports of the library only 13 use the full barrel (includes UploadInputV3 and MuiFormikUpload), the rest already follow the documented individual-path convention
|
|
34
|
+
track-chairs is the consumer that broke on the 4.2.34 bump, its code was not reviewed in this session, the information about its import pattern comes from the user's initial report
|
|
35
|
+
|
|
36
|
+
Risks:
|
|
37
|
+
Risk: removing UploadInputV3 from the barrel is a breaking change for any consumer relying on that barrel import. Confirmed concretely: summit-admin imports UploadInputV3 from the barrel in src/components/upload-dialog/index.js (line 26) and src/pages/companies/components/company-dialog.js (line 36, used at lines 481 and 502) — both break the moment this ships
|
|
38
|
+
Mitigation: keep the standalone webpack entry, which already exists, and document the alternative import path before removing it from the barrel, communicating the change in release notes. Land the summit-admin import fix in the same window as the openstack-uicore-foundation release (or before it, since importing from the individual path already works today on 5.0.40) to avoid a broken window
|
|
39
|
+
|
|
40
|
+
Out of scope:
|
|
41
|
+
track-chairs code is not investigated in this session, it is outside this repo
|
|
42
|
+
The MUI v9 migration (branch feature/5.x-react-17-x-upgrade, tag v6.0.0-beta.1) is not addressed, it is a separate effort
|
|
43
|
+
This session did not modify code, investigation only
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Extract shared order ledger derivation (SponsorOrderGrid / order-invoice-pdf) into utils/order-ledger
|
|
2
|
+
|
|
3
|
+
GOAL
|
|
4
|
+
|
|
5
|
+
Current state: the order ledger derivation (ordered rows plus running balance: item +, discount -, fee +, payment -, refund +, payments and refunds interleaved by created) exists twice in openstack-uicore-foundation. SponsorOrderGrid (mapOrderData + calculateBalance) renders it on screen, and order-invoice-pdf buildRows renders it on the invoice PDF. The two copies must stay in sync by hand and have already diverged in three places: quantity filter (grid drops items with missing quantity, PDF defaults them to 1), fee row keys (grid uses fee.id, which is undefined on purchases-api v2 payloads; PDF already uses line_id), and zero-amount discount rows (grid renders them, PDF skips them). These are the numbers a sponsor sees on screen and on their invoice; a future change to one copy is unlikely to reach the other, and the failure mode is a silent mismatch between the two surfaces rather than an error.
|
|
6
|
+
|
|
7
|
+
Target state: a single pure derivation module (src/utils/order-ledger.js) consumed by both renderers, leaving only presentation (MUI vs react-pdf) per consumer. Ref: PR 282 review thread discussion_r3683971969. Pattern reference: summit-admin-reuse-before-build, Duplicate Mapping Tables ("two copies of the same mapping drift silently when one gets updated and the other doesn't").
|
|
8
|
+
|
|
9
|
+
TASKS
|
|
10
|
+
|
|
11
|
+
- Create src/utils/order-ledger.js exporting a pure buildOrderLedger(order) that takes the raw PurchaseV2 payload and returns ordered entries (item, discount, fee, payment, refund, note) carrying raw fields, amounts in cents and a precomputed running balanceCents per entry; no i18n, no date or currency formatting, no react-pdf or MUI imports
|
|
12
|
+
- Centralize the ledger rules in that module: sign conventions, payments and refunds interleaved by created, quantity filter (quantity default 1, exclude 0), row keys from line_id
|
|
13
|
+
- Refactor order-invoice-pdf buildRows into a thin mapper from ledger entries to presentational rows (T.translate labels, currencyAmountFromCents, formatDate with LOC), and drop the now-unused summit parameter
|
|
14
|
+
- Refactor SponsorOrderGrid to consume ledger entries: replace mapOrderData and the mutable calculateBalance accumulator with entry.balanceCents; adopt line_id-based fee row keys and skip zero-amount discount rows
|
|
15
|
+
- Migrate the existing buildRows unit tests to order-ledger tests with raw cents expectations; keep the PDF mapper and render tests; keep SponsorOrderGrid tests green and add assertions for the three behavior fixes
|
|
16
|
+
- Verify sponsor-services grid path still works: normalizeOrder spreads the raw payload so raw fields survive, confirm the order object handed to the grid still carries them
|
|
17
|
+
|
|
18
|
+
ACCEPTANCE CRITERIA
|
|
19
|
+
|
|
20
|
+
- src/utils/order-ledger.js exists, exports buildOrderLedger, and imports nothing from @react-pdf/renderer, @mui or i18n-react
|
|
21
|
+
- The invoice PDF and SponsorOrderGrid produce the same rows and the same running balance for the same raw v2 order, asserted with one shared fixture run against both consumers in the same test suite
|
|
22
|
+
- buildRows no longer takes a summit parameter and all invoice dates remain in LOC
|
|
23
|
+
- SponsorOrderGrid fee rows get unique keys from line_id with a v2 payload (no fee-row-undefined keys)
|
|
24
|
+
- SponsorOrderGrid renders no discount row when discount_in_cents is 0
|
|
25
|
+
- jest passes with 0 failures across the order-invoice-pdf, SponsorOrderGrid and utils suites
|
|
26
|
+
- No visual regression on the sponsor order screen and on the invoice PDF for an order containing items, a discount, fees, payments, refunds and a cancelled item
|
|
27
|
+
|
|
28
|
+
DEVELOPMENT NOTES
|
|
29
|
+
|
|
30
|
+
Key files:
|
|
31
|
+
- src/utils/order-ledger.js (new) - pure derivation module
|
|
32
|
+
- src/components/order-invoice-pdf/helpers.js - buildRows becomes a thin presentational mapper
|
|
33
|
+
- src/components/mui/SponsorOrderGrid/index.js - mapOrderData, calculateBalance and the row rendering loop
|
|
34
|
+
- src/components/order-invoice-pdf/__tests__/order-invoice-pdf.test.js - buildRows tests migrate to the ledger
|
|
35
|
+
|
|
36
|
+
Gotchas:
|
|
37
|
+
- SponsorOrderGrid is published and consumed by sponsor-services; behavior must be preserved except for the three documented fixes
|
|
38
|
+
- normalizeOrder in sponsor-services spreads the raw payload and only adds props (addon_name, discount, amount), so the ledger can read raw fields on both raw and normalized orders; the ledger must never read the added formatted props
|
|
39
|
+
- The grid balance today is a mutable accumulator evaluated in JSX render order; replacing it with precomputed balanceCents also removes a re-render fragility
|
|
40
|
+
- purchases-api v2 fees carry line_id, position, title and amount only - never id
|
|
41
|
+
|
|
42
|
+
Risk: silent number mismatch between screen and invoice if only one consumer is migrated. Mitigation: one shared fixture asserted against both consumers in the same test run, and both consumers migrated in the same PR.
|
|
43
|
+
|
|
44
|
+
Out of scope: sponsor-services normalizeOrder adopting getOrderTotal from uicore utils/money (separate small change), any visual redesign of the grid or the PDF, react-pdf version work.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components/clock-context.js","mappings":"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","sources":["webpack://openstack-uicore-foundation/webpack/universalModuleDefinition","webpack://openstack-uicore-foundation/./src/components/clock.js","webpack://openstack-uicore-foundation/./src/components/fragment-parser.js","webpack://openstack-uicore-foundation/external commonjs \"use-sync-external-store/shim\"","webpack://openstack-uicore-foundation/external commonjs \"use-sync-external-store/shim/with-selector\"","webpack://openstack-uicore-foundation/./src/utils/external-store.js","webpack://openstack-uicore-foundation/./src/utils/methods.js","webpack://openstack-uicore-foundation/external commonjs \"@babel/runtime/helpers/defineProperty\"","webpack://openstack-uicore-foundation/external commonjs \"moment-timezone\"","webpack://openstack-uicore-foundation/external commonjs \"react\"","webpack://openstack-uicore-foundation/external commonjs \"urijs\"","webpack://openstack-uicore-foundation/webpack/bootstrap","webpack://openstack-uicore-foundation/webpack/runtime/compat get default export","webpack://openstack-uicore-foundation/webpack/runtime/define property getters","webpack://openstack-uicore-foundation/webpack/runtime/hasOwnProperty shorthand","webpack://openstack-uicore-foundation/webpack/runtime/make namespace object","webpack://openstack-uicore-foundation/./src/components/clock-context.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"openstack-uicore-foundation\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"openstack-uicore-foundation\"] = factory();\n\telse\n\t\troot[\"openstack-uicore-foundation\"] = factory();\n})(this, () => {\nreturn ","/**\n * Copyright 2020 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\nimport React from 'react';\nimport moment from \"moment-timezone\";\nimport FragmentParser from \"./fragment-parser\";\nimport {getTimeServiceUrl} from '../utils/methods';\n\n/**\n * class Clock\n */\nclass Clock extends React.Component {\n\n constructor(props) {\n super(props);\n this.fragmentParser = new FragmentParser();\n this.interval = null;\n this.state = {\n timestamp: null,\n manualSet : false,\n }\n this._isMounted = false;\n this.onVisibilityChange = this.onVisibilityChange.bind(this);\n }\n\n /**\n * @param response\n * @param localBefore\n */\n processServerTimeResponse = (response, localBefore) => {\n const localAfter = moment().unix();\n let timestamp = localAfter;\n if (response) {\n timestamp = response.timestamp + (localAfter - localBefore);\n }\n if(this._isMounted) {\n console.log(`Clock::processServerTimeResponse setting timestamp ${timestamp}`)\n this.setState({timestamp});\n }\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n processServerTimeResponseError = () => {\n const localAfter = moment().unix();\n let timestamp = localAfter;\n if(this._isMounted) {\n console.log(`Clock::processServerTimeResponseError setting timestamp ${timestamp}`)\n this.setState({timestamp});\n }\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n componentDidMount() {\n this._isMounted = true;\n const {timezone = 'UTC', now} = this.props;\n const nowQS = this.fragmentParser.getParam('now');\n const momentQS = moment.tz(nowQS, 'YYYY-MM-DD,hh:mm:ss', timezone);\n let timestamp = null;\n let manualSet = false;\n if (momentQS.isValid()) {\n timestamp = momentQS.valueOf() / 1000;\n console.log(`Clock::componentDidMount nowQS ${nowQS} is valid setting timestamp ${timestamp}`);\n manualSet = true;\n } else if (now) {\n timestamp = now\n } else {\n const localBefore = moment().unix();\n this.getServerTime()\n .then((response) => this.processServerTimeResponse(response, localBefore))\n .catch(() => this.processServerTimeResponseError())\n }\n\n if(timestamp) {\n this.setState({timestamp, manualSet});\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n this.interval = setInterval(this.tick, 1000);\n document.addEventListener(\"visibilitychange\", this.onVisibilityChange, false)\n }\n\n onVisibilityChange() {\n const visibilityState = document.visibilityState;\n const {manualSet} = this.state;\n if (visibilityState === \"visible\") {\n console.log(`Clock::onVisibilityChange manualSet ${manualSet}`)\n if(manualSet) return;\n const localBefore = moment().unix();\n this.getServerTime()\n .then((response) => this.processServerTimeResponse(response, localBefore))\n .catch(() => this.processServerTimeResponseError());\n }\n }\n\n componentWillUnmount() {\n this._isMounted = false;\n clearInterval(this.interval);\n document.removeEventListener(\"visibilitychange\", this.onVisibilityChange)\n this.interval = null;\n }\n\n getServerTime = () => {\n const timeServiceUrl = getTimeServiceUrl();\n return fetch(`${timeServiceUrl}`).then(async (response) => {\n if (response.status === 200) {\n return response.json();\n }\n return Promise.reject(null);\n })\n .catch(err => {\n console.log(err);\n return Promise.reject(err);\n });\n };\n\n tick = () => {\n const {timestamp} = this.state;\n if (timestamp !== null) {\n if(this.props.onTick)\n this.props.onTick(timestamp + 1);\n if(this._isMounted)\n this.setState({timestamp: timestamp + 1})\n }\n };\n\n // epoch utc time in seconds\n now = () => {\n return this.state.timestamp;\n };\n\n render() {\n const {display, timezone = 'UTC'} = this.props;\n const {timestamp} = this.state;\n\n if (!display || !timestamp) return null;\n\n return (\n <div style={{marginTop: '50px', textAlign: 'center', fontSize: '20px'}}>\n {moment.tz(timestamp * 1000, timezone).format('YYYY-MM-DD hh:mm:ss')}\n </div>\n );\n }\n\n}\n\nexport default Clock;\n","\nexport default class FragmentParser {\n\n constructor(){\n this.originalHash = '';\n this.hash = {};\n }\n\n convertToHash(strHash)\n {\n strHash = strHash.substr(1).toLowerCase();\n let params = strHash.split('&');\n let res = {};\n for(let param of params)\n {\n param = param.split('=');\n if(param.length !== 2) continue;\n let val = param[1].trim();\n if(val === '') continue;\n\n if (val === 'true' || val === 'false')\n param[1] = val == 'true';\n\n res[param[0]] = param[1];\n }\n return res;\n }\n\n clearParams(){\n this.originalHash = '';\n this.hash = {};\n }\n\n getParam(key){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n\n if(!this.hash.hasOwnProperty(key) ) return null;\n return this.hash[key];\n }\n\n getParams(){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n\n return { ... this.hash };\n }\n\n deleteParam(param){\n var hash = this.getParams();\n this.clearParams();\n for(let key in hash) {\n if(key == param) continue;\n this.hash[key] = hash[key];\n }\n }\n\n deleteParams(params){\n var hash = this.getParams();\n this.clearParams();\n for(let key in hash) {\n if(params.includes(key)) continue;\n this.hash[key] = hash[key];\n }\n }\n\n setParam(key, value){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n if(value !== null && value !== '')\n this.hash[key] = value;\n else\n delete this.hash[key];\n return this;\n }\n\n serialize(){\n let res = '';\n for(let key in this.hash)\n {\n let val = this.hash[key];\n if(res !== '') res += '&';\n res += key+'='+val;\n }\n return res;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"use-sync-external-store/shim\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"use-sync-external-store/shim/with-selector\");","/**\n * Copyright 2026 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * createExternalStore - Factory for creating React-optimized external stores.\n *\n * Problem:\n * When a frequently-updating data source (clock, WebSocket, polling, etc.)\n * pushes its value into shared state (global store, lifted useState, or a\n * context value), every consuming component re-renders on every update,\n * even when they only care about a derived condition that rarely changes.\n *\n * Solution:\n * createExternalStore() returns a Provider and hooks that store the value\n * in a ref (no re-renders) and use useSyncExternalStore so components\n * can opt in to updates selectively:\n *\n * - useValue() → re-renders on every update\n * - useSelector(compute, isEqual) → re-renders only when the computed result changes\n *\n * Components that don't call either hook are never affected by updates.\n *\n * How it works:\n * 1. The Provider stores the value in a ref (writing to a ref never triggers\n * a React re-render) and keeps a Set of listener callbacks.\n * 2. When emit(value) is called, the ref is updated and all listeners are\n * notified. These listeners come from useSyncExternalStore.\n * 3. useSyncExternalStore (React 18, shimmed for 16/17) calls getSnapshot()\n * to read the ref, compares with the previous value, and only re-renders\n * the component if the value changed.\n * 4. useMemo adds a layer on top: it runs a compute function on the raw value\n * and only re-renders if the computed result changed (checked via isEqual).\n *\n * API:\n * createExternalStore(name) returns:\n *\n * - Provider Wraps your component tree. Pass children as a render function\n * to receive the emit callback: (emit) => JSX. Call emit(value)\n * each time your data source has a new value.\n *\n * - useValue() Returns the latest emitted value. The component re-renders\n * on every emit.\n *\n * - useSelector(compute, isEqual?)\n * Returns a derived value. compute(rawValue) runs on every emit,\n * but the component only re-renders when isEqual returns false\n * (default: ===). Useful when you need to derive something that\n * changes less frequently than the raw value.\n *\n * The name parameter is used in error messages. For example,\n * createExternalStore('Clock') throws \"Clock hooks must be used within\n * their Provider\" when a hook is called outside the Provider.\n *\n * For clock-specific usage:\n * A pre-built clock store is available at:\n * import { ClockProvider, useClock, useClockSelector } from 'openstack-uicore-foundation/lib/components/clock-context';\n * This wires createExternalStore to the Clock component so projects don't\n * have to repeat that boilerplate.\n *\n * Custom store example:\n * import { createExternalStore } from 'openstack-uicore-foundation/lib/utils/external-store';\n *\n * const { Provider, useValue, useSelector } = createExternalStore('WebSocket');\n *\n * const WebSocketProvider = ({ url, children }) => (\n * <Provider>\n * {(emit) => (\n * <>\n * <WebSocketSource url={url} onMessage={emit} />\n * {children}\n * </>\n * )}\n * </Provider>\n * );\n *\n * // Re-renders on every message:\n * const message = useValue();\n *\n * // Re-renders only when the derived value changes:\n * const isActive = useSelector((msg) => msg?.status === 'active');\n **/\n\nimport React, { createContext, useContext, useRef, useCallback, useMemo as reactUseMemo } from 'react';\n// Shim for React 16/17 compatibility, falls back to native in React 18+\nimport { useSyncExternalStore } from 'use-sync-external-store/shim';\nimport { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';\n\nconst strictEqual = (a, b) => a === b;\n\n/**\n * Creates an external store with a Provider and subscription hooks.\n *\n * @param {string} name - Store name, used in error messages (e.g., \"Clock\", \"WebSocket\")\n * @returns {{ Provider, useValue, useSelector }}\n */\nexport function createExternalStore(name = 'ExternalStore') {\n const Context = createContext(null);\n\n const useStoreContext = () => {\n const context = useContext(Context);\n if (context === null) {\n throw new Error(`${name} hooks must be used within their Provider`);\n }\n return context;\n };\n\n /**\n * Provider - Wraps your component tree and provides the store.\n *\n * Pass children as a render function to receive the `emit` callback:\n * <Provider>{(emit) => <Source onUpdate={emit} />}</Provider>\n *\n * Or pass children normally if you wire emit externally.\n *\n * Pass `initialValue` to seed the store synchronously so that hooks\n * called before the first emit see a real value instead of null.\n */\n const Provider = ({ initialValue = null, children }) => {\n const valueRef = useRef(initialValue);\n const listenersRef = useRef(new Set());\n\n const subscribe = useCallback((callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n }, []);\n\n const getSnapshot = useCallback(() => valueRef.current, []);\n\n const emit = useCallback((value) => {\n valueRef.current = value;\n listenersRef.current.forEach(listener => listener());\n }, []);\n\n const contextValue = reactUseMemo(() => ({ subscribe, getSnapshot }), [subscribe, getSnapshot]);\n\n return (\n <Context.Provider value={contextValue}>\n {typeof children === 'function' ? children(emit) : children}\n </Context.Provider>\n );\n };\n\n /**\n * useValue - Subscribe to every update.\n * Component re-renders each time emit() is called.\n *\n * @returns {*} The current value, or null before first emit\n */\n const useValue = () => {\n const { subscribe, getSnapshot } = useStoreContext();\n return useSyncExternalStore(subscribe, getSnapshot);\n };\n\n /**\n * useSelector - Subscribe with a selector function.\n * Only re-renders when the selected/derived value changes.\n *\n * @param {Function} compute - (value) => derivedValue\n * @param {Function} isEqual - Optional equality function (default: ===)\n * @returns {*} The computed value\n */\n const useSelector = (compute, isEqual = strictEqual) => {\n const { subscribe, getSnapshot } = useStoreContext();\n return useSyncExternalStoreWithSelector(\n subscribe,\n getSnapshot,\n getSnapshot,\n compute,\n isEqual\n );\n };\n\n return { Provider, useValue, useSelector };\n}\n","/**\n * Copyright 2018 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\n\nimport moment from 'moment-timezone';\nimport URI from \"urijs\";\n\nexport const findElementPos = (obj) => {\n var curtop = -70;\n if (obj.offsetParent) {\n do {\n curtop += obj.offsetTop;\n } while (obj = obj.offsetParent);\n return [curtop];\n }\n};\n\nexport const epochToMoment = (atime) => {\n if(!atime) return atime;\n atime = atime * 1000;\n return moment(atime);\n};\n\nexport const epochToMomentTimeZone = (atime, time_zone) => {\n if(!atime) return atime;\n atime = atime * 1000;\n return moment(atime).tz(time_zone);\n};\n\nexport const formatEpoch = (atime, format = 'M/D/YYYY h:mm a') => {\n if(!atime) return atime;\n return epochToMoment(atime).format(format);\n};\n\nexport const parseLocationHour = (hour) => {\n let parsedHour = hour.toString();\n if(parsedHour.length < 4) parsedHour = `0${parsedHour}`;\n parsedHour = parsedHour.match(/.{2}/g);\n parsedHour = parsedHour.join(':');\n return parsedHour;\n}\n\nexport const objectToQueryString = (obj) => {\n var str = \"\";\n for (var key in obj) {\n if (str != \"\") {\n str += \"&\";\n }\n str += key + \"=\" + encodeURIComponent(obj[key]);\n }\n\n return str;\n};\n\nexport const getBackURL = () => {\n let url = URI(window.location.href);\n let query = url.search(true);\n let fragment = url.fragment();\n let backUrl = query.hasOwnProperty('BackUrl') ? query['BackUrl'] : null;\n if(backUrl != null && fragment != null && fragment != ''){\n backUrl += `#${fragment}`;\n }\n return backUrl;\n};\n\nexport const toSlug = (text) =>{\n text = text.toLowerCase();\n return text.replace(/[^a-zA-Z0-9]+/g,'_');\n}\n\nexport const getAuthCallback = () => {\n if(typeof window !== 'undefined') {\n return `${window.location.origin}/auth/callback`;\n }\n return null;\n};\n\nexport const getCurrentLocation = () => {\n let location = '';\n if(typeof window !== 'undefined') {\n location = window.location;\n // check if we are on iframe\n if (window.top)\n location = window.top.location;\n }\n return location;\n};\n\nexport const getOrigin = () => {\n if(typeof window !== 'undefined') {\n return window.location.origin;\n }\n return null;\n};\n\nexport const getCurrentPathName = () => {\n if(typeof window !== 'undefined') {\n return window.location.pathname;\n }\n return null;\n};\n\nexport const getCurrentHref = () => {\n if(typeof window !== 'undefined') {\n return window.location.href;\n }\n return null;\n};\n\nexport const getAllowedUserGroups = () => {\n if(typeof window !== 'undefined') {\n return window.ALLOWED_USER_GROUPS || '';\n }\n return null;\n};\n\nexport const buildAPIBaseUrl = (relativeUrl) => {\n if(typeof window !== 'undefined'){\n return `${window.API_BASE_URL}${relativeUrl}`;\n }\n return null``;\n};\n\nexport const putOnLocalStorage = (key, value) => {\n if(typeof window !== 'undefined') {\n window.localStorage.setItem(key, value);\n }\n};\n\nexport const getFromLocalStorage = (key, removeIt) => {\n if(typeof window !== 'undefined') {\n let val = window.localStorage.getItem(key);\n if(removeIt){\n console.log(`getFromLocalStorage removing key ${key}`);\n removeFromLocalStorage(key);\n }\n return val;\n }\n return null;\n};\n\nexport const removeFromLocalStorage = (key) => {\n if(typeof window !== 'undefined') {\n window.localStorage.removeItem(key);\n }\n}\n\nexport const isClearingSessionState = () => {\n if(typeof window !== 'undefined') {\n return window.clearing_session_state;\n }\n return false;\n};\n\nexport const setSessionClearingState = (val) => {\n if(typeof window !== 'undefined') {\n window.clearing_session_state = val;\n }\n};\n\nexport const getCurrentUserLanguage = () => {\n let language = 'en';\n if(typeof navigator !== 'undefined') {\n language = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage;\n }\n return language;\n};\n\nexport const scrollToError = (errors) => {\n if(Object.keys(errors).length > 0) {\n const firstError = Object.keys(errors)[0];\n const firstNode = document.getElementById(firstError);\n if (firstNode) window.scrollTo(0, findElementPos(firstNode));\n }\n};\n\nexport const hasErrors = (field, errors) => {\n if(field in errors) {\n return errors[field];\n }\n return '';\n};\n\nexport const shallowEqual = (object1, object2) => {\n const keys1 = Object.keys(object1);\n const keys2 = Object.keys(object2);\n\n if (keys1.length !== keys2.length) {\n return false;\n }\n\n for (let key of keys1) {\n if (object1[key] !== object2[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport const arraysEqual = (a1, a2) =>\n a1.length === a2.length && a1.every((o, idx) => shallowEqual(o, a2[idx]));\n\nexport const isEmpty = (obj) => {\n return Object.keys(obj).length === 0;\n};\n\n\nexport const base64URLEncode = (str) => {\n return str\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nexport const retryPromise = async (\n cb,\n maxNumberOfRetries = 3\n) => {\n for (let i = 0; i < maxNumberOfRetries; i++) {\n if (await cb()) {\n return true;\n }\n }\n\n return false;\n}\n\nexport const getTimeServiceUrl = () => {\n if(typeof window !== 'undefined') {\n return window.TIMEINTERVALSINCE1970_API_URL || process.env.TIMEINTERVALSINCE1970_API_URL;\n }\n return null;\n};\n\nexport const getEventLocation = (event, summitVenueCount, summitShowLocDate = null, nowUtc = null) => {\n const shouldShowVenues = (summitShowLocDate && nowUtc) ? summitShowLocDate * 1000 < nowUtc : true;\n const locationName = [];\n const { location } = event;\n\n if (!shouldShowVenues) return 'TBA';\n\n if (!location) return 'TBA';\n\n if (summitVenueCount > 1 && location.venue?.name) locationName.push(location.venue.name);\n if (location.floor?.name) locationName.push(location.floor.name);\n if (location.name) locationName.push(location.name);\n\n return locationName.length > 0 ? locationName.join(' - ') : 'TBA';\n};\n\nexport const getEventHosts = (event) => {\n let hosts = [];\n if (event.speakers?.length > 0) {\n hosts = [...event.speakers];\n }\n if (event.moderator) hosts.push(event.moderator);\n\n return hosts;\n};\n\nconst loadImage = async url => {\n const img = document.createElement('img')\n img.src = url\n img.crossOrigin = 'anonymous'\n\n return new Promise((resolve, reject) => {\n img.onload = () => resolve(img)\n img.onerror = reject\n })\n}\n\nexport const convertSVGtoImg = async (svgUrl) => {\n const img = await loadImage(svgUrl)\n const newWidth = 100\n const newHeight = Math.floor(img.naturalHeight * 100 / img.naturalWidth)\n\n const canvas = document.createElement('canvas')\n canvas.width = newWidth\n canvas.height = newHeight\n canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight)\n\n const url = await canvas.toDataURL(`image/png`, 1.0)\n console.log(url, newWidth, newHeight);\n return {url, width: newWidth, height: newHeight}\n}\n\nexport const isRateEnabled = (value) =>\n value !== null && value !== undefined && value !== \"\";\n\n/**\n * Returns true if value is null, undefined, empty/whitespace string,\n * empty array, or empty object.\n */\nexport const empty = (value) => {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.trim().length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") return Object.keys(value).length === 0;\n return false;\n};\n\nexport const isSentryInitialized = () => typeof window !== \"undefined\" && !!window.SENTRY_DSN;\n\nexport const normalizeHtmlString = (textInput) => {\n if (!textInput) return \"\";\n const doc = new DOMParser().parseFromString(textInput, \"text/html\");\n return doc.body.textContent.trim().length === 0 ? \"\" : textInput;\n};","module.exports = require(\"@babel/runtime/helpers/defineProperty\");","module.exports = require(\"moment-timezone\");","module.exports = require(\"react\");","module.exports = require(\"urijs\");","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Copyright 2026 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Pre-built clock store using createExternalStore.\n *\n * Wires the Clock component (server-synced, ticks every second) into a\n * createExternalStore instance. Components choose their update strategy:\n *\n * - useClock() re-renders every second (for countdowns, live displays)\n * - useClockSelector(compute) only re-renders when the computed result changes\n * - Components that use neither are never affected by clock ticks\n *\n * Usage:\n * import { ClockProvider, useClock, useClockSelector } from 'openstack-uicore-foundation/lib/components/clock-context';\n *\n * <ClockProvider timezone={summit.time_zone_id}>\n * <App />\n * </ClockProvider>\n *\n * const nowUtc = useClock();\n *\n * const phase = useClockSelector((nowUtc) => {\n * if (nowUtc < event.start) return 'before';\n * if (nowUtc <= event.end) return 'during';\n * return 'after';\n * });\n *\n * For custom (non-clock) stores, see createExternalStore in utils/external-store.js.\n **/\n\nimport React from 'react';\nimport { createExternalStore } from '../utils/external-store';\nimport Clock from './clock';\n\nconst { Provider, useValue: useClock, useSelector: useClockSelector } = createExternalStore('Clock');\n\n/**\n * ClockProvider - Wraps your app with server-synced clock context.\n *\n * @param {string} timezone - Timezone for the clock (e.g., \"America/New_York\")\n * @param {number} now - Optional initial timestamp (for testing or manual override)\n * @param {React.ReactNode} children - Child components\n */\nexport const ClockProvider = ({ timezone, now, children }) => (\n <Provider initialValue={now}>\n {(emit) => (\n <>\n <Clock onTick={emit} timezone={timezone} now={now} />\n {children}\n </>\n )}\n </Provider>\n);\n\nexport { useClock, useClockSelector };\n"],"names":["root","factory","exports","module","define","amd","this","Clock","React","constructor","props","super","_defineProperty","response","localBefore","localAfter","moment","unix","timestamp","_isMounted","console","log","setState","onTick","timeServiceUrl","getTimeServiceUrl","fetch","then","async","status","json","Promise","reject","catch","err","state","fragmentParser","FragmentParser","interval","manualSet","onVisibilityChange","bind","componentDidMount","timezone","now","nowQS","getParam","momentQS","isValid","valueOf","getServerTime","processServerTimeResponse","processServerTimeResponseError","setInterval","tick","document","addEventListener","visibilityState","componentWillUnmount","clearInterval","removeEventListener","render","display","style","marginTop","textAlign","fontSize","format","originalHash","hash","convertToHash","strHash","params","substr","toLowerCase","split","res","param","length","val","trim","clearParams","key","window","location","hasOwnProperty","getParams","_objectSpread","deleteParam","deleteParams","includes","setParam","value","serialize","require","strictEqual","a","b","createExternalStore","name","Context","createContext","useStoreContext","context","useContext","Error","Provider","initialValue","children","valueRef","useRef","listenersRef","Set","subscribe","useCallback","callback","current","add","delete","getSnapshot","emit","forEach","listener","contextValue","reactUseMemo","useValue","useSyncExternalStore","useSelector","compute","isEqual","useSyncExternalStoreWithSelector","TIMEINTERVALSINCE1970_API_URL","process","env","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","n","getter","__esModule","d","definition","o","Object","defineProperty","enumerable","get","obj","prop","prototype","call","r","Symbol","toStringTag","useClock","useClockSelector","ClockProvider"],"sourceRoot":""}
|
|
1
|
+
{"version":3,"file":"components/clock-context.js","mappings":"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","sources":["webpack://openstack-uicore-foundation/webpack/universalModuleDefinition","webpack://openstack-uicore-foundation/./src/components/clock.js","webpack://openstack-uicore-foundation/./src/components/fragment-parser.js","webpack://openstack-uicore-foundation/external commonjs \"use-sync-external-store/shim\"","webpack://openstack-uicore-foundation/external commonjs \"use-sync-external-store/shim/with-selector\"","webpack://openstack-uicore-foundation/./src/utils/external-store.js","webpack://openstack-uicore-foundation/./src/utils/methods.js","webpack://openstack-uicore-foundation/external commonjs \"@babel/runtime/helpers/defineProperty\"","webpack://openstack-uicore-foundation/external commonjs \"moment-timezone\"","webpack://openstack-uicore-foundation/external commonjs \"react\"","webpack://openstack-uicore-foundation/external commonjs \"urijs\"","webpack://openstack-uicore-foundation/webpack/bootstrap","webpack://openstack-uicore-foundation/webpack/runtime/compat get default export","webpack://openstack-uicore-foundation/webpack/runtime/define property getters","webpack://openstack-uicore-foundation/webpack/runtime/hasOwnProperty shorthand","webpack://openstack-uicore-foundation/webpack/runtime/make namespace object","webpack://openstack-uicore-foundation/./src/components/clock-context.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"openstack-uicore-foundation\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"openstack-uicore-foundation\"] = factory();\n\telse\n\t\troot[\"openstack-uicore-foundation\"] = factory();\n})(this, () => {\nreturn ","/**\n * Copyright 2020 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\nimport React from 'react';\nimport moment from \"moment-timezone\";\nimport FragmentParser from \"./fragment-parser\";\nimport {getTimeServiceUrl} from '../utils/methods';\n\n/**\n * class Clock\n */\nclass Clock extends React.Component {\n\n constructor(props) {\n super(props);\n this.fragmentParser = new FragmentParser();\n this.interval = null;\n this.state = {\n timestamp: null,\n manualSet : false,\n }\n this._isMounted = false;\n this.onVisibilityChange = this.onVisibilityChange.bind(this);\n }\n\n /**\n * @param response\n * @param localBefore\n */\n processServerTimeResponse = (response, localBefore) => {\n const localAfter = moment().unix();\n let timestamp = localAfter;\n if (response) {\n timestamp = response.timestamp + (localAfter - localBefore);\n }\n if(this._isMounted) {\n console.log(`Clock::processServerTimeResponse setting timestamp ${timestamp}`)\n this.setState({timestamp});\n }\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n processServerTimeResponseError = () => {\n const localAfter = moment().unix();\n let timestamp = localAfter;\n if(this._isMounted) {\n console.log(`Clock::processServerTimeResponseError setting timestamp ${timestamp}`)\n this.setState({timestamp});\n }\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n componentDidMount() {\n this._isMounted = true;\n const {timezone = 'UTC', now} = this.props;\n const nowQS = this.fragmentParser.getParam('now');\n const momentQS = moment.tz(nowQS, 'YYYY-MM-DD,hh:mm:ss', timezone);\n let timestamp = null;\n let manualSet = false;\n if (momentQS.isValid()) {\n timestamp = momentQS.valueOf() / 1000;\n console.log(`Clock::componentDidMount nowQS ${nowQS} is valid setting timestamp ${timestamp}`);\n manualSet = true;\n } else if (now) {\n timestamp = now\n } else {\n const localBefore = moment().unix();\n this.getServerTime()\n .then((response) => this.processServerTimeResponse(response, localBefore))\n .catch(() => this.processServerTimeResponseError())\n }\n\n if(timestamp) {\n this.setState({timestamp, manualSet});\n if(this.props.onTick)\n this.props.onTick(timestamp);\n }\n\n this.interval = setInterval(this.tick, 1000);\n document.addEventListener(\"visibilitychange\", this.onVisibilityChange, false)\n }\n\n onVisibilityChange() {\n const visibilityState = document.visibilityState;\n const {manualSet} = this.state;\n if (visibilityState === \"visible\") {\n console.log(`Clock::onVisibilityChange manualSet ${manualSet}`)\n if(manualSet) return;\n const localBefore = moment().unix();\n this.getServerTime()\n .then((response) => this.processServerTimeResponse(response, localBefore))\n .catch(() => this.processServerTimeResponseError());\n }\n }\n\n componentWillUnmount() {\n this._isMounted = false;\n clearInterval(this.interval);\n document.removeEventListener(\"visibilitychange\", this.onVisibilityChange)\n this.interval = null;\n }\n\n getServerTime = () => {\n const timeServiceUrl = getTimeServiceUrl();\n return fetch(`${timeServiceUrl}`).then(async (response) => {\n if (response.status === 200) {\n return response.json();\n }\n return Promise.reject(null);\n })\n .catch(err => {\n console.log(err);\n return Promise.reject(err);\n });\n };\n\n tick = () => {\n const {timestamp} = this.state;\n if (timestamp !== null) {\n if(this.props.onTick)\n this.props.onTick(timestamp + 1);\n if(this._isMounted)\n this.setState({timestamp: timestamp + 1})\n }\n };\n\n // epoch utc time in seconds\n now = () => {\n return this.state.timestamp;\n };\n\n render() {\n const {display, timezone = 'UTC'} = this.props;\n const {timestamp} = this.state;\n\n if (!display || !timestamp) return null;\n\n return (\n <div style={{marginTop: '50px', textAlign: 'center', fontSize: '20px'}}>\n {moment.tz(timestamp * 1000, timezone).format('YYYY-MM-DD hh:mm:ss')}\n </div>\n );\n }\n\n}\n\nexport default Clock;\n","\nexport default class FragmentParser {\n\n constructor(){\n this.originalHash = '';\n this.hash = {};\n }\n\n convertToHash(strHash)\n {\n strHash = strHash.substr(1).toLowerCase();\n let params = strHash.split('&');\n let res = {};\n for(let param of params)\n {\n param = param.split('=');\n if(param.length !== 2) continue;\n let val = param[1].trim();\n if(val === '') continue;\n\n if (val === 'true' || val === 'false')\n param[1] = val == 'true';\n\n res[param[0]] = param[1];\n }\n return res;\n }\n\n clearParams(){\n this.originalHash = '';\n this.hash = {};\n }\n\n getParam(key){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n\n if(!this.hash.hasOwnProperty(key) ) return null;\n return this.hash[key];\n }\n\n getParams(){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n\n return { ... this.hash };\n }\n\n deleteParam(param){\n var hash = this.getParams();\n this.clearParams();\n for(let key in hash) {\n if(key == param) continue;\n this.hash[key] = hash[key];\n }\n }\n\n deleteParams(params){\n var hash = this.getParams();\n this.clearParams();\n for(let key in hash) {\n if(params.includes(key)) continue;\n this.hash[key] = hash[key];\n }\n }\n\n setParam(key, value){\n if(typeof window !== 'undefined' && this.originalHash !== window.location.hash){\n this.originalHash = window.location.hash;\n this.hash = this.convertToHash(this.originalHash);\n }\n if(value !== null && value !== '')\n this.hash[key] = value;\n else\n delete this.hash[key];\n return this;\n }\n\n serialize(){\n let res = '';\n for(let key in this.hash)\n {\n let val = this.hash[key];\n if(res !== '') res += '&';\n res += key+'='+val;\n }\n return res;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"use-sync-external-store/shim\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"use-sync-external-store/shim/with-selector\");","/**\n * Copyright 2026 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * createExternalStore - Factory for creating React-optimized external stores.\n *\n * Problem:\n * When a frequently-updating data source (clock, WebSocket, polling, etc.)\n * pushes its value into shared state (global store, lifted useState, or a\n * context value), every consuming component re-renders on every update,\n * even when they only care about a derived condition that rarely changes.\n *\n * Solution:\n * createExternalStore() returns a Provider and hooks that store the value\n * in a ref (no re-renders) and use useSyncExternalStore so components\n * can opt in to updates selectively:\n *\n * - useValue() → re-renders on every update\n * - useSelector(compute, isEqual) → re-renders only when the computed result changes\n *\n * Components that don't call either hook are never affected by updates.\n *\n * How it works:\n * 1. The Provider stores the value in a ref (writing to a ref never triggers\n * a React re-render) and keeps a Set of listener callbacks.\n * 2. When emit(value) is called, the ref is updated and all listeners are\n * notified. These listeners come from useSyncExternalStore.\n * 3. useSyncExternalStore (React 18, shimmed for 16/17) calls getSnapshot()\n * to read the ref, compares with the previous value, and only re-renders\n * the component if the value changed.\n * 4. useMemo adds a layer on top: it runs a compute function on the raw value\n * and only re-renders if the computed result changed (checked via isEqual).\n *\n * API:\n * createExternalStore(name) returns:\n *\n * - Provider Wraps your component tree. Pass children as a render function\n * to receive the emit callback: (emit) => JSX. Call emit(value)\n * each time your data source has a new value.\n *\n * - useValue() Returns the latest emitted value. The component re-renders\n * on every emit.\n *\n * - useSelector(compute, isEqual?)\n * Returns a derived value. compute(rawValue) runs on every emit,\n * but the component only re-renders when isEqual returns false\n * (default: ===). Useful when you need to derive something that\n * changes less frequently than the raw value.\n *\n * The name parameter is used in error messages. For example,\n * createExternalStore('Clock') throws \"Clock hooks must be used within\n * their Provider\" when a hook is called outside the Provider.\n *\n * For clock-specific usage:\n * A pre-built clock store is available at:\n * import { ClockProvider, useClock, useClockSelector } from 'openstack-uicore-foundation/lib/components/clock-context';\n * This wires createExternalStore to the Clock component so projects don't\n * have to repeat that boilerplate.\n *\n * Custom store example:\n * import { createExternalStore } from 'openstack-uicore-foundation/lib/utils/external-store';\n *\n * const { Provider, useValue, useSelector } = createExternalStore('WebSocket');\n *\n * const WebSocketProvider = ({ url, children }) => (\n * <Provider>\n * {(emit) => (\n * <>\n * <WebSocketSource url={url} onMessage={emit} />\n * {children}\n * </>\n * )}\n * </Provider>\n * );\n *\n * // Re-renders on every message:\n * const message = useValue();\n *\n * // Re-renders only when the derived value changes:\n * const isActive = useSelector((msg) => msg?.status === 'active');\n **/\n\nimport React, { createContext, useContext, useRef, useCallback, useMemo as reactUseMemo } from 'react';\n// Shim for React 16/17 compatibility, falls back to native in React 18+\nimport { useSyncExternalStore } from 'use-sync-external-store/shim';\nimport { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';\n\nconst strictEqual = (a, b) => a === b;\n\n/**\n * Creates an external store with a Provider and subscription hooks.\n *\n * @param {string} name - Store name, used in error messages (e.g., \"Clock\", \"WebSocket\")\n * @returns {{ Provider, useValue, useSelector }}\n */\nexport function createExternalStore(name = 'ExternalStore') {\n const Context = createContext(null);\n\n const useStoreContext = () => {\n const context = useContext(Context);\n if (context === null) {\n throw new Error(`${name} hooks must be used within their Provider`);\n }\n return context;\n };\n\n /**\n * Provider - Wraps your component tree and provides the store.\n *\n * Pass children as a render function to receive the `emit` callback:\n * <Provider>{(emit) => <Source onUpdate={emit} />}</Provider>\n *\n * Or pass children normally if you wire emit externally.\n *\n * Pass `initialValue` to seed the store synchronously so that hooks\n * called before the first emit see a real value instead of null.\n */\n const Provider = ({ initialValue = null, children }) => {\n const valueRef = useRef(initialValue);\n const listenersRef = useRef(new Set());\n\n const subscribe = useCallback((callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n }, []);\n\n const getSnapshot = useCallback(() => valueRef.current, []);\n\n const emit = useCallback((value) => {\n valueRef.current = value;\n listenersRef.current.forEach(listener => listener());\n }, []);\n\n const contextValue = reactUseMemo(() => ({ subscribe, getSnapshot }), [subscribe, getSnapshot]);\n\n return (\n <Context.Provider value={contextValue}>\n {typeof children === 'function' ? children(emit) : children}\n </Context.Provider>\n );\n };\n\n /**\n * useValue - Subscribe to every update.\n * Component re-renders each time emit() is called.\n *\n * @returns {*} The current value, or null before first emit\n */\n const useValue = () => {\n const { subscribe, getSnapshot } = useStoreContext();\n return useSyncExternalStore(subscribe, getSnapshot);\n };\n\n /**\n * useSelector - Subscribe with a selector function.\n * Only re-renders when the selected/derived value changes.\n *\n * @param {Function} compute - (value) => derivedValue\n * @param {Function} isEqual - Optional equality function (default: ===)\n * @returns {*} The computed value\n */\n const useSelector = (compute, isEqual = strictEqual) => {\n const { subscribe, getSnapshot } = useStoreContext();\n return useSyncExternalStoreWithSelector(\n subscribe,\n getSnapshot,\n getSnapshot,\n compute,\n isEqual\n );\n };\n\n return { Provider, useValue, useSelector };\n}\n","/**\n * Copyright 2018 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\n\nimport moment from 'moment-timezone';\nimport URI from \"urijs\";\n\nexport const findElementPos = (obj) => {\n var curtop = -70;\n if (obj.offsetParent) {\n do {\n curtop += obj.offsetTop;\n } while (obj = obj.offsetParent);\n return [curtop];\n }\n};\n\nexport const epochToMoment = (atime) => {\n if(!atime) return atime;\n atime = atime * 1000;\n return moment(atime);\n};\n\nexport const epochToMomentTimeZone = (atime, time_zone) => {\n if(!atime) return atime;\n atime = atime * 1000;\n return moment(atime).tz(time_zone);\n};\n\nexport const formatEpoch = (atime, format = 'M/D/YYYY h:mm a') => {\n if(!atime) return atime;\n return epochToMoment(atime).format(format);\n};\n\nexport const parseLocationHour = (hour) => {\n let parsedHour = hour.toString();\n if(parsedHour.length < 4) parsedHour = `0${parsedHour}`;\n parsedHour = parsedHour.match(/.{2}/g);\n parsedHour = parsedHour.join(':');\n return parsedHour;\n}\n\nexport const objectToQueryString = (obj) => {\n var str = \"\";\n for (var key in obj) {\n if (str != \"\") {\n str += \"&\";\n }\n str += key + \"=\" + encodeURIComponent(obj[key]);\n }\n\n return str;\n};\n\nexport const getBackURL = () => {\n let url = URI(window.location.href);\n let query = url.search(true);\n let fragment = url.fragment();\n let backUrl = query.hasOwnProperty('BackUrl') ? query['BackUrl'] : null;\n if(backUrl != null && fragment != null && fragment != ''){\n backUrl += `#${fragment}`;\n }\n return backUrl;\n};\n\nexport const toSlug = (text) =>{\n text = text.toLowerCase();\n return text.replace(/[^a-zA-Z0-9]+/g,'_');\n}\n\nexport const getAuthCallback = () => {\n if(typeof window !== 'undefined') {\n return `${window.location.origin}/auth/callback`;\n }\n return null;\n};\n\nexport const getCurrentLocation = () => {\n let location = '';\n if(typeof window !== 'undefined') {\n location = window.location;\n // check if we are on iframe\n if (window.top)\n location = window.top.location;\n }\n return location;\n};\n\nexport const getOrigin = () => {\n if(typeof window !== 'undefined') {\n return window.location.origin;\n }\n return null;\n};\n\nexport const getCurrentPathName = () => {\n if(typeof window !== 'undefined') {\n return window.location.pathname;\n }\n return null;\n};\n\nexport const getCurrentHref = () => {\n if(typeof window !== 'undefined') {\n return window.location.href;\n }\n return null;\n};\n\nexport const getAllowedUserGroups = () => {\n if(typeof window !== 'undefined') {\n return window.ALLOWED_USER_GROUPS || '';\n }\n return null;\n};\n\nexport const buildAPIBaseUrl = (relativeUrl) => {\n if(typeof window !== 'undefined'){\n return `${window.API_BASE_URL}${relativeUrl}`;\n }\n return null``;\n};\n\nexport const putOnLocalStorage = (key, value) => {\n if(typeof window !== 'undefined') {\n window.localStorage.setItem(key, value);\n }\n};\n\nexport const getFromLocalStorage = (key, removeIt) => {\n if(typeof window !== 'undefined') {\n let val = window.localStorage.getItem(key);\n if(removeIt){\n console.log(`getFromLocalStorage removing key ${key}`);\n removeFromLocalStorage(key);\n }\n return val;\n }\n return null;\n};\n\nexport const removeFromLocalStorage = (key) => {\n if(typeof window !== 'undefined') {\n window.localStorage.removeItem(key);\n }\n}\n\nexport const isClearingSessionState = () => {\n if(typeof window !== 'undefined') {\n return window.clearing_session_state;\n }\n return false;\n};\n\nexport const setSessionClearingState = (val) => {\n if(typeof window !== 'undefined') {\n window.clearing_session_state = val;\n }\n};\n\nexport const getCurrentUserLanguage = () => {\n let language = 'en';\n if(typeof navigator !== 'undefined') {\n language = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage;\n }\n return language;\n};\n\nexport const scrollToError = (errors) => {\n if(Object.keys(errors).length > 0) {\n const firstError = Object.keys(errors)[0];\n const firstNode = document.getElementById(firstError);\n if (firstNode) window.scrollTo(0, findElementPos(firstNode));\n }\n};\n\nexport const hasErrors = (field, errors) => {\n if(field in errors) {\n return errors[field];\n }\n return '';\n};\n\nexport const shallowEqual = (object1, object2) => {\n const keys1 = Object.keys(object1);\n const keys2 = Object.keys(object2);\n\n if (keys1.length !== keys2.length) {\n return false;\n }\n\n for (let key of keys1) {\n if (object1[key] !== object2[key]) {\n return false;\n }\n }\n\n return true;\n};\n\nexport const arraysEqual = (a1, a2) =>\n a1.length === a2.length && a1.every((o, idx) => shallowEqual(o, a2[idx]));\n\nexport const isEmpty = (obj) => {\n return Object.keys(obj).length === 0;\n};\n\n\nexport const base64URLEncode = (str) => {\n return str\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nexport const retryPromise = async (\n cb,\n maxNumberOfRetries = 3\n) => {\n for (let i = 0; i < maxNumberOfRetries; i++) {\n if (await cb()) {\n return true;\n }\n }\n\n return false;\n}\n\nexport const getTimeServiceUrl = () => {\n if(typeof window !== 'undefined') {\n return window.TIMEINTERVALSINCE1970_API_URL || process.env.TIMEINTERVALSINCE1970_API_URL;\n }\n return null;\n};\n\nexport const getEventLocation = (event, summitVenueCount, summitShowLocDate = null, nowUtc = null) => {\n const shouldShowVenues = (summitShowLocDate && nowUtc) ? summitShowLocDate * 1000 < nowUtc : true;\n const locationName = [];\n const { location } = event;\n\n if (!shouldShowVenues) return 'TBA';\n\n if (!location) return 'TBA';\n\n if (summitVenueCount > 1 && location.venue?.name) locationName.push(location.venue.name);\n if (location.floor?.name) locationName.push(location.floor.name);\n if (location.name) locationName.push(location.name);\n\n return locationName.length > 0 ? locationName.join(' - ') : 'TBA';\n};\n\nexport const getEventHosts = (event) => {\n let hosts = [];\n if (event.speakers?.length > 0) {\n hosts = [...event.speakers];\n }\n if (event.moderator) hosts.push(event.moderator);\n\n return hosts;\n};\n\nconst loadImage = async url => {\n const img = document.createElement('img')\n img.src = url\n img.crossOrigin = 'anonymous'\n\n return new Promise((resolve, reject) => {\n img.onload = () => resolve(img)\n img.onerror = reject\n })\n}\n\nexport const convertSVGtoImg = async (svgUrl) => {\n const img = await loadImage(svgUrl)\n const newWidth = 100\n const newHeight = Math.floor(img.naturalHeight * 100 / img.naturalWidth)\n\n const canvas = document.createElement('canvas')\n canvas.width = newWidth\n canvas.height = newHeight\n canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight)\n\n const url = await canvas.toDataURL(`image/png`, 1.0)\n console.log(url, newWidth, newHeight);\n return {url, width: newWidth, height: newHeight}\n}\n\nexport const isRateEnabled = (value) =>\n value !== null && value !== undefined && value !== \"\";\n\n/**\n * Returns true if value is null, undefined, empty/whitespace string,\n * empty array, or empty object.\n */\nexport const empty = (value) => {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.trim().length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") return Object.keys(value).length === 0;\n return false;\n};\n\nexport const isSentryInitialized = () => typeof window !== \"undefined\" && !!window.SENTRY_DSN;\n\nexport const normalizeHtmlString = (textInput) => {\n if (!textInput) return \"\";\n const doc = new DOMParser().parseFromString(textInput, \"text/html\");\n return doc.body.textContent.trim().length === 0 ? \"\" : textInput;\n};","module.exports = require(\"@babel/runtime/helpers/defineProperty\");","module.exports = require(\"moment-timezone\");","module.exports = require(\"react\");","module.exports = require(\"urijs\");","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Copyright 2026 OpenStack Foundation\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Pre-built clock store using createExternalStore.\n *\n * Wires the Clock component (server-synced, ticks every second) into a\n * createExternalStore instance. Components choose their update strategy:\n *\n * - useClock() re-renders every second (for countdowns, live displays)\n * - useClockSelector(compute) only re-renders when the computed result changes\n * - Components that use neither are never affected by clock ticks\n *\n * Usage:\n * import { ClockProvider, useClock, useClockSelector } from 'openstack-uicore-foundation/lib/components/clock-context';\n *\n * <ClockProvider timezone={summit.time_zone_id}>\n * <App />\n * </ClockProvider>\n *\n * const nowUtc = useClock();\n *\n * const phase = useClockSelector((nowUtc) => {\n * if (nowUtc < event.start) return 'before';\n * if (nowUtc <= event.end) return 'during';\n * return 'after';\n * });\n *\n * For custom (non-clock) stores, see createExternalStore in utils/external-store.js.\n **/\n\nimport React from 'react';\nimport { createExternalStore } from '../utils/external-store';\nimport Clock from './clock';\n\nconst { Provider, useValue: useClock, useSelector: useClockSelector } = createExternalStore('Clock');\n\n/**\n * ClockProvider - Wraps your app with server-synced clock context.\n *\n * @param {string} timezone - Timezone for the clock (e.g., \"America/New_York\")\n * @param {number} now - Optional initial timestamp (for testing or manual override)\n * @param {React.ReactNode} children - Child components\n */\nexport const ClockProvider = ({ timezone, now, children }) => (\n <Provider initialValue={now}>\n {(emit) => (\n <>\n <Clock onTick={emit} timezone={timezone} now={now} />\n {children}\n </>\n )}\n </Provider>\n);\n\nexport { useClock, useClockSelector };\n"],"names":["root","factory","exports","module","define","amd","this","Clock","React","constructor","props","super","_defineProperty","response","localBefore","localAfter","moment","unix","timestamp","_isMounted","console","log","setState","onTick","timeServiceUrl","getTimeServiceUrl","fetch","then","async","status","json","Promise","reject","catch","err","state","fragmentParser","FragmentParser","interval","manualSet","onVisibilityChange","bind","componentDidMount","timezone","now","nowQS","getParam","momentQS","isValid","valueOf","getServerTime","processServerTimeResponse","processServerTimeResponseError","setInterval","tick","document","addEventListener","visibilityState","componentWillUnmount","clearInterval","removeEventListener","render","display","style","marginTop","textAlign","fontSize","format","originalHash","hash","convertToHash","strHash","params","substr","toLowerCase","split","res","param","length","val","trim","clearParams","key","window","location","hasOwnProperty","getParams","_objectSpread","deleteParam","deleteParams","includes","setParam","value","serialize","require","strictEqual","a","b","createExternalStore","name","Context","createContext","useStoreContext","context","useContext","Error","Provider","initialValue","children","valueRef","useRef","listenersRef","Set","subscribe","useCallback","callback","current","add","delete","getSnapshot","emit","forEach","listener","contextValue","reactUseMemo","useValue","useSyncExternalStore","useSelector","compute","isEqual","useSyncExternalStoreWithSelector","TIMEINTERVALSINCE1970_API_URL","process","env","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","n","getter","__esModule","d","definition","o","Object","defineProperty","enumerable","get","obj","prop","prototype","call","r","Symbol","toStringTag","useClock","useClockSelector","ClockProvider"],"sourceRoot":""}
|