From a36c850b860dec42d4e6ae041f9a7fbc30e90cf7 Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 12 Aug 2025 22:55:12 +0000 Subject: [PATCH] [BLD-108] Dashboard: Update Claim conditions UI to handle very large snapshots (#7846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## 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}` ## 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. --- apps/dashboard/package.json | 1 - apps/dashboard/src/@/hooks/useCsvUpload.ts | 48 ++- .../Inputs/ClaimerSelection.tsx | 21 +- .../claim-conditions-form/index.tsx | 178 +++++++---- .../claim-conditions-form/phase.tsx | 7 +- .../claim-conditions/snapshot-upload.tsx | 301 ++++++++++++------ .../tokens/components/airdrop-upload.tsx | 21 +- .../token/distribution/token-airdrop.tsx | 7 +- pnpm-lock.yaml | 67 +--- 9 files changed, 394 insertions(+), 257 deletions(-) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index af8254c1482..88e7161bd96 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -43,7 +43,6 @@ "next-themes": "^0.4.6", "nextjs-toploader": "^1.6.12", "nuqs": "^2.4.3", - "p-limit": "^6.2.0", "papaparse": "^5.5.3", "pluralize": "^8.0.0", "posthog-js": "1.256.1", diff --git a/apps/dashboard/src/@/hooks/useCsvUpload.ts b/apps/dashboard/src/@/hooks/useCsvUpload.ts index b3028734327..66abe914310 100644 --- a/apps/dashboard/src/@/hooks/useCsvUpload.ts +++ b/apps/dashboard/src/@/hooks/useCsvUpload.ts @@ -1,5 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; -import pLimit from "p-limit"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import Papa from "papaparse"; import { useCallback, useState } from "react"; import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; @@ -95,6 +94,7 @@ export function useCsvUpload< // Always gonna need the wallet address T extends { address: string }, >(props: Props) { + const queryClient = useQueryClient(); const [rawData, setRawData] = useState< T[] | Array >(props.defaultRawData || []); @@ -132,16 +132,29 @@ export function useCsvUpload< [props.csvParser], ); + const [normalizeProgress, setNormalizeProgress] = useState({ + total: 0, + current: 0, + }); + const normalizeQuery = useQuery({ queryFn: async () => { - const limit = pLimit(50); - const results = await Promise.all( - rawData.map((item) => { - return limit(() => + const batchSize = 50; + const results = []; + 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); + } + return { invalidFound: !!results.find((item) => !item?.isValid), result: processAirdropData(results), @@ -153,12 +166,18 @@ export function useCsvUpload< const removeInvalid = useCallback(() => { 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( (data: T[]) => { @@ -181,5 +200,6 @@ export function useCsvUpload< removeInvalid, reset, setFiles, + normalizeProgress, }; } diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx index 09685ce1210..7b8f9293454 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx @@ -27,13 +27,15 @@ export const ClaimerSelection = () => { setOpenSnapshotIndex: setOpenIndex, isAdmin, claimConditionType, + phaseSnapshots, + setPhaseSnapshot, } = useClaimConditionsFormContext(); const handleClaimerChange = (value: string) => { const val = value as "any" | "specific" | "overrides"; if (val === "any") { - form.setValue(`phases.${phaseIndex}.snapshot`, undefined); + setPhaseSnapshot(phaseIndex, undefined); } else { if (val === "specific") { form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0); @@ -41,7 +43,7 @@ export const ClaimerSelection = () => { if (val === "overrides" && field.maxClaimablePerWallet !== 1) { form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1); } - form.setValue(`phases.${phaseIndex}.snapshot`, []); + setPhaseSnapshot(phaseIndex, []); setOpenIndex(phaseIndex); } }; @@ -49,6 +51,7 @@ export const ClaimerSelection = () => { let helperText: React.ReactNode; const disabledSnapshotButton = isAdmin && formDisabled; + const snapshot = phaseSnapshots[phaseIndex]; if (dropType === "specific") { helperText = ( @@ -87,10 +90,7 @@ export const ClaimerSelection = () => { return ( { )} {/* Edit or See Snapshot */} - {field.snapshot ? ( + {snapshot ? (
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */} - - {csvUpload.normalizeQuery.data?.invalidFound ? ( + ({ + address: x.resolvedAddress, + ensName: x.address.startsWith("0x") ? undefined : x.address, + maxClaimable: x.maxClaimable, + currencyAddress: x.currencyAddress, + price: x.price, + }))} + /> + +
+ {csvUpload.normalizeQuery.data.invalidFound && ( +

+ Invalid addresses found, please remove from the list to continue +

+ )} + +
- ) : ( - - )} + + {csvUpload.normalizeQuery.data?.invalidFound ? ( + + ) : ( + + )} +
) : ( @@ -308,6 +355,23 @@ const SnapshotViewerSheetContent: React.FC = ({ ); }; +function ViewSnapshot(props: { + data: (SnapshotEntry & { ensName?: string })[]; + onReset: () => void; +}) { + return ( +
+ +
+ +
+
+ ); +} + export function SnapshotViewerSheet( props: SnapshotUploadProps & { isOpen: boolean; @@ -322,7 +386,7 @@ export function SnapshotViewerSheet( }} open={props.isOpen} > - + Snapshot @@ -332,6 +396,33 @@ export function SnapshotViewerSheet( ); } +function SnapshotViewerSheetContent(props: SnapshotUploadProps) { + const [showEditMode, setShowEditMode] = useState(false); + + if (showEditMode) { + return ( + + ); + } + + if (props.value && props.value.length > 0) { + return ( + setShowEditMode(true)} + /> + ); + } + + return ; +} + const snapshotWithMaxClaimable = `\ address,maxClaimable 0x0000000000000000000000000000000000000000,2 diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx index ab16732526d..85f6f96a3d0 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx @@ -21,12 +21,29 @@ export function AirdropUpload(props: { client: props.client, csvParser, }); + + if (csvUpload.normalizeQuery.isPending) { + return ( +
+ +

Resolving ENS

+

+ {csvUpload.normalizeProgress.current} /{" "} + {csvUpload.normalizeProgress.total} +

+
+ ); + } + const normalizeData = csvUpload.normalizeQuery.data; if (!normalizeData) { return ( -
- +
+

Failed to resolve ENS

+

+ {String(csvUpload.normalizeQuery.error)} +

); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx index e715c313f9d..b26b36cc962 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx @@ -262,8 +262,13 @@ const AirdropUpload: React.FC = ({ if (csvUpload.normalizeQuery.isPending) { return ( -
+
+

Resolving ENS

+

+ {csvUpload.normalizeProgress.current} /{" "} + {csvUpload.normalizeProgress.total} +

); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f7d52a839a..e27e44f580f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,9 +190,6 @@ importers: nuqs: specifier: ^2.4.3 version: 2.4.3(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) - p-limit: - specifier: ^6.2.0 - version: 6.2.0 papaparse: specifier: ^5.5.3 version: 5.5.3 @@ -12371,10 +12368,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@6.2.0: - resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} - engines: {node: '>=18'} - p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -15717,10 +15710,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15763,10 +15756,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15809,10 +15802,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -15856,7 +15849,7 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)': + '@aws-sdk/client-sso-oidc@3.592.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -15899,7 +15892,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.840.0': @@ -16036,7 +16028,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16155,24 +16147,6 @@ snapshots: '@smithy/util-stream': 4.2.3 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': - dependencies: - '@aws-sdk/client-sts': 3.592.0 - '@aws-sdk/credential-provider-env': 3.587.0 - '@aws-sdk/credential-provider-http': 3.587.0 - '@aws-sdk/credential-provider-process': 3.587.0 - '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) - '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/types': 3.577.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/client-sts': 3.592.0 @@ -16227,25 +16201,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': - dependencies: - '@aws-sdk/credential-provider-env': 3.587.0 - '@aws-sdk/credential-provider-http': 3.587.0 - '@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/credential-provider-process': 3.587.0 - '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) - '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) - '@aws-sdk/types': 3.577.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/credential-provider-env': 3.587.0 @@ -16519,7 +16474,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.592.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/client-sso-oidc': 3.592.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 @@ -32476,10 +32431,6 @@ snapshots: dependencies: yocto-queue: 1.1.1 - p-limit@6.2.0: - dependencies: - yocto-queue: 1.1.1 - p-locate@3.0.0: dependencies: p-limit: 2.3.0