From 30c3678c1e6a108fa7258a4129efe4f3e3cf8250 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 6 Aug 2026 22:17:25 +0530 Subject: [PATCH] PM-5126 Add member handle input box in bulk reports lookup --- .../InputHandlesSelector.module.scss | 24 ++++- .../BulkMemberLookupPage.module.scss | 11 +++ .../BulkMemberLookupPage.tsx | 99 +++++++++++++++---- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/src/apps/admin/src/lib/components/InputHandlesSelector/InputHandlesSelector.module.scss b/src/apps/admin/src/lib/components/InputHandlesSelector/InputHandlesSelector.module.scss index 5a3b5c669..d90f017ec 100644 --- a/src/apps/admin/src/lib/components/InputHandlesSelector/InputHandlesSelector.module.scss +++ b/src/apps/admin/src/lib/components/InputHandlesSelector/InputHandlesSelector.module.scss @@ -12,8 +12,12 @@ } .selectUserHandlesCustomMultiValue { + display: inline-flex; + align-items: center; + gap: 6px; font-size: 13px; - padding: 0 8px; + line-height: 1.4; + padding: 2px 8px; margin-right: 6px; color: $black-60; background-color: $black-10; @@ -28,6 +32,24 @@ } } +// Sized here so the control works outside admin-app (which provides svg.icon globals). +.removeIcon { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + line-height: 1; + + :global(svg) { + display: block; + width: 12px; + height: 12px; + max-width: 12px; + max-height: 12px; + stroke: currentColor; + } +} + .selectUserHandlesDropdownContainer { z-index: 9999 !important; } diff --git a/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.module.scss b/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.module.scss index 5de227b0e..707dbb32a 100644 --- a/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.module.scss +++ b/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.module.scss @@ -36,3 +36,14 @@ color: #6b6f75; font-style: italic; } + +.handleLink { + color: #0f62fe; + font-weight: 700; + text-decoration: none; + + &:hover { + color: #0043ce; + text-decoration: underline; + } +} diff --git a/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.tsx b/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.tsx index 5dece3164..7e1b9c439 100644 --- a/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.tsx +++ b/src/apps/reports/src/pages/bulk-member-lookup/BulkMemberLookupPage.tsx @@ -1,5 +1,8 @@ import { Dispatch, FC, SetStateAction, useCallback, useMemo, useState } from 'react' +import { InputHandlesSelector } from '~/apps/admin/src/lib/components/InputHandlesSelector' +import { SearchUserInfo } from '~/apps/admin/src/lib/models' +import { EnvironmentConfig } from '~/config' import { Button, InputFilePicker, @@ -63,7 +66,6 @@ const parseLookupResults = async (blob: Blob): Promise => { * Parses uploaded text/CSV content into a normalized handle list. * @param file Uploaded `.txt` or `.csv` file. * @returns Ordered non-empty handles from the file. - * @throws Error when no handles are found in the uploaded content. */ const parseHandlesFromFile = async (file: File): Promise => { const content = (await file.text()) @@ -81,8 +83,37 @@ const parseHandlesFromFile = async (file: File): Promise => { handles.shift() } + return handles +} + +/** + * Merges typed and file-sourced handles, preserving order and dropping duplicates. + * @param selectedHandles Handles chosen in the multi-entry selector. + * @param file Optional uploaded handles file. + * @returns Combined unique handle list. + * @throws Error when neither source provides handles. + */ +const collectHandles = async ( + selectedHandles: SearchUserInfo[], + file: File | undefined, +): Promise => { + const fromInput = selectedHandles.map(item => item.handle.trim()) + .filter(Boolean) + const fromFile = file ? await parseHandlesFromFile(file) : [] + const seen = new Set() + const handles: string[] = [] + + for (const handle of [...fromInput, ...fromFile]) { + const key = handle.toLowerCase() + + if (!seen.has(key)) { + seen.add(key) + handles.push(handle) + } + } + if (!handles.length) { - throw new Error('Uploaded file does not contain any handles.') + throw new Error('Enter at least one handle or upload a file that contains handles.') } return handles @@ -106,14 +137,19 @@ const downloadBlob = (blob: Blob, fileName: string): void => { } /** - * Bulk Member Lookup page for uploading handles and resolving account details. + * Bulk Member Lookup page for resolving account details by handle. * - * Users upload a `.txt` or `.csv` file of handles, submit for lookup, - * review results in a table, and optionally download JSON/CSV output. + * Users can enter handles in a multi-entry selector and/or upload a `.txt` or + * `.csv` file, submit for lookup, review results in a table, and optionally + * download JSON/CSV output. */ export const BulkMemberLookupPage: FC = () => { const [file, setFile]: [File | undefined, Dispatch>] = useState(undefined) + const [selectedHandles, setSelectedHandles]: [ + SearchUserInfo[], + Dispatch> + ] = useState([]) const [isSubmitting, setIsSubmitting]: [boolean, Dispatch>] = useState(false) const [results, setResults]: [BulkMemberRow[], Dispatch>] @@ -125,6 +161,9 @@ export const BulkMemberLookupPage: FC = () => { Dispatch> ] = useState<'json' | 'csv' | undefined>(undefined) + const hasHandlesInput = selectedHandles.length > 0 || !!file + const isBusy = isSubmitting || isDownloading !== undefined + const tableColumns = useMemo[]>(() => ([ { label: 'User ID', @@ -135,7 +174,19 @@ export const BulkMemberLookupPage: FC = () => { { label: 'Handle', propertyName: 'handle', - type: 'text', + renderer: data => ( + data.handle ? ( + + {data.handle} + + ) : <>{emptyValue} + ), + type: 'element', }, { label: 'First Name', @@ -175,14 +226,20 @@ export const BulkMemberLookupPage: FC = () => { setResults([]) }, []) + const handleHandlesChange = useCallback((handles: SearchUserInfo[]): void => { + setSelectedHandles(handles) + setHasSubmitted(false) + setResults([]) + }, []) + const handleLookupMembers = useCallback(async (): Promise => { - if (!file) { + if (!hasHandlesInput) { return } try { setIsSubmitting(true) - const handles = await parseHandlesFromFile(file) + const handles = await collectHandles(selectedHandles, file) const responseBlob = await postReportAsJson(bulkMembersByHandlesPath, { handles }) const lookupResults = await parseLookupResults(responseBlob) @@ -193,16 +250,16 @@ export const BulkMemberLookupPage: FC = () => { } finally { setIsSubmitting(false) } - }, [file]) + }, [file, hasHandlesInput, selectedHandles]) const handleDownload = useCallback(async (format: 'json' | 'csv'): Promise => { - if (!file) { + if (!hasHandlesInput) { return } try { setIsDownloading(format) - const handles = await parseHandlesFromFile(file) + const handles = await collectHandles(selectedHandles, file) const blob = format === 'json' ? await postReportAsJson(bulkMembersByHandlesPath, { handles }) @@ -214,7 +271,7 @@ export const BulkMemberLookupPage: FC = () => { } finally { setIsDownloading(undefined) } - }, [file]) + }, [file, hasHandlesInput, selectedHandles]) const handleJsonDownload = useCallback(() => { handleDownload('json') @@ -224,7 +281,7 @@ export const BulkMemberLookupPage: FC = () => { handleDownload('csv') }, [handleDownload]) - const isDownloadDisabled = !file || isSubmitting || isDownloading !== undefined + const isDownloadDisabled = !hasHandlesInput || isBusy return ( <> @@ -235,11 +292,19 @@ export const BulkMemberLookupPage: FC = () => { {pageTitle}

- Upload a TXT or CSV file that contains one member handle per line, - then submit to resolve user details. + Enter member handles below and/or upload a TXT or CSV file that + contains one member handle per line, then submit to resolve user details.

+ + {