Skip to content

feat(locations): materializer allowlist picker with drift badges + saved-state banner#1007

Open
caseylocker wants to merge 2 commits into
masterfrom
feature/materializer-allowlist-picker
Open

feat(locations): materializer allowlist picker with drift badges + saved-state banner#1007
caseylocker wants to merge 2 commits into
masterfrom
feature/materializer-allowlist-picker

Conversation

@caseylocker

@caseylocker caseylocker commented Jul 7, 2026

Copy link
Copy Markdown

ref: https://app.clickup.com/t/86b9y4k8t

What

Adds the materializer allowlist picker to the Locations page Synchronization Settings panel: a checkbox-group selecting which MediaUploadTypes participate in the Dropbox materializer, saved as {id, name} pairs through the existing updateSyncConfig PUT. Includes drift-badge reconciliation against the live type list (rename/recreate/storage-change/invalid cases 0–8 per the SDS decision table), a saved-state banner, and an inline error/Retry state.

Spec: ftn-docsnsklz/sds/dropbox-materializer-participating-types-allowlist.md (incl. the 2026-07-06 amendment consolidating plumbing into existing domain files, and the 2026-07-10 amendment recording the review-round changes below).

Scope note (9 files, test-dominated, single concern)

Per process/ai-assisted-development-workflow.md: 9 files changed — aggregator and config thunks in dropbox-sync-actions.js, cache in the existing dropboxSyncState slice, plus two new source files added at review request: src/models/materializer-allowlist.js (reconciliation/normalization helpers, exemplar models/lead-report-settings.js) and src/components/locations/allowlist-panel.js (the picker UI + selection state, rendered from the Locations page). The large majority of insertions are test code.

Review round (second commit, b69149b)

Addresses all five review comments plus the CodeRabbit finding: model + panel extraction; snackbarErrorHandler on the aggregator and both config thunks; mount fetches chained with the panel gating rows/Save on a new allowlistOptions.loaded flag (stored rows are never reconciled against an options list that has not arrived, regardless of global-overlay timing); storedRows derived via useMemo; Save/toggle/Rebuild mutually exclusive with any in-flight config request — both config thunks dispatch REQUEST_SYNC_CONFIG before token acquisition and share the aggregator-style sequence + summit guard, so a stale response can neither commit cross-summit nor clear the newer request's loading flag, a failed PUT clears loading via a new SYNC_CONFIG_ERROR without resetting the stored config, and a superseded-but-successful PUT refetches so redux converges on the saved server state.

Ruling requested: casefold approximation (blocking question for reviewer)

SDS:618 requires the UI's name-normalization key-space to equal the worker's Python strip().casefold(). JavaScript has no built-in casefold; this PR uses trim().toUpperCase().toLowerCase() — the closest zero-dependency approximation, exact for ASCII and realistic non-ASCII (ß, ligatures). It diverges on the dotless-ı class: JS folds ı to i, Python keeps them distinct. Consequence in the worst case (a stored name containing ı whose live type now spells it i, same id): the UI classifies it Case 1 (no badge) and the Case-2 save-time rename repair never triggers, so the worker keeps skipping those uploads; worker-side diagnostics MAY surface it but are not guaranteed. The divergence is documented at the normalization site and pinned by a dedicated test.

Options:

  • (a) Accept the approximation with the bounded-but-real blind spot, recorded as a dated SDS amendment.
  • (b) Require exact parity — swap normalizeName's body (now in src/models/materializer-allowlist.js) for a vendored Unicode fold table (single-owner design; nothing else changes).

UX question: no UI path from one saved type back to zero (non-blocking, product call)

As specced (SDS:686), Save is disabled when the effective selection is empty, so an admin who has saved ≥1 type cannot shrink the allowlist to zero through this UI — the sanctioned off-path is the dropbox_sync_enabled Synchronization toggle, and the helper text says so. Field-testing feedback: unchecking or removing the last type silently deactivates Save with no adjacent explanation, which reads as a broken button rather than a guarded contract. If this should change (e.g. tooltip on the disabled Save explaining the rule, or allowing an explicit empty save behind a confirm), it needs a product ruling + SDS amendment; the current implementation follows the spec as written. Suggestion: When the effective selection hits zero and there are stored/visible rows (i.e. the user just unchecked or removed the last type — not the pristine never-configured state), render a small Alert severity="info" above the Save button: "At least one type must stay selected. To stop file sync for this summit, turn off Synchronization above." Save stays disabled.

Merge gating

Merge only AFTER dropbox-materializer PR #25 deploys — the picker saves a key (materialized_media_upload_types) that the pre-#25 materializer ignores. Draft until then.

Testing

  • Full suite: 142 suites / 1255 tests green (allowlist page test file: 96; action tests include request-identity guard regressions red-verified against the pre-guard code).
  • Convention pass per skills/react-frontend.md § summit-admin Conventions: global-overlay loading (no per-slice loading), Promise.resolve() thunk guards, constants from utils/constants.js, MUI + sx for the new section, single-brace i18n, no snapshots.
  • Local smoke against a local materializer running the fix room report #25 backend: GET → select → Save → re-GET round-trip and empty-selection gating verified in-browser. IDP scope path (dropbox-materializer/read write on the show-admin client) untested locally; covered by the staging walk.

Summary by CodeRabbit

  • New Features
    • Added a Dropbox materializer allowlist panel for media upload types, including save/retry flows, status banners, and updated i18n for validation/mismatch messaging.
    • Added allowlist option aggregation with pagination and protections to prevent stale results from overwriting newer state.
  • Bug Fixes
    • Improved allowlist reconciliation (renamed, missing, ambiguous, recreated, duplicates/invalid) with deterministic selection and safe payload construction.
    • Added explicit sync-config error handling that stops loading while preserving the last saved sync config.
  • Tests
    • Expanded action, reducer, and UI tests to cover concurrency behavior, guarded updates, reconciliation rules, and panel interaction states.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Dropbox Materializer allowlist flow with paginated fetching, stale-request protection, Redux state, reconciliation helpers, and a location-page selection panel. Sync-config GET/PUT flows now use guarded dispatch behavior, with expanded reducer, action, and UI tests.

Changes

Materializer Allowlist Feature

Layer / File(s) Summary
Guarded sync and allowlist requests
src/actions/dropbox-sync-actions.js, src/actions/__tests__/dropbox-sync-actions.test.js
Adds paginated, concurrency-limited allowlist fetching and supersession guards for allowlist and sync-config requests, with error and dispatch-order coverage.
Redux allowlist state
src/reducers/locations/dropbox-sync-reducer.js, src/reducers/__tests__/dropbox-sync-reducer.test.js
Adds stored materializer types and allowlist option, loading, and error state transitions.
Allowlist reconciliation model
src/models/materializer-allowlist.js, src/pages/locations/__tests__/location-list-page-allowlist.test.js
Adds validation, name normalization, reconciliation cases, deduplication, and decision-table tests.
Location-page allowlist panel
src/components/locations/allowlist-panel.js, src/pages/locations/location-list-page.js, src/i18n/en.json
Adds allowlist selection, reconciliation badges, save/remove/retry behavior, page wiring, and Materializer translations.
Allowlist panel integration coverage
src/pages/locations/__tests__/location-list-page-allowlist.test.js
Covers panel wiring, rendering, payloads, interactions, loading/error gating, fallback arrays, and saved-state banners.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • fntechgit/summit-admin#833: Both changes extend the Dropbox sync action, reducer, and UI flow; this PR adds guarded requests and Materializer allowlist behavior.

Suggested reviewers: smarcet, santipalenque

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a new materializer allowlist picker in Locations with drift badges and a saved-state banner.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/materializer-allowlist-picker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a “materialized media upload types” allowlist picker to the Locations → Synchronization Settings panel, backed by Redux state and a new paged fetch action for MediaUploadTypes, plus UI reconciliation badges and a saved-state banner.

Changes:

  • Extends dropboxSyncState to store materialized_media_upload_types and allowlist option fetch state (options + error).
  • Implements getAllMediaUploadTypesForAllowlist to fetch MediaUploadTypes (paged + concurrency-limited), guarded against stale/superseded invocations.
  • Adds the allowlist picker UI + reconciliation helpers to location-list-page.js, with extensive unit and component tests plus i18n strings.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/reducers/locations/dropbox-sync-reducer.js Adds allowlist options sub-state and materialized_media_upload_types default to the dropbox sync reducer.
src/reducers/tests/dropbox-sync-reducer.test.js Adds reducer tests covering the new allowlist actions and persisted allowlist field behavior.
src/pages/locations/location-list-page.js Implements the allowlist picker UI, reconciliation logic, and saved-state banner on the Locations page.
src/pages/locations/tests/location-list-page-allowlist.test.js Adds thorough unit/component tests for reconciliation cases, UI behavior, gating, and regression coverage.
src/i18n/en.json Adds UI strings for allowlist section labels, badges, banner text, and error/retry copy.
src/actions/dropbox-sync-actions.js Adds paged allowlist options fetch thunk with stale-invocation guards and global overlay loading.
src/actions/tests/dropbox-sync-actions.test.js Adds action tests for paging, ordering, stale-guard behavior, loading bracketing, and error paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pages/locations/location-list-page.js Outdated
Comment thread src/pages/locations/location-list-page.js Outdated
@caseylocker caseylocker force-pushed the feature/materializer-allowlist-picker branch from f667a66 to 5060ed2 Compare July 7, 2026 19:53
@caseylocker caseylocker requested a review from Copilot July 7, 2026 19:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/pages/locations/location-list-page.js Outdated
@caseylocker caseylocker force-pushed the feature/materializer-allowlist-picker branch from 5060ed2 to ba3cd40 Compare July 7, 2026 20:29
…ved-state banner

Checkbox-group picker on the Locations sync panel selecting which MediaUploadTypes
participate in the Dropbox materializer, saved as {id, name} pairs via updateSyncConfig.

- paginate-to-completeness aggregator with two-tier staleness guard (seq+summit commits,
  seq-only loading) and pLimit fan-out
- dropboxSyncState gains allowlistOptions + materialized_media_upload_types default
- reconciliation named exports per the SDS rename-handling decision table (cases 0-8),
  drift badges, saved-state banner, error/Retry state, MUI primitives
- stable empty-array fallbacks so out-of-contract sync-config responses cannot crash the page

SDS: ftn-docsnsklz sds/dropbox-materializer-participating-types-allowlist.md (amended 2026-07-06)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@caseylocker caseylocker force-pushed the feature/materializer-allowlist-picker branch from ba3cd40 to 96a59f6 Compare July 7, 2026 20:49
@caseylocker caseylocker marked this pull request as ready for review July 8, 2026 20:01
@smarcet smarcet requested review from santipalenque and smarcet July 9, 2026 18:54
Comment thread src/actions/dropbox-sync-actions.js Outdated
createAction("DUMMY"),
createAction("DUMMY"),
endpoint,
authErrorHandler

@smarcet smarcet Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker
This aggregator still passes authErrorHandler as the error handler for both getRequest calls (page 1 here, and the fan-out on line 130). The established pattern for MUI-styled features is snackbarErrorHandler (from base-actions.js) — a drop-in wrapper (authErrorHandler(err, res, setSnackbarMessage)), already used in 20+ action files including recent MUI additions (e.g. media-file-type-actions.js). It renders the same 401/403/404/412/default branches through a non-blocking MUI snackbar instead of a legacy SweetAlert modal — more consistent with this panel's Alert/Chip/FormGroup UI. Suggest swapping both authErrorHandler references (lines 105 and 130) for snackbarErrorHandler.

@caseylocker caseylocker Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped in b69149b. Both aggregator getRequest calls now use snackbarErrorHandler, and the handler identity test pins it. The two tier staleness guard is unchanged since the wrapper routes through the same seq guarded dispatch. Also swept the category: getSyncConfig and updateSyncConfig, which this round rewrites, now use snackbarErrorHandler as well; rebuildSync and resyncRoom are untouched by this PR so they keep authErrorHandler for now.

Comment thread src/actions/dropbox-sync-actions.js Outdated
createAction("DUMMY"),
createAction("DUMMY"),
endpoint,
authErrorHandler

@smarcet smarcet Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker
Same as above — swap this second authErrorHandler (the fan-out page fetch) for snackbarErrorHandler too.

@caseylocker caseylocker Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped in b69149b together with the page 1 call. Details in the thread above.

props.getLocations();
if (window.DROPBOX_MATERIALIZER_API_BASE_URL) {
props.getSyncConfig();
props.getAllMediaUploadTypesForAllowlist();

@smarcet smarcet Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker
Possible race condition: getSyncConfig() has no startLoading() of its own but unconditionally dispatches the global stopLoading() in its .then(), while getAllMediaUploadTypesForAllowlist() brackets the same global overlay with its own startLoading()/stopLoading(). Since baseState.loading is a plain boolean (not ref-counted), whichever of the two resolves first clears the overlay for both — if getSyncConfig wins, storedTypes is populated but options is still [], so every stored row briefly renders as Case 8 ("missing") with Save enabled. Chaining props.getSyncConfig().then(() => props.getAllMediaUploadTypesForAllowlist()) here shrinks that window to a single tick instead of the full multi-page fetch duration, without touching the global loading architecture.

@caseylocker caseylocker Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b69149b with the chaining you suggested, plus one hardening a second review pass caught: getLocations shares the same non ref counted overlay and usually finishes while the aggregator is still fetching, so chaining the two sync calls alone still left the window open. The allowlistOptions slice now carries a loaded flag and the panel only renders reconciled rows and enables Save once the options have actually arrived, so the missing badge flash cannot happen regardless of overlay timing. Regression tests cover the chaining order and the not loaded gating.

const optionsError = allowlistOptions?.error ?? null;

// Selection model — minimal local state, derived rendering (see SDS § State).
const [storedRows, setStoredRows] = useState([]);

@smarcet smarcet Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker
storedRows is fully derivable from storedTypes + options via reconcileAllowlist (a pure function), but it's stored in useState and synced through a useEffect. Every time storedTypes/options change (mount, post-Save, retry) React commits one render with the stale/previous storedRows before the effect fires and schedules the corrected one — an extra render pass plus a brief flash of stale reconciliation state. summit-admin-popup-dialog-pattern.md's "Derived State, Not Duplicate State" section covers exactly this: compute derived state on render, don't duplicate it in useState.

const storedRows = useMemo(
  () => reconcileAllowlist(storedTypes, options).map((row, i) => ({ ...row, uiKey: `stored-${i}` })),
  [storedTypes, options]
);

Keep uncheckedKeys/removedKeys/pickedIds as useState (genuine local UI state) with their own reset effect on [storedTypes, options].

@caseylocker caseylocker Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b69149b. storedRows is now a useMemo over storedTypes and options, and the three selection Sets stay in useState with their reset effect on the same deps, as you outlined.


const DROPBOX_STORAGE = "DropBox";

/**

@smarcet smarcet Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker
This adds ~230 lines of framework-agnostic reconciliation/normalization logic (normalizeName, isValidEntry, RECONCILE_CASE, reconcileValidEntry, reconcileAllowlist) plus a full allowlist-picker state machine (4 useState, ~7 handlers, ~150 lines of JSX) directly inside LocationListPage, which already owns the unrelated locations CRUD table. Two independent extraction opportunities:

  • reconcileAllowlist is conceptually selector-shaped — a pure (storedTypes, options) => derived rows function, the exact "result function" you'd hand to reselect's createSelector. This repo has no reselect/selectors layer though (verified: not a dependency, zero createSelector usage, zero selectors.js files anywhere), so introducing one now would be a new architectural pattern for a single caller. src/models/ is where this codebase already puts exactly this shape of pure state-derivation logic without calling it a "selector" — see models/lead-report-settings.js's normalizeLeadReportSettings/denormalizeLeadReportSettings. Suggest moving normalizeName, isValidEntry, RECONCILE_CASE, reconcileValidEntry, reconcileAllowlist there; named exports stay the same so the current unit tests import from the new path unchanged.
  • Panel UI + local state → its own child component (e.g. AllowlistPanel), rendered from LocationListPage with syncConfig/allowlistOptions/callbacks as props.

Not a correctness blocker — reuse-before-build.md permits inlining single-consumer logic — but the size/self-containment here matches the extraction precedent closely enough to be worth doing before it grows further.

@caseylocker caseylocker Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b69149b. The five helpers moved to src/models/materializer-allowlist.js next to lead-report-settings, same named exports plus DROPBOX_STORAGE, and the picker UI with its selection state is now src/components/locations/allowlist-panel.js, rendered from the page with syncConfig, allowlistOptions, syncLoading and the save and retry callbacks as props. Test ids unchanged, suite green. Recorded as a dated SDS amendment since it supersedes the 2026-07-06 consolidation entry.

@smarcet

smarcet commented Jul 9, 2026

Copy link
Copy Markdown

@coderabbitai full review

@smarcet smarcet left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker please review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pages/locations/location-list-page.js`:
- Line 357: The Save Allowlist action is missing the shared sync-in-flight
guard, so it can fire overlapping update requests with stale selection data.
Update the save button disable logic in location-list-page’s saveDisabled path
(and any related save action wiring) to also check syncLoading, matching the
Sync toggle and Rebuild button behavior. Keep the change aligned with the
existing dropboxSyncState.loading-based guards so Save is blocked whenever a
sync/config fetch or update is already running.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e701c3d-d22a-4477-a2c8-70b42db90460

📥 Commits

Reviewing files that changed from the base of the PR and between 8f4158a and 96a59f6.

📒 Files selected for processing (7)
  • src/actions/__tests__/dropbox-sync-actions.test.js
  • src/actions/dropbox-sync-actions.js
  • src/i18n/en.json
  • src/pages/locations/__tests__/location-list-page-allowlist.test.js
  • src/pages/locations/location-list-page.js
  • src/reducers/__tests__/dropbox-sync-reducer.test.js
  • src/reducers/locations/dropbox-sync-reducer.js

Comment thread src/pages/locations/location-list-page.js Outdated
…ing, guard config requests

Review round (smarcet + CodeRabbit) on the allowlist picker:

- Reconciliation helpers move to src/models/materializer-allowlist.js
  (exemplar: lead-report-settings) and the picker UI + selection state to
  src/components/locations/allowlist-panel.js; test ids unchanged.
- Aggregator and config thunks use snackbarErrorHandler (MUI snackbar)
  instead of authErrorHandler.
- Mount chains getSyncConfig before the options aggregator, and the panel
  gates rows/Save on a new allowlistOptions.loaded flag: stored rows are
  never reconciled (badged "missing") against an options list that merely
  has not arrived, regardless of global-overlay timing.
- storedRows is derived via useMemo, not duplicated in useState.
- Save/toggle/Rebuild are mutually exclusive with any in-flight config
  request: both config thunks dispatch REQUEST_SYNC_CONFIG synchronously
  before token acquisition, and share the aggregator-style two-tier
  sequence guard (isNewest/stillCurrent) so a stale response can neither
  commit cross-summit nor clear the newer request's loading flag; a new
  SYNC_CONFIG_ERROR clears loading on PUT failure without resetting the
  stored config; a superseded-but-successful PUT refetches so redux
  converges on the saved server state.

142 suites / 1255 tests green; eslint clean on changed files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@caseylocker caseylocker force-pushed the feature/materializer-allowlist-picker branch from 7535604 to b69149b Compare July 10, 2026 21:03
@caseylocker caseylocker requested a review from smarcet July 10, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants