Skip to content

Commit a36c850

Browse files
committed
[BLD-108] Dashboard: Update Claim conditions UI to handle very large 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 -->
1 parent 59a8dad commit a36c850

File tree

9 files changed

+394
-257
lines changed

9 files changed

+394
-257
lines changed

apps/dashboard/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"next-themes": "^0.4.6",
4444
"nextjs-toploader": "^1.6.12",
4545
"nuqs": "^2.4.3",
46-
"p-limit": "^6.2.0",
4746
"papaparse": "^5.5.3",
4847
"pluralize": "^8.0.0",
4948
"posthog-js": "1.256.1",

apps/dashboard/src/@/hooks/useCsvUpload.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { useQuery } from "@tanstack/react-query";
2-
import pLimit from "p-limit";
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
32
import Papa from "papaparse";
43
import { useCallback, useState } from "react";
54
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
@@ -95,6 +94,7 @@ export function useCsvUpload<
9594
// Always gonna need the wallet address
9695
T extends { address: string },
9796
>(props: Props<T>) {
97+
const queryClient = useQueryClient();
9898
const [rawData, setRawData] = useState<
9999
T[] | Array<T & { [key in string]: unknown }>
100100
>(props.defaultRawData || []);
@@ -132,16 +132,29 @@ export function useCsvUpload<
132132
[props.csvParser],
133133
);
134134

135+
const [normalizeProgress, setNormalizeProgress] = useState({
136+
total: 0,
137+
current: 0,
138+
});
139+
135140
const normalizeQuery = useQuery({
136141
queryFn: async () => {
137-
const limit = pLimit(50);
138-
const results = await Promise.all(
139-
rawData.map((item) => {
140-
return limit(() =>
142+
const batchSize = 50;
143+
const results = [];
144+
for (let i = 0; i < rawData.length; i += batchSize) {
145+
const batch = rawData.slice(i, i + batchSize);
146+
setNormalizeProgress({
147+
total: rawData.length,
148+
current: i,
149+
});
150+
const batchResults = await Promise.all(
151+
batch.map((item) =>
141152
checkIsAddress({ item: item, thirdwebClient: props.client }),
142-
);
143-
}),
144-
);
153+
),
154+
);
155+
results.push(...batchResults);
156+
}
157+
145158
return {
146159
invalidFound: !!results.find((item) => !item?.isValid),
147160
result: processAirdropData(results),
@@ -153,12 +166,18 @@ export function useCsvUpload<
153166

154167
const removeInvalid = useCallback(() => {
155168
const filteredData = normalizeQuery.data?.result.filter(
156-
({ isValid }) => isValid,
169+
(d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS,
157170
);
158-
// double type assertion is save here because we don't really use this variable (only check for its length)
159-
// Also filteredData's type is the superset of T[]
160-
setRawData(filteredData as unknown as T[]);
161-
}, [normalizeQuery.data?.result]);
171+
172+
if (filteredData && normalizeQuery.data) {
173+
// Directly update the query result instead of setting new state to avoid triggering refetch
174+
queryClient.setQueryData(["snapshot-check-isAddress", rawData], {
175+
...normalizeQuery.data,
176+
result: filteredData,
177+
invalidFound: false, // Since we removed all invalid items
178+
});
179+
}
180+
}, [normalizeQuery.data, queryClient, rawData]);
162181

163182
const processData = useCallback(
164183
(data: T[]) => {
@@ -181,5 +200,6 @@ export function useCsvUpload<
181200
removeInvalid,
182201
reset,
183202
setFiles,
203+
normalizeProgress,
184204
};
185205
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,31 @@ export const ClaimerSelection = () => {
2727
setOpenSnapshotIndex: setOpenIndex,
2828
isAdmin,
2929
claimConditionType,
30+
phaseSnapshots,
31+
setPhaseSnapshot,
3032
} = useClaimConditionsFormContext();
3133

3234
const handleClaimerChange = (value: string) => {
3335
const val = value as "any" | "specific" | "overrides";
3436

3537
if (val === "any") {
36-
form.setValue(`phases.${phaseIndex}.snapshot`, undefined);
38+
setPhaseSnapshot(phaseIndex, undefined);
3739
} else {
3840
if (val === "specific") {
3941
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0);
4042
}
4143
if (val === "overrides" && field.maxClaimablePerWallet !== 1) {
4244
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1);
4345
}
44-
form.setValue(`phases.${phaseIndex}.snapshot`, []);
46+
setPhaseSnapshot(phaseIndex, []);
4547
setOpenIndex(phaseIndex);
4648
}
4749
};
4850

4951
let helperText: React.ReactNode;
5052

5153
const disabledSnapshotButton = isAdmin && formDisabled;
54+
const snapshot = phaseSnapshots[phaseIndex];
5255

5356
if (dropType === "specific") {
5457
helperText = (
@@ -87,10 +90,7 @@ export const ClaimerSelection = () => {
8790

8891
return (
8992
<FormFieldSetup
90-
errorMessage={
91-
form.getFieldState(`phases.${phaseIndex}.snapshot`, form.formState)
92-
?.error?.message
93-
}
93+
errorMessage={undefined}
9494
helperText={helperText}
9595
label={label}
9696
isRequired={false}
@@ -117,7 +117,7 @@ export const ClaimerSelection = () => {
117117
)}
118118

119119
{/* Edit or See Snapshot */}
120-
{field.snapshot ? (
120+
{snapshot ? (
121121
<div className="flex items-center gap-3">
122122
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
123123
<Button
@@ -133,17 +133,16 @@ export const ClaimerSelection = () => {
133133
<div
134134
className={cn(
135135
"flex gap-2 items-center",
136-
field.snapshot?.length === 0
136+
snapshot?.length === 0
137137
? "text-muted-foreground"
138138
: "text-green-600 dark:text-green-500",
139139
disabledSnapshotButton ? "opacity-50" : "",
140140
)}
141141
>
142142
<div className="size-2 bg-current rounded-full" />
143143
<span className="text-sm">
144-
{field.snapshot?.length}{" "}
145-
{field.snapshot?.length === 1 ? "address" : "addresses"} in
146-
snapshot
144+
{snapshot?.length}{" "}
145+
{snapshot?.length === 1 ? "address" : "addresses"} in snapshot
147146
</span>
148147
</div>
149148
</div>

0 commit comments

Comments
 (0)