From 2e23a176f6557f80e96afd9d9561b02b2e182f55 Mon Sep 17 00:00:00 2001 From: Evis Drenova <80707987+evisdrenova@users.noreply.github.com> Date: Sun, 19 Nov 2023 02:03:58 -0800 Subject: [PATCH] NEOS-206: base progress bar (#615) --- frontend/app/jobs/page.tsx | 5 +- frontend/app/new/job/JobsProgressSteps.tsx | 59 ++ frontend/app/new/job/connect/page.tsx | 521 +++++++++++++++++ frontend/app/new/job/define/page.tsx | 118 ++-- frontend/app/new/job/flow/page.tsx | 522 ------------------ frontend/app/new/job/page.tsx | 15 +- frontend/app/new/job/schema.ts | 6 +- frontend/app/new/job/schema/page.tsx | 59 +- frontend/app/new/job/subset/page.tsx | 197 ++++--- .../jobs/SchemaTable/TransformerSelect.tsx | 4 +- .../jobs/subsets/subset-table/SubsetTable.tsx | 1 + frontend/components/progress-steps/Step.tsx | 60 ++ .../components/progress-steps/useStep.tsx | 71 +++ frontend/util/util.ts | 7 +- 14 files changed, 903 insertions(+), 742 deletions(-) create mode 100644 frontend/app/new/job/JobsProgressSteps.tsx create mode 100644 frontend/app/new/job/connect/page.tsx delete mode 100644 frontend/app/new/job/flow/page.tsx create mode 100644 frontend/components/progress-steps/Step.tsx create mode 100644 frontend/components/progress-steps/useStep.tsx diff --git a/frontend/app/jobs/page.tsx b/frontend/app/jobs/page.tsx index 7ba64062b7..b154f9d7e8 100644 --- a/frontend/app/jobs/page.tsx +++ b/frontend/app/jobs/page.tsx @@ -68,10 +68,7 @@ function JobTable(props: JobTableProps): ReactElement { ); } -interface NewJobButtonProps {} - -function NewJobButton(props: NewJobButtonProps): ReactElement { - const {} = props; +function NewJobButton(): ReactElement { return ( + + + + c.id == + form.getValues().destinations[index].connectionId + )} + maxColNum={2} + /> + + ); + })} + + + + + +
+ + +
+ + + + ); +} + +function getErrorConnectionTypes( + isSource: boolean, + connId: string, + connections: Connection[] +): string { + const conn = connections.find((c) => c.id == connId); + if (!conn) { + return isSource ? '[Postgres, Mysql]' : '[Postgres, Mysql, AWS S3]'; + } + if (conn.connectionConfig?.config.case == 'awsS3Config') { + return '[Mysql, Postgres]'; + } + if (conn.connectionConfig?.config.case == 'mysqlConfig') { + return isSource ? '[Postgres]' : '[Mysql, AWS S3]'; + } + + if (conn.connectionConfig?.config.case == 'pgConfig') { + return isSource ? '[Mysql]' : '[Postgres, AWS S3]'; + } + return ''; +} + +function isValidConnectionPair( + connId1: string, + connId2: string, + connections: Connection[] +): boolean { + const conn1 = connections.find((c) => c.id == connId1); + const conn2 = connections.find((c) => c.id == connId2); + + if (!conn1 || !conn2) { + return true; + } + if ( + conn1.connectionConfig?.config.case == 'awsS3Config' || + conn2.connectionConfig?.config.case == 'awsS3Config' + ) { + return true; + } + + if ( + conn1.connectionConfig?.config.case == conn2.connectionConfig?.config.case + ) { + return true; + } + + return false; +} diff --git a/frontend/app/new/job/define/page.tsx b/frontend/app/new/job/define/page.tsx index 5b705a58d5..e1c15bbef5 100644 --- a/frontend/app/new/job/define/page.tsx +++ b/frontend/app/new/job/define/page.tsx @@ -1,6 +1,4 @@ 'use client'; -import OverviewContainer from '@/components/containers/OverviewContainer'; -import PageHeader from '@/components/headers/PageHeader'; import SwitchCard from '@/components/switches/SwitchCard'; import { PageProps } from '@/components/types'; import { Button } from '@/components/ui/button'; @@ -17,11 +15,12 @@ import { Input } from '@/components/ui/input'; import { yupResolver } from '@hookform/resolvers/yup'; import NeoCron from 'neocron'; import 'neocron/dist/src/globals.css'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { ReactElement, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import useFormPersist from 'react-hook-form-persist'; import { useSessionStorage } from 'usehooks-ts'; +import JobsProgressSteps from '../JobsProgressSteps'; import { DEFINE_FORM_SCHEMA, DefineFormValues } from '../schema'; export default function Page({ searchParams }: PageProps): ReactElement { @@ -56,91 +55,92 @@ export default function Page({ searchParams }: PageProps): ReactElement { }); async function onSubmit(_values: DefineFormValues) { - router.push(`/new/job/flow?sessionId=${sessionPrefix}`); + router.push(`/new/job/connect?sessionId=${sessionPrefix}`); } const [isClient, setIsClient] = useState(false); - useEffect(() => { // This code runs after mount, indicating we're on the client setIsClient(true); }, []); + //check if there is somethign in the values for this page and if so then set this to complete + + const params = usePathname(); + const [stepName, _] = useState(params.split('/').pop() ?? ''); + return ( -
- +
+ +
+
+ + ( + + Name + The unique name of the job. + + + + + + )} /> - } - > - - - ( - Name - The unique name of the job. + Schedule + + Define a schedule to run this job. + - + )} /> - {isClient && ( - + Settings +
+ ( - Schedule - - Define a schedule to run this job. - - )} /> - )} -
- Settings -
- ( - - - - - - - )} - /> -
-
-
-
- - - +
+
+ +
+ +
); } diff --git a/frontend/app/new/job/flow/page.tsx b/frontend/app/new/job/flow/page.tsx deleted file mode 100644 index 1131aeb4c9..0000000000 --- a/frontend/app/new/job/flow/page.tsx +++ /dev/null @@ -1,522 +0,0 @@ -'use client'; -import OverviewContainer from '@/components/containers/OverviewContainer'; -import PageHeader from '@/components/headers/PageHeader'; -import SourceOptionsForm from '@/components/jobs/Form/SourceOptionsForm'; -import { useAccount } from '@/components/providers/account-provider'; -import { PageProps } from '@/components/types'; -import { Button } from '@/components/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from '@/components/ui/form'; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Separator } from '@/components/ui/separator'; -import { Skeleton } from '@/components/ui/skeleton'; -import { useGetConnections } from '@/libs/hooks/useGetConnections'; -import { Connection } from '@/neosync-api-client/mgmt/v1alpha1/connection_pb'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Cross2Icon, PlusIcon } from '@radix-ui/react-icons'; -import { useRouter } from 'next/navigation'; -import { ReactElement, useEffect, useState } from 'react'; -import { useFieldArray, useForm } from 'react-hook-form'; -import useFormPersist from 'react-hook-form-persist'; -import { useSessionStorage } from 'usehooks-ts'; -import DestinationOptionsForm from '../../../../components/jobs/Form/DestinationOptionsForm'; -import { FLOW_FORM_SCHEMA, FlowFormValues } from '../schema'; - -const NEW_CONNECTION_VALUE = 'new-connection'; - -export default function Page({ searchParams }: PageProps): ReactElement { - const { account } = useAccount(); - const router = useRouter(); - useEffect(() => { - if (!searchParams?.sessionId) { - router.push(`/new/job`); - } - }, [searchParams?.sessionId]); - - const sessionPrefix = searchParams?.sessionId ?? ''; - const [defaultValues] = useSessionStorage( - `${sessionPrefix}-new-job-flow`, - { - sourceId: '', - sourceOptions: {}, - destinations: [{ connectionId: '', destinationOptions: {} }], - } - ); - - const form = useForm({ - resolver: yupResolver(FLOW_FORM_SCHEMA), - defaultValues, - }); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: 'destinations', - }); - useFormPersist(`${sessionPrefix}-new-job-flow`, { - watch: form.watch, - setValue: form.setValue, - storage: window.sessionStorage, - }); - const { isLoading: isConnectionsLoading, data: connectionsData } = - useGetConnections(account?.id ?? ''); - - const connections = connectionsData?.connections ?? []; - - async function onSubmit(_values: FlowFormValues) { - router.push(`/new/job/schema?sessionId=${sessionPrefix}`); - } - - const [sourceConn, setSourceConn] = useState( - form.getValues().sourceId - ); - - const [destConn, setDestConn] = useState(form.getValues().sourceId); - - const errors = form.formState.errors; - - const mysqlConns = connections.filter( - (c) => c.connectionConfig?.config.case == 'mysqlConfig' - ); - const postgresConns = connections.filter( - (c) => c.connectionConfig?.config.case == 'pgConfig' - ); - const awsS3Conns = connections.filter( - (c) => c.connectionConfig?.config.case == 'awsS3Config' - ); - - return ( -
- - } - > -
- -
-
-
-
-

- Source -

-

- The location of the source data set. -

-
-
-
-
- ( - - - {isConnectionsLoading ? ( - - ) : ( - - )} - - - - )} - /> - - c.id == form.getValues().sourceId - )} - maxColNum={2} - /> -
-
- - -
-
-

- Destination(s) -

-

- Where the data set should be synced. -

-
-
- {fields.map(({}, index) => { - return ( -
-
-
- ( - - - {isConnectionsLoading ? ( - - ) : ( - - )} - - - - )} - /> -
-
- -
-
- - c.id == - form.getValues().destinations[index].connectionId - )} - maxColNum={2} - /> -
- ); - })} - - -
-
- -
- - -
- - -
-
- ); -} - -function getErrorConnectionTypes( - isSource: boolean, - connId: string, - connections: Connection[] -): string { - const conn = connections.find((c) => c.id == connId); - if (!conn) { - return isSource ? '[Postgres, Mysql]' : '[Postgres, Mysql, AWS S3]'; - } - if (conn.connectionConfig?.config.case == 'awsS3Config') { - return '[Mysql, Postgres]'; - } - if (conn.connectionConfig?.config.case == 'mysqlConfig') { - return isSource ? '[Postgres]' : '[Mysql, AWS S3]'; - } - - if (conn.connectionConfig?.config.case == 'pgConfig') { - return isSource ? '[Mysql]' : '[Postgres, AWS S3]'; - } - return ''; -} - -function isValidConnectionPair( - connId1: string, - connId2: string, - connections: Connection[] -): boolean { - const conn1 = connections.find((c) => c.id == connId1); - const conn2 = connections.find((c) => c.id == connId2); - - if (!conn1 || !conn2) { - return true; - } - if ( - conn1.connectionConfig?.config.case == 'awsS3Config' || - conn2.connectionConfig?.config.case == 'awsS3Config' - ) { - return true; - } - - if ( - conn1.connectionConfig?.config.case == conn2.connectionConfig?.config.case - ) { - return true; - } - - return false; -} diff --git a/frontend/app/new/job/page.tsx b/frontend/app/new/job/page.tsx index 9e53bde6aa..ed19e20c56 100644 --- a/frontend/app/new/job/page.tsx +++ b/frontend/app/new/job/page.tsx @@ -1,6 +1,4 @@ 'use client'; -import OverviewContainer from '@/components/containers/OverviewContainer'; -import PageHeader from '@/components/headers/PageHeader'; import { PageProps } from '@/components/types'; import { nanoid } from 'nanoid'; import { useRouter } from 'next/navigation'; @@ -13,16 +11,5 @@ export default function NewJob({ params }: PageProps): ReactElement { router.push(`/new/job/define?sessionId=${sessionToken}`); }, [sessionToken]); - return ( - - } - > -
-
- ); + return
; } diff --git a/frontend/app/new/job/schema.ts b/frontend/app/new/job/schema.ts index 64a1f54c82..fbcc9f0c4c 100644 --- a/frontend/app/new/job/schema.ts +++ b/frontend/app/new/job/schema.ts @@ -38,13 +38,13 @@ export const DEFINE_FORM_SCHEMA = Yup.object({ export type DefineFormValues = Yup.InferType; -export const FLOW_FORM_SCHEMA = SOURCE_FORM_SCHEMA.concat( +export const CONNECT_FORM_SCHEMA = SOURCE_FORM_SCHEMA.concat( Yup.object({ destinations: Yup.array(DESTINATION_FORM_SCHEMA).required(), }) ); -export type FlowFormValues = Yup.InferType; +export type ConnectFormValues = Yup.InferType; const SINGLE_SUBSET_FORM_SCSHEMA = Yup.object({ schema: Yup.string().trim().required(), @@ -62,7 +62,7 @@ export type SubsetFormValues = Yup.InferType; const FORM_SCHEMA = Yup.object({ define: DEFINE_FORM_SCHEMA, - flow: FLOW_FORM_SCHEMA, + connect: CONNECT_FORM_SCHEMA, schema: SCHEMA_FORM_SCHEMA, subset: SUBSET_FORM_SCHEMA.optional(), }); diff --git a/frontend/app/new/job/schema/page.tsx b/frontend/app/new/job/schema/page.tsx index 61b7722d58..be77e3f645 100644 --- a/frontend/app/new/job/schema/page.tsx +++ b/frontend/app/new/job/schema/page.tsx @@ -1,7 +1,5 @@ 'use client'; -import OverviewContainer from '@/components/containers/OverviewContainer'; -import PageHeader from '@/components/headers/PageHeader'; import { SchemaTable, getConnectionSchema, @@ -14,12 +12,13 @@ import { useToast } from '@/components/ui/use-toast'; import { getErrorMessage } from '@/util/util'; import { SCHEMA_FORM_SCHEMA, SchemaFormValues } from '@/yup-validations/jobs'; import { yupResolver } from '@hookform/resolvers/yup'; -import { useRouter } from 'next/navigation'; -import { ReactElement, useEffect } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { ReactElement, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import useFormPersist from 'react-hook-form-persist'; import { useSessionStorage } from 'usehooks-ts'; -import { FlowFormValues } from '../schema'; +import JobsProgressSteps from '../JobsProgressSteps'; +import { ConnectFormValues } from '../schema'; export default function Page({ searchParams }: PageProps): ReactElement { const { account } = useAccount(); @@ -34,8 +33,8 @@ export default function Page({ searchParams }: PageProps): ReactElement { const sessionPrefix = searchParams?.sessionId ?? ''; - const [flowFormValues] = useSessionStorage( - `${sessionPrefix}-new-job-flow`, + const [connectFormValues] = useSessionStorage( + `${sessionPrefix}-new-job-connect`, { sourceId: '', sourceOptions: {}, @@ -49,7 +48,7 @@ export default function Page({ searchParams }: PageProps): ReactElement { async function getSchema() { try { - const res = await getConnectionSchema(flowFormValues.sourceId); + const res = await getConnectionSchema(connectFormValues.sourceId); if (!res) { return { mappings: [] }; } @@ -96,30 +95,28 @@ export default function Page({ searchParams }: PageProps): ReactElement { router.push(`/new/job/subset?sessionId=${sessionPrefix}`); } + const params = usePathname(); + const [stepName, _] = useState(params.split('/').pop() ?? ''); + return ( -
- - } - > -
- - -
- - -
- - -
+
+
+ +
+ +
+ + +
+ + +
+ +
); } diff --git a/frontend/app/new/job/subset/page.tsx b/frontend/app/new/job/subset/page.tsx index 921968857c..849537dbd1 100644 --- a/frontend/app/new/job/subset/page.tsx +++ b/frontend/app/new/job/subset/page.tsx @@ -1,8 +1,6 @@ 'use client'; import { MergeSystemAndCustomTransformers } from '@/app/transformers/EditTransformerOptions'; -import OverviewContainer from '@/components/containers/OverviewContainer'; -import PageHeader from '@/components/headers/PageHeader'; import EditItem from '@/components/jobs/subsets/EditItem'; import SubsetTable from '@/components/jobs/subsets/subset-table/SubsetTable'; import { TableRow } from '@/components/jobs/subsets/subset-table/column'; @@ -38,14 +36,15 @@ import { } from '@/yup-validations/jobs'; import { ToTransformerConfigOptions } from '@/yup-validations/transformers'; import { yupResolver } from '@hookform/resolvers/yup'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { ReactElement, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import useFormPersist from 'react-hook-form-persist'; import { useSessionStorage } from 'usehooks-ts'; +import JobsProgressSteps from '../JobsProgressSteps'; import { + ConnectFormValues, DefineFormValues, - FlowFormValues, FormValues, SUBSET_FORM_SCHEMA, SubsetFormValues, @@ -77,8 +76,8 @@ export default function Page({ searchParams }: PageProps): ReactElement { ); // Used to complete the whole form - const [flowFormValues] = useSessionStorage( - `${sessionPrefix}-new-job-flow`, + const [connectFormValues] = useSessionStorage( + `${sessionPrefix}-new-job-connect`, { sourceId: '', sourceOptions: {}, @@ -125,7 +124,7 @@ export default function Page({ searchParams }: PageProps): ReactElement { const job = await createNewJob( { define: defineFormValues, - flow: flowFormValues, + connect: connectFormValues, schema: schemaFormValues, subset: values, }, @@ -185,101 +184,95 @@ export default function Page({ searchParams }: PageProps): ReactElement { } } + const params = usePathname(); + const [stepName, _] = useState(params.split('/').pop() ?? ''); + return ( -
- - } - > -
-
-

- Set table subset rules by pressing the edit button and filling out - the form below -

-
-
- -
- { - const key = buildRowKey(schema, table); - if (tableRowData[key]) { - // make copy so as to not edit in place - setItemToEdit({ - ...tableRowData[key], - }); - } - }} - hasLocalChange={hasLocalChange} - onReset={onLocalRowReset} - /> -
-
- -
-
- setItemToEdit(undefined)} - onSave={() => { - if (!itemToEdit) { - return; - } - const key = buildRowKey( - itemToEdit.schema, - itemToEdit.table +
+
+ +
+
+
+

+ Set table subset rules by pressing the edit button and filling out + the form below +

+
+ + +
+ { + const key = buildRowKey(schema, table); + if (tableRowData[key]) { + // make copy so as to not edit in place + setItemToEdit({ + ...tableRowData[key], + }); + } + }} + hasLocalChange={hasLocalChange} + onReset={onLocalRowReset} + /> +
+
+ +
+
+ setItemToEdit(undefined)} + onSave={() => { + if (!itemToEdit) { + return; + } + const key = buildRowKey(itemToEdit.schema, itemToEdit.table); + const idx = form + .getValues() + .subsets.findIndex( + (item) => buildRowKey(item.schema, item.table) === key ); - const idx = form - .getValues() - .subsets.findIndex( - (item) => buildRowKey(item.schema, item.table) === key - ); - if (idx >= 0) { - form.setValue(`subsets.${idx}`, { + if (idx >= 0) { + form.setValue(`subsets.${idx}`, { + schema: itemToEdit.schema, + table: itemToEdit.table, + whereClause: itemToEdit.where, + }); + } else { + form.setValue( + `subsets`, + form.getValues().subsets.concat({ schema: itemToEdit.schema, table: itemToEdit.table, whereClause: itemToEdit.where, - }); - } else { - form.setValue( - `subsets`, - form.getValues().subsets.concat({ - schema: itemToEdit.schema, - table: itemToEdit.table, - whereClause: itemToEdit.where, - }) - ); - } - setItemToEdit(undefined); - }} - /> -
-
- -
-
- - -
- - -
- + }) + ); + } + setItemToEdit(undefined); + }} + /> +
+
+ +
+
+ + +
+ + +
); } @@ -294,7 +287,7 @@ async function createNewJob( connections.map((connection) => [connection.id, connection]) ); const sourceConnection = connections.find( - (c) => c.id == formData.flow.sourceId + (c) => c.id == formData.connect.sourceId ); const body = new CreateJobRequest({ @@ -311,10 +304,10 @@ async function createNewJob( }); }), source: new JobSource({ - connectionId: formData.flow.sourceId, + connectionId: formData.connect.sourceId, options: toJobSourceOptions(formData, sourceConnection), }), - destinations: formData.flow.destinations.map((d) => { + destinations: formData.connect.destinations.map((d) => { return new JobDestination({ connectionId: d.connectionId, options: toJobDestinationOptions( @@ -339,7 +332,7 @@ async function createNewJob( case: 'postgresOptions', value: new PostgresSourceConnectionOptions({ haltOnNewColumnAddition: - values.flow.sourceOptions.haltOnNewColumnAddition, + values.connect.sourceOptions.haltOnNewColumnAddition, }), }, }); @@ -349,7 +342,7 @@ async function createNewJob( case: 'mysqlOptions', value: new MysqlSourceConnectionOptions({ haltOnNewColumnAddition: - values.flow.sourceOptions.haltOnNewColumnAddition, + values.connect.sourceOptions.haltOnNewColumnAddition, }), }, }); diff --git a/frontend/components/jobs/SchemaTable/TransformerSelect.tsx b/frontend/components/jobs/SchemaTable/TransformerSelect.tsx index 42cc302829..9311912281 100644 --- a/frontend/components/jobs/SchemaTable/TransformerSelect.tsx +++ b/frontend/components/jobs/SchemaTable/TransformerSelect.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/popover'; import { cn } from '@/libs/utils'; import { CustomTransformer } from '@/neosync-api-client/mgmt/v1alpha1/transformer_pb'; -import { ToTitleCase } from '@/util/util'; +import { toTitleCase } from '@/util/util'; import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'; import { ReactElement, useState } from 'react'; @@ -50,7 +50,7 @@ export default function TransformerSelect(props: Props): ReactElement { className="justify-between w-[175px]" >
- {ToTitleCase(value) ? ToTitleCase(value) : 'Select a transformer'} + {toTitleCase(value) ? toTitleCase(value) : 'Select a transformer'}
diff --git a/frontend/components/jobs/subsets/subset-table/SubsetTable.tsx b/frontend/components/jobs/subsets/subset-table/SubsetTable.tsx index 4b3c3778fc..b6ac7707f5 100644 --- a/frontend/components/jobs/subsets/subset-table/SubsetTable.tsx +++ b/frontend/components/jobs/subsets/subset-table/SubsetTable.tsx @@ -1,3 +1,4 @@ +'use client'; import { ReactElement } from 'react'; import { TableRow, getColumns } from './column'; import { DataTable } from './data-table'; diff --git a/frontend/components/progress-steps/Step.tsx b/frontend/components/progress-steps/Step.tsx new file mode 100644 index 0000000000..f488c165f9 --- /dev/null +++ b/frontend/components/progress-steps/Step.tsx @@ -0,0 +1,60 @@ +import { cn } from '@/libs/utils'; +import { toTitleCase } from '@/util/util'; +import { CheckIcon } from '@radix-ui/react-icons'; + +interface Props { + isCompleted: boolean; + isActive: boolean; + isLastStep: boolean; + name: string; +} + +export const Step = (props: Props) => { + const { isActive, isCompleted, isLastStep, name } = props; + + return ( +
+ +
+ ); +}; + +interface StepCircleProps { + isCompleted: boolean; + isLastStep: boolean; + name: string; + isActive: boolean; +} + +const StepCircle = (props: StepCircleProps) => { + const { isCompleted, isLastStep, name, isActive } = props; + return ( +
+
+
+ {isCompleted && } +
+
+ {toTitleCase(name)} +
+
+ {!isLastStep && ( +
+ )} +
+ ); +}; diff --git a/frontend/components/progress-steps/useStep.tsx b/frontend/components/progress-steps/useStep.tsx new file mode 100644 index 0000000000..9942b6a9e5 --- /dev/null +++ b/frontend/components/progress-steps/useStep.tsx @@ -0,0 +1,71 @@ +import { + Dispatch, + SetStateAction, + useCallback, + useMemo, + useState, +} from 'react'; + +interface Helpers { + goToNextStep: () => void; + goToPrevStep: () => void; + reset: () => void; + canGoToNextStep: boolean; + canGoToPrevStep: boolean; + setStep: Dispatch>; +} + +interface UseStepProps { + maxStep: number; + initialStep?: number; +} + +export const useStep = (props: UseStepProps): [number, Helpers] => { + const { maxStep, initialStep = 0 } = props; + const [currentStep, setCurrentStep] = useState(initialStep); + const canGoToNextStep = useMemo( + () => currentStep + 1 <= maxStep, + [currentStep, maxStep] + ); + const canGoToPrevStep = useMemo(() => currentStep - 1 >= 0, [currentStep]); + + const setStep = useCallback( + (step: unknown) => { + const newStep = step instanceof Function ? step(currentStep) : step; + if (newStep >= 0 && newStep <= maxStep) { + setCurrentStep(newStep); + return; + } + throw new Error('Step not valid'); + }, + [maxStep, currentStep] + ); + + const goToNextStep = useCallback(() => { + if (canGoToNextStep) { + setCurrentStep((step) => step + 1); + } + }, [canGoToNextStep]); + + const goToPrevStep = useCallback(() => { + if (canGoToPrevStep) { + setCurrentStep((step) => step - 1); + } + }, [canGoToPrevStep]); + + const reset = useCallback(() => { + setCurrentStep(0); + }, []); + + return [ + currentStep, + { + goToNextStep, + goToPrevStep, + canGoToNextStep, + canGoToPrevStep, + setStep, + reset, + }, + ]; +}; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 7a4d5f7b83..3fd345aba7 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -51,9 +51,6 @@ function isErrorWithMessage(error: unknown): error is { message: string } { ); } -export const ToTitleCase = (str: string) => { - return str.replace( - /\w\S*/g, - (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() - ); +export const toTitleCase = (str: string) => { + return str.toLowerCase().replace(/\b\w/g, (s) => s.toUpperCase()); };