From c0cb3a47f3efffc4c1fd6c8ba5e83de29e9cf0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Mon, 4 Sep 2023 11:50:12 +0200 Subject: [PATCH] Fix/csv import (#1397) * feat: add ability to enable or disable header selection * feat: limit to max of 200 records for now * fix: bigger modal * feat: add missing standard fields for company * fix: person fields * feat: add hotkeys on dialog * feat: mobile device * fix: company import error * fix: csv import crash * fix: use scoped hotkey --- .../hooks/useSpreadsheetCompanyImport.ts | 24 ++---- .../companies/utils/fieldsForCompany.tsx | 86 ++++++++++++++----- .../hooks/useSpreadsheetPersonImport.ts | 14 +-- .../modules/people/utils/fieldsForPerson.tsx | 25 +----- .../components/ModalCloseButton.tsx | 2 +- .../components/ModalWrapper.tsx | 13 ++- .../provider/components/SpreadsheetImport.tsx | 2 + .../MatchColumnsStep/MatchColumnsStep.tsx | 3 + .../components/ColumnGrid.tsx | 2 +- .../SelectSheetStep/SelectSheetStep.tsx | 2 + .../steps/components/Steps.tsx | 5 ++ .../steps/components/UploadFlow.tsx | 26 +++++- .../ValidationStep/ValidationStep.tsx | 10 ++- .../modules/spreadsheet-import/types/index.ts | 2 + .../modules/ui/dialog/components/Dialog.tsx | 37 +++++++- .../ui/dialog/components/DialogProvider.tsx | 18 ++++ .../ui/dialog/types/DialogHotkeyScope.ts | 3 + .../modules/ui/step-bar/components/Step.tsx | 8 +- .../ui/step-bar/components/StepBar.tsx | 17 ++++ 19 files changed, 213 insertions(+), 86 deletions(-) create mode 100644 front/src/modules/ui/dialog/types/DialogHotkeyScope.ts diff --git a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts index a16feeec2..f32f1af3d 100644 --- a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts +++ b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts @@ -3,12 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { SpreadsheetOptions } from '@/spreadsheet-import/types'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; -import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; -import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds'; -import { - GetPeopleDocument, - useInsertManyCompanyMutation, -} from '~/generated/graphql'; +import { useInsertManyCompanyMutation } from '~/generated/graphql'; import { fieldsForCompany } from '../utils/fieldsForCompany'; @@ -16,8 +11,6 @@ export type FieldCompanyMapping = (typeof fieldsForCompany)[number]['key']; export function useSpreadsheetCompanyImport() { const { openSpreadsheetImport } = useSpreadsheetImport(); - const upsertEntityTableItems = useUpsertEntityTableItems(); - const upsertTableRowIds = useUpsertTableRowIds(); const { enqueueSnackBar } = useSnackBar(); const [createManyCompany] = useInsertManyCompanyMutation(); @@ -34,11 +27,11 @@ export function useSpreadsheetCompanyImport() { // TODO: Add better type checking in spreadsheet import later const createInputs = data.validData.map((company) => ({ id: uuidv4(), - name: company.name as string, - domainName: company.domainName as string, - address: company.address as string, - employees: parseInt(company.employees as string, 10), - linkedinUrl: company.linkedinUrl as string | undefined, + name: (company.name ?? '') as string, + domainName: (company.domainName ?? '') as string, + address: (company.address ?? '') as string, + employees: parseInt((company.employees ?? '') as string, 10), + linkedinUrl: (company.linkedinUrl ?? '') as string | undefined, })); try { @@ -46,15 +39,12 @@ export function useSpreadsheetCompanyImport() { variables: { data: createInputs, }, - refetchQueries: [GetPeopleDocument], + refetchQueries: 'active', }); if (result.errors) { throw result.errors; } - - upsertTableRowIds(createInputs.map((company) => company.id)); - upsertEntityTableItems(createInputs); } catch (error: any) { enqueueSnackBar(error?.message || 'Something went wrong', { variant: 'error', diff --git a/front/src/modules/companies/utils/fieldsForCompany.tsx b/front/src/modules/companies/utils/fieldsForCompany.tsx index 2d5d568cb..991d94e82 100644 --- a/front/src/modules/companies/utils/fieldsForCompany.tsx +++ b/front/src/modules/companies/utils/fieldsForCompany.tsx @@ -1,8 +1,11 @@ import { IconBrandLinkedin, + IconBrandX, IconBuildingSkyscraper, IconMail, IconMap, + IconMoneybag, + IconTarget, IconUsers, } from '@/ui/icon'; @@ -16,13 +19,6 @@ export const fieldsForCompany = [ type: 'input', }, example: 'Tim', - validations: [ - { - rule: 'required', - errorMessage: 'Name is required', - level: 'error', - }, - ], }, { icon: , @@ -33,13 +29,6 @@ export const fieldsForCompany = [ type: 'input', }, example: 'apple.dev', - validations: [ - { - rule: 'required', - errorMessage: 'Domain name is required', - level: 'error', - }, - ], }, { icon: , @@ -51,6 +40,61 @@ export const fieldsForCompany = [ }, example: 'https://www.linkedin.com/in/apple', }, + { + icon: , + label: 'ARR', + key: 'annualRecurringRevenue', + alternateMatches: [ + 'arr', + 'annual revenue', + 'revenue', + 'recurring revenue', + 'annual recurring revenue', + ], + fieldType: { + type: 'input', + }, + validation: [ + { + regex: /^(\d+)?$/, + errorMessage: 'Annual recurring revenue must be a number', + level: 'error', + }, + ], + example: '1000000', + }, + { + icon: , + label: 'ICP', + key: 'idealCustomerProfile', + alternateMatches: [ + 'icp', + 'ideal profile', + 'ideal customer profile', + 'ideal customer', + ], + fieldType: { + type: 'input', + }, + validation: [ + { + regex: /^(true|false)?$/, + errorMessage: 'Ideal custoner profile must be a boolean', + level: 'error', + }, + ], + example: 'true/false', + }, + { + icon: , + label: 'x URL', + key: 'xUrl', + alternateMatches: ['x', 'twitter', 'twitter url', 'x url'], + fieldType: { + type: 'input', + }, + example: 'https://x.com/tim_cook', + }, { icon: , label: 'Address', @@ -59,13 +103,6 @@ export const fieldsForCompany = [ type: 'input', }, example: 'Maple street', - validations: [ - { - rule: 'required', - errorMessage: 'Address is required', - level: 'error', - }, - ], }, { icon: , @@ -75,6 +112,13 @@ export const fieldsForCompany = [ fieldType: { type: 'input', }, + validation: [ + { + regex: /^\d+$/, + errorMessage: 'Employees must be a number', + level: 'error', + }, + ], example: '150', }, ] as const; diff --git a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts index 1894c09a3..ccee9edaf 100644 --- a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts +++ b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts @@ -3,12 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { SpreadsheetOptions } from '@/spreadsheet-import/types'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; -import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; -import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds'; -import { - GetPeopleDocument, - useInsertManyPersonMutation, -} from '~/generated/graphql'; +import { useInsertManyPersonMutation } from '~/generated/graphql'; import { fieldsForPerson } from '../utils/fieldsForPerson'; @@ -16,8 +11,6 @@ export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key']; export function useSpreadsheetPersonImport() { const { openSpreadsheetImport } = useSpreadsheetImport(); - const upsertEntityTableItems = useUpsertEntityTableItems(); - const upsertTableRowIds = useUpsertTableRowIds(); const { enqueueSnackBar } = useSnackBar(); const [createManyPerson] = useInsertManyPersonMutation(); @@ -49,15 +42,12 @@ export function useSpreadsheetPersonImport() { variables: { data: createInputs, }, - refetchQueries: [GetPeopleDocument], + refetchQueries: 'active', }); if (result.errors) { throw result.errors; } - - upsertTableRowIds(createInputs.map((person) => person.id)); - upsertEntityTableItems(createInputs); } catch (error: any) { enqueueSnackBar(error?.message || 'Something went wrong', { variant: 'error', diff --git a/front/src/modules/people/utils/fieldsForPerson.tsx b/front/src/modules/people/utils/fieldsForPerson.tsx index 6d8ccb45d..2b1631957 100644 --- a/front/src/modules/people/utils/fieldsForPerson.tsx +++ b/front/src/modules/people/utils/fieldsForPerson.tsx @@ -2,7 +2,7 @@ import { isValidPhoneNumber } from 'libphonenumber-js'; import { IconBrandLinkedin, - IconBrandTwitter, + IconBrandX, IconBriefcase, IconMail, IconMap, @@ -19,13 +19,6 @@ export const fieldsForPerson = [ type: 'input', }, example: 'Tim', - validations: [ - { - rule: 'required', - errorMessage: 'Firstname is required', - level: 'error', - }, - ], }, { icon: , @@ -36,13 +29,6 @@ export const fieldsForPerson = [ type: 'input', }, example: 'Cook', - validations: [ - { - rule: 'required', - errorMessage: 'Lastname is required', - level: 'error', - }, - ], }, { icon: , @@ -53,13 +39,6 @@ export const fieldsForPerson = [ type: 'input', }, example: 'tim@apple.dev', - validations: [ - { - rule: 'required', - errorMessage: 'email is required', - level: 'error', - }, - ], }, { icon: , @@ -72,7 +51,7 @@ export const fieldsForPerson = [ example: 'https://www.linkedin.com/in/timcook', }, { - icon: , + icon: , label: 'X URL', key: 'xUrl', alternateMatches: ['x', 'x url'], diff --git a/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx b/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx index 316334166..fbfac9770 100644 --- a/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx +++ b/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx @@ -45,7 +45,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => { message: 'Are you sure? Your current information will not be saved.', buttons: [ { title: 'Cancel' }, - { title: 'Exit', onClick: onClose, accent: 'danger' }, + { title: 'Exit', onClick: onClose, accent: 'danger', role: 'confirm' }, ], }); } diff --git a/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx b/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx index 9e89f20b1..8a136e81b 100644 --- a/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx +++ b/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx @@ -3,15 +3,22 @@ import styled from '@emotion/styled'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { Modal } from '@/ui/modal/components/Modal'; +import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { ModalCloseButton } from './ModalCloseButton'; const StyledModal = styled(Modal)` height: 61%; - min-height: 500px; - min-width: 600px; + min-height: 600px; + min-width: 800px; position: relative; - width: 53%; + width: 63%; + @media (max-width: ${MOBILE_VIEWPORT}px) { + min-width: auto; + min-height: auto; + width: 100%; + height: 100%; + } `; const StyledRtlLtr = styled.div` diff --git a/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx index d4fb470dc..9975beb61 100644 --- a/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx +++ b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx @@ -12,6 +12,8 @@ export const defaultSpreadsheetImportProps: Partial> = { matchColumnsStepHook: async (table) => table, dateFormat: 'yyyy-mm-dd', // ISO 8601, parseRaw: true, + selectHeader: false, + maxRecords: 200, } as const; export const SpreadsheetImport = ( diff --git a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index 2ae7589f7..c8c2239fb 100644 --- a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -21,6 +21,8 @@ import { UserTableColumn } from './components/UserTableColumn'; const StyledContent = styled(Modal.Content)` align-items: center; + padding-left: ${({ theme }) => theme.spacing(6)}; + padding-right: ${({ theme }) => theme.spacing(6)}; `; const StyledColumnsContainer = styled.div` @@ -224,6 +226,7 @@ export const MatchColumnsStep = ({ title: 'Continue', onClick: handleAlertOnContinue, variant: 'primary', + role: 'confirm', }, ], }); diff --git a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx index 2c1ee3f82..3c218dda5 100644 --- a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx @@ -19,7 +19,7 @@ const StyledGrid = styled.div` display: flex; flex-direction: column; margin-top: ${({ theme }) => theme.spacing(8)}; - width: 75%; + width: 100%; `; type HeightProps = { diff --git a/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx index e58470517..5469ccdd1 100644 --- a/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx @@ -9,6 +9,8 @@ import { Modal } from '@/ui/modal/components/Modal'; const StyledContent = styled(Modal.Content)` align-items: center; + padding-left: ${({ theme }) => theme.spacing(6)}; + padding-right: ${({ theme }) => theme.spacing(6)}; `; const StyledHeading = styled(Heading)` diff --git a/front/src/modules/spreadsheet-import/steps/components/Steps.tsx b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx index 8ca164109..3700afbcb 100644 --- a/front/src/modules/spreadsheet-import/steps/components/Steps.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx @@ -5,6 +5,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { Modal } from '@/ui/modal/components/Modal'; import { StepBar } from '@/ui/step-bar/components/StepBar'; import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; +import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { UploadFlow } from './UploadFlow'; @@ -15,6 +16,10 @@ const StyledHeader = styled(Modal.Header)` padding: 0px; padding-left: ${({ theme }) => theme.spacing(30)}; padding-right: ${({ theme }) => theme.spacing(30)}; + @media (max-width: ${MOBILE_VIEWPORT}px) { + padding-left: ${({ theme }) => theme.spacing(4)}; + padding-right: ${({ theme }) => theme.spacing(4)}; + } `; const stepTitles = { diff --git a/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx index 6f4d7ad81..d9099b794 100644 --- a/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx @@ -72,6 +72,7 @@ export const UploadFlow = ({ nextStep }: Props) => { uploadStepHook, selectHeaderStepHook, matchColumnsStepHook, + selectHeader, } = useSpreadsheetImportInternal(); const { enqueueSnackBar } = useSnackBar(); @@ -109,10 +110,27 @@ export const UploadFlow = ({ nextStep }: Props) => { const mappedWorkbook = await uploadStepHook( mapWorkbook(workbook), ); - setState({ - type: StepType.selectHeader, - data: mappedWorkbook, - }); + + if (selectHeader) { + setState({ + type: StepType.selectHeader, + data: mappedWorkbook, + }); + } else { + // Automatically select first row as header + const trimmedData = mappedWorkbook.slice(1); + + const { data, headerValues } = await selectHeaderStepHook( + mappedWorkbook[0], + trimmedData, + ); + + setState({ + type: StepType.matchColumns, + data, + headerValues, + }); + } } catch (e) { errorToast((e as Error).message); } diff --git a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index b52da37ea..3ffbbb70a 100644 --- a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -17,6 +17,11 @@ import { Modal } from '@/ui/modal/components/Modal'; import { generateColumns } from './components/columns'; import type { Meta } from './types'; +const StyledContent = styled(Modal.Content)` + padding-left: ${({ theme }) => theme.spacing(6)}; + padding-right: ${({ theme }) => theme.spacing(6)}; +`; + const StyledToolbar = styled.div` display: flex; flex-direction: row; @@ -175,6 +180,7 @@ export const ValidationStep = ({ title: 'Submit', variant: 'primary', onClick: submitData, + role: 'confirm', }, ], }); @@ -183,7 +189,7 @@ export const ValidationStep = ({ return ( <> - + ({ }} /> - + ); diff --git a/front/src/modules/spreadsheet-import/types/index.ts b/front/src/modules/spreadsheet-import/types/index.ts index 2346e1679..5379384f5 100644 --- a/front/src/modules/spreadsheet-import/types/index.ts +++ b/front/src/modules/spreadsheet-import/types/index.ts @@ -50,6 +50,8 @@ export type SpreadsheetOptions = { parseRaw?: boolean; // Use for right-to-left (RTL) support rtl?: boolean; + // Allow header selection + selectHeader?: boolean; }; export type RawData = Array; diff --git a/front/src/modules/ui/dialog/components/Dialog.tsx b/front/src/modules/ui/dialog/components/Dialog.tsx index e81b4a65d..d78d7da73 100644 --- a/front/src/modules/ui/dialog/components/Dialog.tsx +++ b/front/src/modules/ui/dialog/components/Dialog.tsx @@ -1,8 +1,12 @@ import { useCallback } from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; +import { Key } from 'ts-key-enum'; import { Button } from '@/ui/button/components/Button'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; + +import { DialogHotkeyScope } from '../types/DialogHotkeyScope'; const StyledDialogOverlay = styled(motion.div)` align-items: center; @@ -52,7 +56,12 @@ const StyledDialogButton = styled(Button)` export type DialogButtonOptions = Omit< React.ComponentProps, 'fullWidth' ->; +> & { + onClick?: ( + event: React.MouseEvent | KeyboardEvent, + ) => void; + role?: 'confirm'; +}; export type DialogProps = React.ComponentPropsWithoutRef & { title?: string; @@ -86,6 +95,32 @@ export function Dialog({ closed: { y: '50vh' }, }; + useScopedHotkeys( + Key.Enter, + (event: KeyboardEvent) => { + const confirmButton = buttons.find((button) => button.role === 'confirm'); + + event.preventDefault(); + + if (confirmButton) { + confirmButton?.onClick?.(event); + closeSnackbar(); + } + }, + DialogHotkeyScope.Dialog, + [], + ); + + useScopedHotkeys( + Key.Escape, + (event: KeyboardEvent) => { + event.preventDefault(); + closeSnackbar(); + }, + DialogHotkeyScope.Dialog, + [], + ); + return ( { setDialogState((prevState) => ({ ...prevState, queue: prevState.queue.filter((snackBar) => snackBar.id !== id), })); + goBackToPreviousHotkeyScope(); }; + useEffect(() => { + if (dialogState.queue.length === 0) { + return; + } + + setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog); + }, [dialogState.queue, setHotkeyScopeAndMemorizePreviousScope]); + return ( <> {children} diff --git a/front/src/modules/ui/dialog/types/DialogHotkeyScope.ts b/front/src/modules/ui/dialog/types/DialogHotkeyScope.ts new file mode 100644 index 000000000..ff5d3a978 --- /dev/null +++ b/front/src/modules/ui/dialog/types/DialogHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum DialogHotkeyScope { + Dialog = 'dialog', +} diff --git a/front/src/modules/ui/step-bar/components/Step.tsx b/front/src/modules/ui/step-bar/components/Step.tsx index 15ecf7192..f0efed22b 100644 --- a/front/src/modules/ui/step-bar/components/Step.tsx +++ b/front/src/modules/ui/step-bar/components/Step.tsx @@ -3,11 +3,16 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { AnimatedCheckmark } from '@/ui/checkmark/components/AnimatedCheckmark'; +import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; const StyledContainer = styled.div<{ isLast: boolean }>` align-items: center; display: flex; flex-grow: ${({ isLast }) => (isLast ? '0' : '1')}; + @media (max-width: ${MOBILE_VIEWPORT}px) { + flex-grow: 0; + } `; const StyledStepCircle = styled(motion.div)` @@ -64,6 +69,7 @@ export const Step = ({ children, }: StepProps) => { const theme = useTheme(); + const isMobile = useIsMobile(); const variantsCircle = { active: { @@ -104,7 +110,7 @@ export const Step = ({ {!isActive && {index + 1}} {label} - {!isLast && ( + {!isLast && !isMobile && ( { + const isMobile = useIsMobile(); + return ( {React.Children.map(children, (child, index) => { @@ -29,6 +38,14 @@ export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => { return child; } + // We should only render the active step, and if activeStep is -1, we should only render the first step only when it's mobile device + if ( + isMobile && + (activeStep === -1 ? index !== 0 : index !== activeStep) + ) { + return null; + } + return React.cloneElement(child as any, { index, isActive: index <= activeStep,