-
Notifications
You must be signed in to change notification settings - Fork 619
[BLD-108] Dashboard: Update Claim conditions UI to handle very large snapshots #7846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BLD-108] Dashboard: Update Claim conditions UI to handle very large snapshots #7846
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughBatches CSV normalization with progress state and React Query cache updates; centralizes per-phase snapshots into a separate state with setter APIs; replaces snapshot types with SnapshotEntry, adds pagination and ENS-resolution progress in snapshot UI; airdrop UIs now surface normalization progress. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Uploader Component
participant Hook as useCsvUpload
participant Batch as Batch Processor
participant Proc as processAirdropData
participant Cache as React Query Cache
UI->>Hook: normalize(rawData)
Hook->>Batch: split into batches (size=50)
loop per batch
Batch->>Proc: process(batch)
Proc-->>Batch: normalized subset
Batch->>Hook: report progress (current/total)
Hook->>Cache: setQueryData(accumulatedResult)
end
Hook-->>UI: return normalized result + normalizeProgress
sequenceDiagram
participant Form as ClaimConditionsForm
participant Ctx as ClaimsConditionContext
participant SnapSheet as SnapshotViewerSheet
participant Submit as setClaimPhasesTx
Form->>Ctx: init phaseSnapshots from query (once)
SnapSheet->>Ctx: setPhaseSnapshot(index, snapshot)
Ctx-->>Form: phaseSnapshots updated
Form->>Form: derive claim type using phase + snapshot
Form->>Submit: onSubmit(merge phases + snapshots)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7846 +/- ##
=======================================
Coverage 56.33% 56.34%
=======================================
Files 905 905
Lines 58834 58834
Branches 4158 4150 -8
=======================================
+ Hits 33147 33151 +4
+ Misses 25582 25577 -5
- Partials 105 106 +1
🚀 New features to boost your workflow:
|
size-limit report 📦
|
e0b59be to
f543d37
Compare
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🔭 Outside diff range comments (1)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx (1)
1-1: Add 'use client' directive for a Hook-using component (per app guidelines).This file uses hooks (
useCsvUpload) and UI components, so it must be a Client Component. Our dashboard guidelines require client components to begin with 'use client'.Apply this diff at the top of the file:
+'use client'; + import { ArrowRightIcon, RefreshCcwIcon, TrashIcon } from "lucide-react";
🧹 Nitpick comments (3)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx (3)
16-19: Annotate component return type explicitly.Per our TS guidelines, add an explicit return type to the exported component.
Apply this diff:
-export function AirdropUpload(props: { +export function AirdropUpload(props: { setAirdrop: (airdrop: AirdropAddressInput[]) => void; client: ThirdwebClient; -}) { +}): JSX.Element {
50-51: Use boolean comparisons for clarity and correct typing in the ternary condition.
normalizeData.result.length && ...relies on numeric truthiness and may degrade type clarity. Prefer> 0.Apply this diff:
- {normalizeData.result.length && csvUpload.rawData.length > 0 ? ( + {normalizeData.result.length > 0 && csvUpload.rawData.length > 0 ? (
53-57: Use the local normalizeData variable for consistency and readability.Avoid re-accessing
csvUpload.normalizeQuery.dataafter storingnormalizeData.Apply this diff:
- <AirdropCSVTable - data={csvUpload.normalizeQuery.data.result.map((row) => ({ + <AirdropCSVTable + data={normalizeData.result.map((row) => ({ address: row.address ?? row.resolvedAddress, quantity: row.quantity, isValid: row.isValid, }))} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
apps/dashboard/package.json(0 hunks)apps/dashboard/src/@/hooks/useCsvUpload.ts(5 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx(4 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx(13 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx(2 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx(8 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx(1 hunks)apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- apps/dashboard/package.json
🚧 Files skipped from review as they are similar to previous changes (6)
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
- apps/dashboard/src/@/hooks/useCsvUpload.ts
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{ts,tsx}: Write idiomatic TypeScript with explicit function declarations and return types
Limit each file to one stateless, single-responsibility function for clarity
Re-use shared types from@/typesor localtypes.tsbarrels
Prefer type aliases over interface except for nominal shapes
Avoidanyandunknownunless unavoidable; narrow generics when possible
Choose composition over inheritance; leverage utility types (Partial,Pick, etc.)
Comment only ambiguous logic; avoid restating TypeScript in prose
Files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Load heavy dependencies inside async paths to keep initial bundle lean (lazy loading)
Files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
apps/{dashboard,playground-web}/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
apps/{dashboard,playground-web}/**/*.{ts,tsx}: Import UI primitives from@/components/ui/*(Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator) in dashboard and playground apps
UseNavLinkfor internal navigation with automatic active states in dashboard and playground apps
Use Tailwind CSS only – no inline styles or CSS modules
Usecn()from@/lib/utilsfor conditional class logic
Use design system tokens (e.g.,bg-card,border-border,text-muted-foreground)
Server Components (Node edge): Start files withimport "server-only";
Client Components (browser): Begin files with'use client';
Always callgetAuthToken()to retrieve JWT from cookies on server side
UseAuthorization: Bearerheader – never embed tokens in URLs
Return typed results (e.g.,Project[],User[]) – avoidany
Wrap client-side data fetching calls in React Query (@tanstack/react-query)
Use descriptive, stablequeryKeysfor React Query cache hits
ConfigurestaleTime/cacheTimein React Query based on freshness (default ≥ 60s)
Keep tokens secret via internal API routes or server actions
Never importposthog-jsin server components
Files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
.../(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
Outdated
Show resolved
Hide resolved
.../(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
Show resolved
Hide resolved
f543d37 to
713b607
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🔭 Outside diff range comments (2)
apps/dashboard/src/@/hooks/useCsvUpload.ts (2)
140-165: Do not run normalization when no CSV is loaded; gate the query and prevent refetch on focusWithout an
enabledguard, the query runs on mount with an empty dataset. This contributes to the “Resolving ENS” spinner showing before any upload (see UploadSnapshot) and wastes cycles. Also, default refetch-on-focus could re-run an expensive normalization unexpectedly.Add
enabled, setstaleTimeto avoid rework, and disablerefetchOnWindowFocus.Apply this diff:
const normalizeQuery = useQuery({ queryFn: async () => { @@ }, queryKey: ["snapshot-check-isAddress", rawData], retry: false, + enabled: rawData.length > 0, + staleTime: Infinity, + refetchOnWindowFocus: false, });
142-162: Fix progress reporting to reflect processed rows and finalize at 100%
currentis set to the batch start index (i), which under-reports progress and never explicitly reachestotal. Update after each batch toi + batch.lengthand set final progress tototalbefore returning.Apply this diff:
for (let i = 0; i < rawData.length; i += batchSize) { const batch = rawData.slice(i, i + batchSize); setNormalizeProgress({ total: rawData.length, current: i, }); const batchResults = await Promise.all( batch.map((item) => checkIsAddress({ item: item, thirdwebClient: props.client }), ), ); results.push(...batchResults); + // Update progress to include the completed batch + setNormalizeProgress({ + total: rawData.length, + current: Math.min(i + batch.length, rawData.length), + }); } + // Ensure progress ends at 100% after the final batch + setNormalizeProgress({ + total: rawData.length, + current: rawData.length, + }); + return { invalidFound: !!results.find((item) => !item?.isValid), result: processAirdropData(results), };
🧹 Nitpick comments (4)
apps/dashboard/src/@/hooks/useCsvUpload.ts (2)
102-106: Reset progress when clearing CSVWhen calling
reset(), progress should also be cleared to keep UI consistent.Apply this diff:
const reset = useCallback(() => { setRawData([]); setNoCsv(false); + setNormalizeProgress({ total: 0, current: 0 }); }, []);
163-164: Large queryKey payload; consider a stable, small key to reduce memory overheadIncluding the entire
rawDataarray (potentially 100k entries) in the queryKey increases memory and comparison overhead. Consider a small, stable key derived fromrawData(e.g., a content hash or an incrementing version) instead of the entire array.Example approach:
- Track a
dataVersionref you bump onsetRawData.- Use
["snapshot-check-isAddress", dataVersion]as the query key.- Store the actual data in component state only, not in the key.
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx (2)
125-130: Use ZERO_ADDRESS constant for consistencyPrefer the
ZERO_ADDRESSconstant over the literal zero-address string.Apply this diff:
- {item.currencyAddress === - "0x0000000000000000000000000000000000000000" || + {item.currencyAddress === ZERO_ADDRESS || !item.currencyAddress ? "Default" : item.currencyAddress}
34-41: Prefer a type alias over interface for props per repo guidelinesMinor style alignment with the codebase’s convention.
Apply this diff:
-interface SnapshotUploadProps { +type SnapshotUploadProps = { setSnapshot: (snapshot: (SnapshotEntry & { ensName?: string })[]) => void; dropType: "specific" | "any" | "overrides"; isDisabled: boolean; value?: (SnapshotEntry & { ensName?: string })[] | undefined; onClose: () => void; client: ThirdwebClient; -} +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
apps/dashboard/package.json(0 hunks)apps/dashboard/src/@/hooks/useCsvUpload.ts(5 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx(4 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx(13 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx(2 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx(8 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx(1 hunks)apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- apps/dashboard/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{ts,tsx}: Write idiomatic TypeScript with explicit function declarations and return types
Limit each file to one stateless, single-responsibility function for clarity
Re-use shared types from@/typesor localtypes.tsbarrels
Prefer type aliases over interface except for nominal shapes
Avoidanyandunknownunless unavoidable; narrow generics when possible
Choose composition over inheritance; leverage utility types (Partial,Pick, etc.)
Comment only ambiguous logic; avoid restating TypeScript in prose
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Load heavy dependencies inside async paths to keep initial bundle lean (lazy loading)
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
apps/{dashboard,playground-web}/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
apps/{dashboard,playground-web}/**/*.{ts,tsx}: Import UI primitives from@/components/ui/*(Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator) in dashboard and playground apps
UseNavLinkfor internal navigation with automatic active states in dashboard and playground apps
Use Tailwind CSS only – no inline styles or CSS modules
Usecn()from@/lib/utilsfor conditional class logic
Use design system tokens (e.g.,bg-card,border-border,text-muted-foreground)
Server Components (Node edge): Start files withimport "server-only";
Client Components (browser): Begin files with'use client';
Always callgetAuthToken()to retrieve JWT from cookies on server side
UseAuthorization: Bearerheader – never embed tokens in URLs
Return typed results (e.g.,Project[],User[]) – avoidany
Wrap client-side data fetching calls in React Query (@tanstack/react-query)
Use descriptive, stablequeryKeysfor React Query cache hits
ConfigurestaleTime/cacheTimein React Query based on freshness (default ≥ 60s)
Keep tokens secret via internal API routes or server actions
Never importposthog-jsin server components
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
🧠 Learnings (7)
📚 Learning: 2025-08-12T20:44:48.474Z
Learnt from: MananTank
PR: thirdweb-dev/js#7846
File: apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx:28-34
Timestamp: 2025-08-12T20:44:48.474Z
Learning: In the useCsvUpload hook (`apps/dashboard/src/@/hooks/useCsvUpload.ts`), the normalizeProgress state is initialized with `useState({ total: 0, current: 0 })`, so it's never undefined and always starts with 0. No need to guard against undefined values when using csvUpload.normalizeProgress.current or csvUpload.normalizeProgress.total.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.ts
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Interactive UI that relies on hooks (`useState`, `useEffect`, React Query, wallet hooks).
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Use React Query (`tanstack/react-query`) for all client data fetching.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.ts
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Anything that consumes hooks from `tanstack/react-query` or thirdweb SDKs.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:19:55.613Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-18T19:19:55.613Z
Learning: Applies to apps/{dashboard,playground-web}/**/*.{ts,tsx} : Wrap client-side data fetching calls in React Query (`tanstack/react-query`)
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.ts
📚 Learning: 2025-07-31T16:17:42.753Z
Learnt from: MananTank
PR: thirdweb-dev/js#7768
File: apps/playground-web/src/app/navLinks.ts:1-1
Timestamp: 2025-07-31T16:17:42.753Z
Learning: Configuration files that import and reference React components (like icon components from lucide-react) need the "use client" directive, even if they primarily export static data, because the referenced components need to be executed in a client context when used by other client components.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-07T21:21:47.488Z
Learnt from: saminacodes
PR: thirdweb-dev/js#7543
File: apps/portal/src/app/pay/page.mdx:4-4
Timestamp: 2025-07-07T21:21:47.488Z
Learning: In the thirdweb-dev/js repository, lucide-react icons must be imported with the "Icon" suffix (e.g., ExternalLinkIcon, RocketIcon) as required by the new linting rule, contrary to the typical lucide-react convention of importing without the suffix.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
🧬 Code Graph Analysis (1)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx (4)
packages/thirdweb/src/exports/thirdweb.ts (1)
ThirdwebClient(25-25)apps/dashboard/src/@/components/blocks/pagination-buttons.tsx (1)
PaginationButtons(18-221)apps/dashboard/src/@/hooks/useCsvUpload.ts (1)
useCsvUpload(93-205)packages/thirdweb/src/react/web/ui/components/Spinner.tsx (1)
Spinner(11-34)
...board)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
Show resolved
Hide resolved
...board)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
Show resolved
Hide resolved
...board)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
Show resolved
Hide resolved
Merge activity
|
…snapshots (#7846) <!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the handling of CSV uploads and snapshots in the dashboard, improving user feedback during data processing, and refining the management of claim conditions within the application. ### Detailed summary - Updated `package.json` to remove `p-limit`. - Improved UI for CSV upload status with additional messages. - Refactored snapshot management in `ClaimConditionsForm`. - Added state for `phaseSnapshots` to optimize performance. - Enhanced snapshot handling in `SnapshotUpload` component. - Updated data processing logic in `useCsvUpload` to track progress. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - CSV upload shows real-time normalization progress and ENS resolution status across relevant upload screens; pending states now display current/total progress. - Per-phase snapshot management with centralized state; improved snapshot viewer with view/edit modes, pagination, and clearer labels. - Improved validation: invalid addresses highlighted with tooltips and options to remove or proceed. - Refactor - CSV normalization switched to batched processing for smoother, more responsive uploads. - Snapshot data moved out of per-field form state to reduce rendering overhead. - Chores - Removed an unused dependency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
713b607 to
a36c850
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🔭 Outside diff range comments (1)
apps/dashboard/src/@/hooks/useCsvUpload.ts (1)
141-165: Gate normalization query and fix progress updates to reflect processed items
- Avoid kicking off the normalization query on mount when there is no CSV by adding enabled: rawData.length > 0.
- Progress should advance after each batch is actually processed (i + batch.length) and finish at 100%.
Apply this diff:
const normalizeQuery = useQuery({ queryFn: async () => { - const batchSize = 50; - const results = []; + const batchSize = 50; + type Normalized = Awaited<ReturnType<typeof checkIsAddress<T>>>; + const results: Normalized[] = []; for (let i = 0; i < rawData.length; i += batchSize) { const batch = rawData.slice(i, i + batchSize); - setNormalizeProgress({ - total: rawData.length, - current: i, - }); - const batchResults = await Promise.all( + const batchResults = await Promise.all( batch.map((item) => checkIsAddress({ item: item, thirdwebClient: props.client }), ), ); results.push(...batchResults); + setNormalizeProgress({ + total: rawData.length, + current: Math.min(i + batch.length, rawData.length), + }); } + // ensure we end at 100% + setNormalizeProgress({ + total: rawData.length, + current: rawData.length, + }); return { invalidFound: !!results.find((item) => !item?.isValid), result: processAirdropData(results), }; }, queryKey: ["snapshot-check-isAddress", rawData], retry: false, + enabled: rawData.length > 0, });
♻️ Duplicate comments (4)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx (4)
1-1: Add 'use client' to enable hooks in this componentThis file uses React state/hooks and client-only UI. It must be a Client Component.
Apply this diff at the very top:
+ "use client"; import { ArrowRightIcon,
164-175: Avoid showing the ENS spinner before any CSV is uploadedGate the spinner on rawData.length > 0 to prevent a loading screen on initial mount. Pair this with enabled: rawData.length > 0 in the hook.
Apply this diff:
- if (!normalizeData) { + if (csvUpload.rawData.length > 0 && !normalizeData) { return ( <div className="flex min-h-[400px] w-full flex-col grow items-center justify-center"> <Spinner className="size-10" /> <p className="text-base text-foreground mt-5">Resolving ENS</p> <p className="text-sm text-muted-foreground mt-2"> {csvUpload.normalizeProgress.current} /{" "} {csvUpload.normalizeProgress.total} </p> </div> ); }
196-205: Memoize the table data to avoid O(n) re-map on every renderFor very large snapshots, re-mapping on each render is costly. Compute it once per normalizeData change.
Apply this diff:
- <SnapshotDataTable - data={csvUpload.normalizeQuery.data.result.map((x) => ({ - address: x.resolvedAddress, - ensName: x.address.startsWith("0x") ? undefined : x.address, - maxClaimable: x.maxClaimable, - currencyAddress: x.currencyAddress, - price: x.price, - }))} - /> + <SnapshotDataTable data={tableData} />And add near the top of UploadSnapshot (after normalizeData):
const tableData = useMemo( () => (normalizeData?.result ?? []).map((x) => ({ address: x.resolvedAddress, ensName: x.address.startsWith("0x") ? undefined : x.address, maxClaimable: x.maxClaimable, currencyAddress: x.currencyAddress, price: x.price, })), [normalizeData?.result], );
7-8: Import useMemo and use it for large table dataWe'll memoize the heavy mapped data used by SnapshotDataTable.
Apply this diff:
-import { useState } from "react"; +import { useMemo, useState } from "react";
🧹 Nitpick comments (2)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx (1)
130-153: Remove unreachable return "custom" and simplify branchingThe final return "custom" is unreachable because the function already returns for both snapshot and !snapshot branches. Simplify to a single snapshot branch.
Apply this diff:
const getClaimConditionTypeFromPhase = ( phase: Omit<ClaimConditionInput, "snapshot">, snapshot: SnapshotEntry[] | undefined, ): ClaimConditionType => { if (!snapshot) { return "public"; } - if (snapshot) { - if ( - phase.maxClaimablePerWallet?.toString() === "0" && - phase.price === "0" && - snapshot.length === 1 && - snapshot.some((a) => a.maxClaimable === "unlimited") - ) { - return "creator"; - } - if (phase.maxClaimablePerWallet?.toString() === "0") { - return "specific"; - } - return "overrides"; - } - return "custom"; + if ( + phase.maxClaimablePerWallet?.toString() === "0" && + phase.price === "0" && + snapshot.length === 1 && + snapshot.some((a) => a.maxClaimable === "unlimited") + ) { + return "creator"; + } + if (phase.maxClaimablePerWallet?.toString() === "0") { + return "specific"; + } + return "overrides"; };apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx (1)
54-147: Pagination UI looks good; consider raising page size for power usersThe table paginates correctly and only slices the current page. For 100k rows, offering a 25/50/100 per-page option can improve scanning speed. Optional enhancement.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
apps/dashboard/package.json(0 hunks)apps/dashboard/src/@/hooks/useCsvUpload.ts(5 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx(4 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx(13 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx(2 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx(8 hunks)apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx(1 hunks)apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- apps/dashboard/package.json
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
- apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx
- apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{ts,tsx}: Write idiomatic TypeScript with explicit function declarations and return types
Limit each file to one stateless, single-responsibility function for clarity
Re-use shared types from@/typesor localtypes.tsbarrels
Prefer type aliases over interface except for nominal shapes
Avoidanyandunknownunless unavoidable; narrow generics when possible
Choose composition over inheritance; leverage utility types (Partial,Pick, etc.)
Comment only ambiguous logic; avoid restating TypeScript in prose
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsxapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Load heavy dependencies inside async paths to keep initial bundle lean (lazy loading)
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsxapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
apps/{dashboard,playground-web}/**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
apps/{dashboard,playground-web}/**/*.{ts,tsx}: Import UI primitives from@/components/ui/*(Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator) in dashboard and playground apps
UseNavLinkfor internal navigation with automatic active states in dashboard and playground apps
Use Tailwind CSS only – no inline styles or CSS modules
Usecn()from@/lib/utilsfor conditional class logic
Use design system tokens (e.g.,bg-card,border-border,text-muted-foreground)
Server Components (Node edge): Start files withimport "server-only";
Client Components (browser): Begin files with'use client';
Always callgetAuthToken()to retrieve JWT from cookies on server side
UseAuthorization: Bearerheader – never embed tokens in URLs
Return typed results (e.g.,Project[],User[]) – avoidany
Wrap client-side data fetching calls in React Query (@tanstack/react-query)
Use descriptive, stablequeryKeysfor React Query cache hits
ConfigurestaleTime/cacheTimein React Query based on freshness (default ≥ 60s)
Keep tokens secret via internal API routes or server actions
Never importposthog-jsin server components
Files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsxapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
🧠 Learnings (15)
📚 Learning: 2025-08-12T20:44:48.474Z
Learnt from: MananTank
PR: thirdweb-dev/js#7846
File: apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx:28-34
Timestamp: 2025-08-12T20:44:48.474Z
Learning: In the useCsvUpload hook (`apps/dashboard/src/@/hooks/useCsvUpload.ts`), the normalizeProgress state is initialized with `useState({ total: 0, current: 0 })`, so it's never undefined and always starts with 0. No need to guard against undefined values when using csvUpload.normalizeProgress.current or csvUpload.normalizeProgress.total.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Interactive UI that relies on hooks (`useState`, `useEffect`, React Query, wallet hooks).
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Use React Query (`tanstack/react-query`) for all client data fetching.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.ts
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Anything that consumes hooks from `tanstack/react-query` or thirdweb SDKs.
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.tsapps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:19:55.613Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-18T19:19:55.613Z
Learning: Applies to apps/{dashboard,playground-web}/**/*.{ts,tsx} : Wrap client-side data fetching calls in React Query (`tanstack/react-query`)
Applied to files:
apps/dashboard/src/@/hooks/useCsvUpload.ts
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/components/*.client.tsx : Client components must start with `'use client';` before imports.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Components that listen to user events, animations or live updates.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:19:55.613Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-18T19:19:55.613Z
Learning: Applies to apps/{dashboard,playground-web}/**/*.{ts,tsx} : Client Components (browser): Begin files with `'use client';`
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : Pages requiring fast transitions where data is prefetched on the client.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*client.tsx : When you need access to browser APIs (localStorage, window, IntersectionObserver etc.).
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*.{tsx,jsx} : Reuse core UI primitives; avoid re-implementing buttons, cards, modals.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*.{tsx,jsx} : For notices & skeletons rely on `AnnouncementBanner`, `GenericLoadingPage`, `EmptyStateCard`.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-18T19:20:32.530Z
Learnt from: CR
PR: thirdweb-dev/js#0
File: .cursor/rules/dashboard.mdc:0-0
Timestamp: 2025-07-18T19:20:32.530Z
Learning: Applies to dashboard/**/*.{tsx,jsx} : Keep components pure; fetch data outside (server component or hook) and pass it down via props.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-31T16:17:42.753Z
Learnt from: MananTank
PR: thirdweb-dev/js#7768
File: apps/playground-web/src/app/navLinks.ts:1-1
Timestamp: 2025-07-31T16:17:42.753Z
Learning: Configuration files that import and reference React components (like icon components from lucide-react) need the "use client" directive, even if they primarily export static data, because the referenced components need to be executed in a client context when used by other client components.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
📚 Learning: 2025-07-07T21:21:47.488Z
Learnt from: saminacodes
PR: thirdweb-dev/js#7543
File: apps/portal/src/app/pay/page.mdx:4-4
Timestamp: 2025-07-07T21:21:47.488Z
Learning: In the thirdweb-dev/js repository, lucide-react icons must be imported with the "Icon" suffix (e.g., ExternalLinkIcon, RocketIcon) as required by the new linting rule, contrary to the typical lucide-react convention of importing without the suffix.
Applied to files:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
🧬 Code Graph Analysis (2)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx (3)
packages/thirdweb/src/exports/thirdweb.ts (1)
ThirdwebClient(25-25)apps/dashboard/src/@/components/blocks/pagination-buttons.tsx (1)
PaginationButtons(18-221)apps/dashboard/src/@/hooks/useCsvUpload.ts (1)
useCsvUpload(93-205)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx (1)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/phase.tsx (1)
ClaimConditionsPhase(24-172)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: E2E Tests (pnpm, esbuild)
- GitHub Check: Size
- GitHub Check: E2E Tests (pnpm, vite)
- GitHub Check: E2E Tests (pnpm, webpack)
- GitHub Check: Lint Packages
- GitHub Check: Build Packages
- GitHub Check: Unit Tests
- GitHub Check: Socket Security: Pull Request Alerts
- GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/dashboard/src/@/hooks/useCsvUpload.ts (2)
1-1: LGTM on react-query client usageBringing useQueryClient in to update cache for removeInvalid is a good direction and avoids unnecessary refetches.
203-204: Expose normalizeProgress: good API additionSurfacing normalizeProgress enables responsive UX for ENS resolution on large datasets.
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx (1)
174-179: Context additions look solidExposing phaseSnapshots and setPhaseSnapshot via context aligns with the separation of large snapshot state from form state and improves performance.
| const [normalizeProgress, setNormalizeProgress] = useState({ | ||
| total: 0, | ||
| current: 0, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Reset normalizeProgress on reset to avoid stale progress UI
Currently reset() clears rawData/noCsv but normalizeProgress remains at the last values, which can render misleading progress numbers on subsequent uploads. Initialize a constant and reuse it in both state init and reset.
Follow-up change outside the selected range (add near the hook top and use in reset):
const initialProgress = { total: 0, current: 0 };
const [normalizeProgress, setNormalizeProgress] = useState(initialProgress);
// inside reset():
setNormalizeProgress(initialProgress);🤖 Prompt for AI Agents
In apps/dashboard/src/@/hooks/useCsvUpload.ts around lines 135 to 139,
normalizeProgress is initialized but not reset by reset(), causing stale
progress UI; define a shared initialProgress constant at the top of the hook
(e.g. const initialProgress = { total: 0, current: 0 }), use it to initialize
useState (useState(initialProgress)) and call
setNormalizeProgress(initialProgress) inside reset() so progress is cleared when
resetting.
| const filteredData = normalizeQuery.data?.result.filter( | ||
| ({ isValid }) => isValid, | ||
| (d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS, | ||
| ); | ||
| // double type assertion is save here because we don't really use this variable (only check for its length) | ||
| // Also filteredData's type is the superset of T[] | ||
| setRawData(filteredData as unknown as T[]); | ||
| }, [normalizeQuery.data?.result]); | ||
|
|
||
| if (filteredData && normalizeQuery.data) { | ||
| // Directly update the query result instead of setting new state to avoid triggering refetch | ||
| queryClient.setQueryData(["snapshot-check-isAddress", rawData], { | ||
| ...normalizeQuery.data, | ||
| result: filteredData, | ||
| invalidFound: false, // Since we removed all invalid items | ||
| }); | ||
| } | ||
| }, [normalizeQuery.data, queryClient, rawData]); | ||
|
|
||
| const processData = useCallback( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Heavy queryKey with rawData array may hurt performance on 100k rows
Putting the entire rawData array in the queryKey forces TanStack Query to hash/compare a huge structure, which can be costly. Prefer a compact, stable key and trigger refetches explicitly.
Two options:
- Simple: use only the length (plus a version) in the key and refetch on rawData changes.
- Accurate: compute a lightweight hash (e.g., of first/last N rows + length).
Example minimal change:
- queryKey: ["snapshot-check-isAddress", rawData],
+ queryKey: ["snapshot-check-isAddress", rawData.length],And trigger refetch when rawData changes:
useEffect(() => {
if (rawData.length > 0) {
normalizeQuery.refetch();
}
}, [rawData.length]); // keep dependency lightweight#!/bin/bash
set -eux
# Locate the useCsvUpload.ts file
file=$(fd --full-path useCsvUpload.ts)
echo "Inspecting file: $file"
# Show lines around the useQuery declaration to inspect queryKey
rg -n -C3 "useQuery" "$file"
# Show all occurrences of queryKey usage in this file
rg -n "queryKey" "$file"
# Show where rawData is defined or referenced
rg -n "rawData" "$file"Action Required: Optimize queryKey for Large rawData
Including the full rawData array in your queryKey forces TanStack Query to deep-compare/hash a potentially huge structure on each operation, which can severely impact performance for ~100k-row CSVs.
Affected location:
- File:
apps/dashboard/src/@/hooks/useCsvUpload.ts - Lines: 167–181
Suggested changes:
- // heavy key: deep-compares entire array
- queryClient.setQueryData(["snapshot-check-isAddress", rawData], {
+ // lightweight key: only uses array length
+ queryClient.setQueryData(["snapshot-check-isAddress", rawData.length], {
...normalizeQuery.data,
result: filteredData,
invalidFound: false,
});And ensure the query refetches when rawData changes:
useEffect(() => {
if (rawData.length > 0) {
normalizeQuery.refetch();
}
}, [rawData.length]); // lightweight dependencyThis keeps your cache key compact and stable while still updating the query whenever the CSV payload changes.
🤖 Prompt for AI Agents
In apps/dashboard/src/@/hooks/useCsvUpload.ts around lines 167–181 the TanStack
Query key currently includes the full rawData array which causes expensive deep
hashing for large CSVs; replace the heavy queryKey that contains rawData with a
lightweight stable identifier (for example rawData.length or a short hash) when
calling useQuery and setQueryData, and update setQueryData to use that new
compact key; additionally add a useEffect that triggers normalizeQuery.refetch()
when rawData.length changes (or when the chosen lightweight identifier changes)
so the query refreshes when the CSV payload changes.
| const isSnapshotInitialized = useRef(false); | ||
| // save snapshots in a separate state instead of form state because it makes rendering very slow for large snapshots (10k, 100k rows etc) | ||
| // eslint-disable-next-line no-restricted-syntax | ||
| useEffect(() => { | ||
| if (isSnapshotInitialized.current || !claimConditionsQuery.data) { | ||
| return; | ||
| } | ||
|
|
||
| const newPhaseSnapshots: Record<number, SnapshotEntry[] | undefined> = {}; | ||
|
|
||
| claimConditionsQuery.data.forEach((phase, idx) => { | ||
| const snapshot = phase.snapshot?.map( | ||
| ({ address, maxClaimable, price, currencyAddress }) => ({ | ||
| address, | ||
| currencyAddress: currencyAddress || undefined, | ||
| maxClaimable: maxClaimable || "0", | ||
| price: price || undefined, | ||
| }), | ||
| ); | ||
| newPhaseSnapshots[idx] = snapshot; | ||
| }); | ||
|
|
||
| setPhaseSnapshots(newPhaseSnapshots); | ||
| isSnapshotInitialized.current = true; | ||
| }, [claimConditionsQuery.data]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Snapshot initialization never re-runs on contract/token change
isSnapshotInitialized prevents re-initialization forever in this component instance. If the user switches contract or tokenId, snapshot state can become stale. Reset the guard on identity changes.
Suggested addition near this effect:
useEffect(() => {
// reset snapshot initialization when context identity changes
isSnapshotInitialized.current = false;
setPhaseSnapshots({});
}, [contract.address, tokenId]);Reset snapshot initialization on contract/token identity changes
The existing effect (lines 251–276) guards against re-running snapshot setup with isSnapshotInitialized, but it never clears that guard when the user switches contracts or tokenId. As a result, phaseSnapshots can become stale after an identity change.
• File:
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
• Lines: 251–276
Suggested fix—add this alongside the current effect to reset snapshots whenever contract.address or tokenId change:
useEffect(() => {
// Reset snapshot initialization when contract or token ID changes
isSnapshotInitialized.current = false;
setPhaseSnapshots({});
}, [contract.address, tokenId]);🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
around lines 251–276 the isSnapshotInitialized ref is never cleared when the
user switches contract or tokenId, causing stale phaseSnapshots; add a new
useEffect that runs on [contract.address, tokenId] which sets
isSnapshotInitialized.current = false and calls setPhaseSnapshots({}) to reset
the guard and clear snapshots whenever the contract identity or token changes.
| const transformedQueryData = useMemo(() => { | ||
| return (claimConditionsQuery.data || []) | ||
| .map((phase, idx) => ({ | ||
| ...phase, | ||
| currencyAddress: phase.currencyAddress?.toLowerCase() || "0", | ||
| currencyMetadata: { | ||
| ...phase.currencyMetadata, | ||
| value: phase.currencyMetadata.value?.toString() || "0", | ||
| }, | ||
| fromSdk: true, | ||
| isEditing: false, | ||
| maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0", | ||
| maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", | ||
| merkleRootHash: (phase.merkleRootHash || "") as string, | ||
| metadata: { | ||
| ...phase.metadata, | ||
| name: phase?.metadata?.name || `Phase ${idx + 1}`, | ||
| }, | ||
| price: phase.currencyMetadata.displayValue, | ||
| snapshot: phase.snapshot?.map( | ||
| ({ address, maxClaimable, price, currencyAddress }) => ({ | ||
| address, | ||
| currencyAddress: currencyAddress || undefined, | ||
| maxClaimable: maxClaimable || "0", | ||
| price: price || undefined, | ||
| }), | ||
| ), | ||
| startTime: new Date(phase.startTime), | ||
| })) | ||
| .map((phase, idx) => { | ||
| return { | ||
| ...phase, | ||
| currencyAddress: phase.currencyAddress?.toLowerCase() || "0", | ||
| currencyMetadata: { | ||
| ...phase.currencyMetadata, | ||
| value: phase.currencyMetadata.value?.toString() || "0", | ||
| }, | ||
| fromSdk: true, | ||
| isEditing: false, | ||
| maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0", | ||
| maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", | ||
| merkleRootHash: (phase.merkleRootHash || "") as string, | ||
| metadata: { | ||
| ...phase.metadata, | ||
| name: phase?.metadata?.name || `Phase ${idx + 1}`, | ||
| }, | ||
| price: phase.currencyMetadata.displayValue, | ||
| snapshot: undefined, // Do not save snapshot in form data - its slow | ||
| startTime: new Date(phase.startTime), | ||
| }; | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currency fallback "0" is not a valid address; default to NATIVE_TOKEN_ADDRESS
Using "0" can propagate an invalid address into form state. Prefer the known NATIVE_TOKEN_ADDRESS (already used in DEFAULT_PHASE).
Apply this diff:
- currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
+ currencyAddress:
+ phase.currencyAddress?.toLowerCase() || NATIVE_TOKEN_ADDRESS,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const transformedQueryData = useMemo(() => { | |
| return (claimConditionsQuery.data || []) | |
| .map((phase, idx) => ({ | |
| ...phase, | |
| currencyAddress: phase.currencyAddress?.toLowerCase() || "0", | |
| currencyMetadata: { | |
| ...phase.currencyMetadata, | |
| value: phase.currencyMetadata.value?.toString() || "0", | |
| }, | |
| fromSdk: true, | |
| isEditing: false, | |
| maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0", | |
| maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", | |
| merkleRootHash: (phase.merkleRootHash || "") as string, | |
| metadata: { | |
| ...phase.metadata, | |
| name: phase?.metadata?.name || `Phase ${idx + 1}`, | |
| }, | |
| price: phase.currencyMetadata.displayValue, | |
| snapshot: phase.snapshot?.map( | |
| ({ address, maxClaimable, price, currencyAddress }) => ({ | |
| address, | |
| currencyAddress: currencyAddress || undefined, | |
| maxClaimable: maxClaimable || "0", | |
| price: price || undefined, | |
| }), | |
| ), | |
| startTime: new Date(phase.startTime), | |
| })) | |
| .map((phase, idx) => { | |
| return { | |
| ...phase, | |
| currencyAddress: phase.currencyAddress?.toLowerCase() || "0", | |
| currencyMetadata: { | |
| ...phase.currencyMetadata, | |
| value: phase.currencyMetadata.value?.toString() || "0", | |
| }, | |
| fromSdk: true, | |
| isEditing: false, | |
| maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0", | |
| maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", | |
| merkleRootHash: (phase.merkleRootHash || "") as string, | |
| metadata: { | |
| ...phase.metadata, | |
| name: phase?.metadata?.name || `Phase ${idx + 1}`, | |
| }, | |
| price: phase.currencyMetadata.displayValue, | |
| snapshot: undefined, // Do not save snapshot in form data - its slow | |
| startTime: new Date(phase.startTime), | |
| }; | |
| }) | |
| const transformedQueryData = useMemo(() => { | |
| return (claimConditionsQuery.data || []) | |
| .map((phase, idx) => { | |
| return { | |
| ...phase, | |
| currencyAddress: | |
| phase.currencyAddress?.toLowerCase() || NATIVE_TOKEN_ADDRESS, | |
| currencyMetadata: { | |
| ...phase.currencyMetadata, | |
| value: phase.currencyMetadata.value?.toString() || "0", | |
| }, | |
| fromSdk: true, | |
| isEditing: false, | |
| maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0", | |
| maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", | |
| merkleRootHash: (phase.merkleRootHash || "") as string, | |
| metadata: { | |
| ...phase.metadata, | |
| name: phase?.metadata?.name || `Phase ${idx + 1}`, | |
| }, | |
| price: phase.currencyMetadata.displayValue, | |
| snapshot: undefined, // Do not save snapshot in form data - its slow | |
| startTime: new Date(phase.startTime), | |
| }; | |
| }) |
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
around lines 277 to 300, the code falls back to the string "0" for
currencyAddress which is not a valid address; replace that fallback with the
project constant NATIVE_TOKEN_ADDRESS (ensure it is imported if not already) and
use .toLowerCase() on it consistently, i.e. change the currencyAddress fallback
from "0" to NATIVE_TOKEN_ADDRESS, and likewise ensure any other numeric/string
"0" address fallbacks in this mapped object follow the same pattern to match
DEFAULT_PHASE usage.
| {controlledFields.map((field, index) => { | ||
| const dropType: DropType = field.snapshot | ||
| const snapshot = phaseSnapshots[index]; | ||
| const dropType: DropType = snapshot | ||
| ? field.maxClaimablePerWallet?.toString() === "0" | ||
| ? "specific" | ||
| : "overrides" | ||
| : "any"; | ||
|
|
||
| const claimConditionType = getClaimConditionTypeFromPhase(field); | ||
| const claimConditionType = getClaimConditionTypeFromPhase( | ||
| field, | ||
| snapshot, | ||
| ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid re-mapping large snapshots on every render; compute only when the sheet is open
For 10k–100k rows, mapping to snapshotValue each render is expensive. Compute only when the corresponding sheet is open.
Apply this diff:
- const snapshotValue = snapshot?.map((v) =>
- typeof v === "string"
- ? {
- address: v,
- currencyAddress: ZERO_ADDRESS,
- maxClaimable: "unlimited",
- price: "unlimited",
- }
- : {
- ...v,
- currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
- maxClaimable: v?.maxClaimable?.toString() || "unlimited",
- price: v?.price?.toString() || "unlimited",
- },
- );
+ const snapshotValue = useMemo(() => {
+ if (openSnapshotIndex !== index) return undefined;
+ return snapshot?.map((v) =>
+ typeof v === "string"
+ ? {
+ address: v,
+ currencyAddress: ZERO_ADDRESS,
+ maxClaimable: "unlimited",
+ price: "unlimited",
+ }
+ : {
+ ...v,
+ currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
+ maxClaimable: v?.maxClaimable?.toString() || "unlimited",
+ price: v?.price?.toString() || "unlimited",
+ },
+ );
+ }, [snapshot, openSnapshotIndex, index]);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In the file
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx
around lines 496 to 508, the code is mapping over the `phaseSnapshots` array on
every render, which can be expensive for large datasets (10k-100k rows). To
optimize performance, only compute the `dropType` and `claimConditionType` when
the corresponding sheet is open. Refactor the code to move the computation of
these values inside a conditional that checks if the sheet is open, to avoid
unnecessary processing when the sheet is not visible.
| onRemove={() => { | ||
| formFields.remove(index); | ||
| // Clean up snapshot when phase is removed | ||
| setPhaseSnapshot(index, undefined); | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Phase snapshot index drift when removing phases (use stable reindexing)
Using phase index as the key means removing a phase shifts indices. Setting phaseSnapshots[index] = undefined leaves snapshots after index misaligned. Reindex keys > index down by one.
Apply this diff:
onRemove={() => {
formFields.remove(index);
- // Clean up snapshot when phase is removed
- setPhaseSnapshot(index, undefined);
+ // Clean up and reindex snapshots when a phase is removed
+ setPhaseSnapshots((prev) => {
+ const next: Record<number, SnapshotEntry[] | undefined> = {};
+ Object.keys(prev)
+ .map((k) => Number(k))
+ .sort((a, b) => a - b)
+ .forEach((k) => {
+ if (k < index) next[k] = prev[k];
+ else if (k > index) next[k - 1] = prev[k];
+ });
+ return next;
+ });
}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onRemove={() => { | |
| formFields.remove(index); | |
| // Clean up snapshot when phase is removed | |
| setPhaseSnapshot(index, undefined); | |
| }} | |
| onRemove={() => { | |
| formFields.remove(index); | |
| // Clean up and reindex snapshots when a phase is removed | |
| setPhaseSnapshots((prev) => { | |
| const next: Record<number, SnapshotEntry[] | undefined> = {}; | |
| Object.keys(prev) | |
| .map((k) => Number(k)) | |
| .sort((a, b) => a - b) | |
| .forEach((k) => { | |
| if (k < index) next[k] = prev[k]; | |
| else if (k > index) next[k - 1] = prev[k]; | |
| }); | |
| return next; | |
| }); | |
| }} |

PR-Codex overview
This PR focuses on enhancing the handling of CSV uploads and snapshots in the dashboard application, improving UI elements, and optimizing performance by managing state more efficiently.
Detailed summary
p-limitdependency.phaseSnapshotsstate to manage snapshots more efficiently.setPhaseSnapshotfor updating snapshots.useCsvUploadfor better performance.Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Chores