From 31a820f8e94f353704d08a584f632c95d0a3bc2f Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Wed, 5 Feb 2025 14:40:04 -0700 Subject: [PATCH 1/9] feat: replace api context provider with speakeasy generated provider --- package-lock.json | 32 +++++++++++++++ package.json | 1 + src/api/context.tsx | 40 ++++++------------- .../GustoApiProvider/GustoApiProvider.tsx | 6 +-- src/index.ts | 1 + 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index d98bf82e..11e7084d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { + "@gusto/embedded-api": "^0.1.5", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", @@ -1606,6 +1607,28 @@ "tslib": "2" } }, + "node_modules/@gusto/embedded-api": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@gusto/embedded-api/-/embedded-api-0.1.5.tgz", + "integrity": "sha512-HuptmXiFVuRXY8fq5w8JVY9VAVOClWaY37KP4PYtcrD9WNp+Kr1MZ5FrSfOPQSj6Z91eN+E2bXj2+gppW6NG+g==", + "peerDependencies": { + "@tanstack/react-query": "^5", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "zod": ">= 3" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@hookform/error-message": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", @@ -18967,6 +18990,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 89607aec..9ff1749d 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "typescript": "^5.6.3" }, "dependencies": { + "@gusto/embedded-api": "^0.1.5", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", diff --git a/src/api/context.tsx b/src/api/context.tsx index 8da98efb..a8ad0977 100644 --- a/src/api/context.tsx +++ b/src/api/context.tsx @@ -1,50 +1,36 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { GustoEmbedded } from '@gusto/embedded-api' +import { GustoEmbeddedProvider } from '@gusto/embedded-api/react-query' import { createContext, useContext } from 'react' import { GustoClient } from './client' -import { ApiError } from './queries/helpers' - -const defaultQueryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: (failureCount, error) => { - const apiError = error as ApiError - if (failureCount >= 3) return false - // 4xx errors (excecpt for 429) are unlikely to be fixed by retrying - if ( - apiError.statusCode && - 400 <= apiError.statusCode && - apiError.statusCode <= 499 && - apiError.statusCode !== 429 - ) { - return false - } else { - return true - } - }, - }, - }, -}) type GustoApiContextType = { GustoClient: GustoClient } +const defaultGustoClient = new GustoEmbedded() const GustoApiContext = createContext(null) export function GustoApiContextProvider({ children, context, - queryClient = defaultQueryClient, + gustoClient = defaultGustoClient, }: { context: { GustoClient: GustoClient } - queryClient?: QueryClient + gustoClient?: GustoEmbedded children: React.ReactNode }) { + const queryClient = new QueryClient() + queryClient.setQueryDefaults(['@gusto/embedded-api'], { retry: false }) + queryClient.setMutationDefaults(['@gusto/embedded-api'], { retry: false }) + return ( - {children} + + {children} + ) @@ -55,5 +41,3 @@ export const useGustoApi = () => { if (!context) throw Error('useGustoApi can only be used inside GustoApiProvider.') return context } - -export { defaultQueryClient as queryClient } diff --git a/src/contexts/GustoApiProvider/GustoApiProvider.tsx b/src/contexts/GustoApiProvider/GustoApiProvider.tsx index 8db9acb0..bceacfea 100644 --- a/src/contexts/GustoApiProvider/GustoApiProvider.tsx +++ b/src/contexts/GustoApiProvider/GustoApiProvider.tsx @@ -1,6 +1,6 @@ import { type CustomTypeOptions } from 'i18next' import React, { useEffect, useMemo } from 'react' -import { QueryClient } from '@tanstack/react-query' +import { GustoEmbedded } from '@gusto/embedded-api' import { ErrorBoundary } from 'react-error-boundary' import { I18nextProvider } from 'react-i18next' import { InternalError } from '@/components/Common' @@ -26,7 +26,7 @@ export interface GustoApiProps { currency?: string theme?: DeepPartial children?: React.ReactNode - queryClient?: QueryClient + queryClient?: GustoEmbedded } const GustoApiProvider: React.FC = ({ @@ -65,7 +65,7 @@ const GustoApiProvider: React.FC = ({ - + {children} diff --git a/src/index.ts b/src/index.ts index 1ea9fe9a..267f10be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from '@/api/queries/helpers' export * from '@/components' export * from '@/contexts' export { componentEvents } from '@/shared/constants' +export { GustoEmbedded } from '@gusto/embedded-api' From dfb2d17627a94118854b6193442218978d868dc7 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Wed, 5 Feb 2025 14:52:46 -0700 Subject: [PATCH 2/9] feat: replace first api call --- src/components/Employee/EmployeeList/EmployeeList.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Employee/EmployeeList/EmployeeList.tsx b/src/components/Employee/EmployeeList/EmployeeList.tsx index e70c87c1..324b4611 100644 --- a/src/components/Employee/EmployeeList/EmployeeList.tsx +++ b/src/components/Employee/EmployeeList/EmployeeList.tsx @@ -11,6 +11,7 @@ import { useI18n } from '@/i18n' import { componentEvents, EmployeeOnboardingStatus } from '@/shared/constants' import { Schemas } from '@/types/schema' import { useDeleteEmployee, useGetEmployeesByCompany } from '@/api/queries/company' +import { useEmployeesGetSuspense } from '@gusto/embedded-api/react-query' import { Head } from '@/components/Employee/EmployeeList/Head' import { List } from '@/components/Employee/EmployeeList/List' import { useUpdateEmployeeOnboardingStatus } from '@/api/queries' @@ -57,8 +58,8 @@ function Root({ companyId, className, children }: EmployeeListProps) { const [currentPage, setCurrentPage] = useState(1) const [itemsPerPage, setItemsPerPage] = useState(5) - const { data } = useGetEmployeesByCompany({ - company_id: companyId, + const { data } = useEmployeesGetSuspense({ + companyId, page: currentPage, per: itemsPerPage, }) From 7359177be59d20fdeedaeddf88efcdf18cf3a6ce Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Thu, 6 Feb 2025 10:31:11 -0700 Subject: [PATCH 3/9] feat: link to local generated client for faster feedback - Changed vite config according to this thread on including local dependencies: https://github.com/vitejs/vite/issues/15412#issuecomment-1868436277 --- package-lock.json | 64 +++++++++++++++++++++++++---------------------- package.json | 2 +- vite.config.ts | 1 + 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11e7084d..14ba08c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { - "@gusto/embedded-api": "^0.1.5", + "@gusto/embedded-api": "file:../gusto-typescript-client/gusto_embedded", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", @@ -72,6 +72,37 @@ "typescript": "^5.6.3" } }, + "../gusto-typescript-client/gusto_embedded": { + "name": "@gusto/embedded-api", + "version": "0.1.18", + "devDependencies": { + "@eslint/js": "^9.19.0", + "@tanstack/react-query": "^5.61.4", + "@types/react": "^18.3.12", + "eslint": "^9.19.0", + "globals": "^15.14.0", + "typescript": "^5.4.5", + "typescript-eslint": "^8.22.0", + "zod": "^3.23.4" + }, + "peerDependencies": { + "@tanstack/react-query": "^5", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "zod": ">= 3" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", @@ -1608,26 +1639,8 @@ } }, "node_modules/@gusto/embedded-api": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@gusto/embedded-api/-/embedded-api-0.1.5.tgz", - "integrity": "sha512-HuptmXiFVuRXY8fq5w8JVY9VAVOClWaY37KP4PYtcrD9WNp+Kr1MZ5FrSfOPQSj6Z91eN+E2bXj2+gppW6NG+g==", - "peerDependencies": { - "@tanstack/react-query": "^5", - "react": "^18 || ^19", - "react-dom": "^18 || ^19", - "zod": ">= 3" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } + "resolved": "../gusto-typescript-client/gusto_embedded", + "link": true }, "node_modules/@hookform/error-message": { "version": "2.0.1", @@ -18990,15 +19003,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 9ff1749d..02c5a815 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "@gusto/embedded-api": "^0.1.5", + "@gusto/embedded-api": "file:../gusto-typescript-client/gusto_embedded", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", diff --git a/vite.config.ts b/vite.config.ts index 9f408691..99a0fdf7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,6 +35,7 @@ export default defineConfig({ }), ], resolve: { + preserveSymlinks: true, alias: { '@': resolve(__dirname, './src'), }, From cdb61a901d002e9a5e99c07184cc19e4a4cdc942 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 7 Feb 2025 09:18:38 -0700 Subject: [PATCH 4/9] feat: converts entire Employee Profile component to use speakeasy generated client --- .../Employee/EmployeeList/EmployeeList.tsx | 5 +- src/components/Employee/EmployeeList/List.tsx | 2 +- src/components/Employee/Profile/Profile.tsx | 221 +++++++++++------- 3 files changed, 143 insertions(+), 85 deletions(-) diff --git a/src/components/Employee/EmployeeList/EmployeeList.tsx b/src/components/Employee/EmployeeList/EmployeeList.tsx index 324b4611..f118dc28 100644 --- a/src/components/Employee/EmployeeList/EmployeeList.tsx +++ b/src/components/Employee/EmployeeList/EmployeeList.tsx @@ -58,7 +58,7 @@ function Root({ companyId, className, children }: EmployeeListProps) { const [currentPage, setCurrentPage] = useState(1) const [itemsPerPage, setItemsPerPage] = useState(5) - const { data } = useEmployeesGetSuspense({ + const { data: employees } = useEmployeesGetSuspense({ companyId, page: currentPage, per: itemsPerPage, @@ -66,7 +66,8 @@ function Root({ companyId, className, children }: EmployeeListProps) { const deleteEmployeeMutation = useDeleteEmployee(companyId) const updateEmployeeOnboardingStatusMutation = useUpdateEmployeeOnboardingStatus(companyId) - const { items: employees, pagination } = data + // TODO: Get this from response headers + const pagination = { totalPages: 1 } const totalPages = Number(pagination.totalPages) || 1 const handleItemsPerPageChange = (newCount: number) => { diff --git a/src/components/Employee/EmployeeList/List.tsx b/src/components/Employee/EmployeeList/List.tsx index 1dd302ea..4efe727a 100644 --- a/src/components/Employee/EmployeeList/List.tsx +++ b/src/components/Employee/EmployeeList/List.tsx @@ -45,7 +45,7 @@ export const List = () => { key: 'name', title: t('nameLabel'), render: employee => { - return firstLastName(employee) + return firstLastName({ first_name: employee.firstName, last_name: employee.lastName }) }, }, { diff --git a/src/components/Employee/Profile/Profile.tsx b/src/components/Employee/Profile/Profile.tsx index 38430963..3790a6ee 100644 --- a/src/components/Employee/Profile/Profile.tsx +++ b/src/components/Employee/Profile/Profile.tsx @@ -20,17 +20,18 @@ import { EmployeeSelfOnboardingStatuses, } from '@/shared/constants' import { - useAddEmployeeHomeAddress, - useAddEmployeeWorkAddress, - useGetEmployee, - useGetEmployeeHomeAddresses, - useGetEmployeeWorkAddresses, - useUpdateEmployee, - useUpdateEmployeeHomeAddress, - useUpdateEmployeeOnboardingStatus, - useUpdateEmployeeWorkAddress, -} from '@/api/queries/employee' -import { useCreateEmployee, useGetCompanyLocations } from '@/api/queries/company' + useEmployeeAddressesUpdateHomeAddressMutation, + useEmployeeAddressesCreateMutation, + useEmployeeAddressesUpdateWorkAddressMutation, + useEmployeeAddressesWorkAddressesCreateMutation, + useEmployeesCreateMutation, + useEmployeesUpdateMutation, + useLocationsGetAllSuspense, + useEmployeesUpdateOnboardingStatusMutation, + useEmployeesRetrieve, + useEmployeeAddressesGetHomeAddresses, + useEmployeeAddressesGet, +} from '@gusto/embedded-api/react-query' import { Schemas } from '@/types/schema' import { AdminPersonalDetails, AdminPersonalDetailsSchema } from './AdminPersonalDetails' @@ -92,10 +93,19 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { useI18n('Employee.HomeAddress') const { companyId, employeeId, children, className = '', defaultValues } = props const { onEvent, baseSubmitHandler } = useBase() - const { data: companyLocations } = useGetCompanyLocations(companyId) - const { data: employee } = useGetEmployee(employeeId) - const { data: workAddresses } = useGetEmployeeWorkAddresses(employeeId) - const { data: homeAddresses } = useGetEmployeeHomeAddresses(employeeId) + const { data: companyLocations } = useLocationsGetAllSuspense({ companyId }) + const { data: employee } = useEmployeesRetrieve( + { employeeId: employeeId ?? '' }, + { enabled: employeeId != undefined }, + ) + const { data: workAddresses } = useEmployeeAddressesGet( + { employeeId: employeeId ?? '' }, + { enabled: employeeId != undefined }, + ) + const { data: homeAddresses } = useEmployeeAddressesGetHomeAddresses( + { employeeId: employeeId ?? '' }, + { enabled: employeeId != undefined }, + ) const existingData = { employee, workAddresses, homeAddresses } @@ -109,55 +119,52 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { workAddress: currentWorkAddress, }) const initialValues = { - first_name: - mergedData.current.employee?.first_name ?? defaultValues?.employee?.first_name ?? '', + first_name: mergedData.current.employee?.firstName ?? defaultValues?.employee?.first_name ?? '', middle_initial: - mergedData.current.employee?.middle_initial ?? defaultValues?.employee?.middle_initial ?? '', - last_name: mergedData.current.employee?.last_name ?? defaultValues?.employee?.last_name ?? '', - work_address: mergedData.current.workAddress?.location_uuid, - start_date: mergedData.current.employee?.jobs?.[0]?.hire_date - ? parseDate(mergedData.current.employee.jobs[0].hire_date) + mergedData.current.employee?.middleInitial ?? defaultValues?.employee?.middle_initial ?? '', + last_name: mergedData.current.employee?.lastName ?? defaultValues?.employee?.last_name ?? '', + work_address: mergedData.current.workAddress?.locationUuid, + start_date: mergedData.current.employee?.jobs?.[0]?.hireDate + ? parseDate(mergedData.current.employee.jobs[0].hireDate) : null, // By default employee response contains only current job - therefore jobs[0] email: mergedData.current.employee?.email ?? defaultValues?.employee?.email ?? '', - date_of_birth: mergedData.current.employee?.date_of_birth - ? parseDate(mergedData.current.employee.date_of_birth) + date_of_birth: mergedData.current.employee?.dateOfBirth + ? parseDate(mergedData.current.employee.dateOfBirth) : defaultValues?.employee?.date_of_birth ? parseDate(defaultValues.employee.date_of_birth) : null, - street_1: - mergedData.current.homeAddress?.street_1 ?? defaultValues?.homeAddress?.street_1 ?? '', - street_2: - mergedData.current.homeAddress?.street_2 ?? defaultValues?.homeAddress?.street_2 ?? '', + street_1: mergedData.current.homeAddress?.street1 ?? defaultValues?.homeAddress?.street_1 ?? '', + street_2: mergedData.current.homeAddress?.street2 ?? defaultValues?.homeAddress?.street_2 ?? '', city: mergedData.current.homeAddress?.city ?? defaultValues?.homeAddress?.city ?? '', zip: mergedData.current.homeAddress?.zip ?? defaultValues?.homeAddress?.zip ?? '', state: mergedData.current.homeAddress?.state ?? defaultValues?.homeAddress?.state ?? '', effective_date: - mergedData.current.homeAddress?.effective_date ?? today(getLocalTimeZone()).toString(), - courtesy_withholding: mergedData.current.homeAddress?.courtesy_withholding ?? false, + mergedData.current.homeAddress?.effectiveDate ?? today(getLocalTimeZone()).toString(), + courtesy_withholding: mergedData.current.homeAddress?.courtesyWithholding ?? false, } const adminDefaultValues = mergedData.current.employee?.onboarded || - mergedData.current.employee?.onboarding_status === + mergedData.current.employee?.onboardingStatus === EmployeeOnboardingStatus.ONBOARDING_COMPLETED || - (mergedData.current.employee?.onboarding_status !== undefined && - mergedData.current.employee.onboarding_status !== + (mergedData.current.employee?.onboardingStatus !== undefined && + mergedData.current.employee.onboardingStatus !== EmployeeOnboardingStatus.ADMIN_ONBOARDING_INCOMPLETE) ? { ...initialValues, enableSsn: false, self_onboarding: true } : { ...initialValues, - self_onboarding: mergedData.current.employee?.onboarding_status + self_onboarding: mergedData.current.employee?.onboardingStatus ? // @ts-expect-error: onboarding_status during runtime can be one of self onboarding statuses EmployeeSelfOnboardingStatuses.has(mergedData.current.employee.onboarding_status) : false, - enableSsn: !mergedData.current.employee?.has_ssn, + enableSsn: !mergedData.current.employee?.hasSsn, ssn: '', } // In edit mode ssn is submitted only if it has been modified const selfDetaultValues = { ...initialValues, - enableSsn: !mergedData.current.employee?.has_ssn, + enableSsn: !mergedData.current.employee?.hasSsn, ssn: '', } @@ -178,17 +185,44 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { const { handleSubmit } = formMethods const watchedSelfOnboarding = useWatch({ control: formMethods.control, name: 'self_onboarding' }) - const { mutateAsync: createEmployee, isPending: isPendingCreateEmployee } = useCreateEmployee() - const { mutateAsync: mutateEmployee, isPending: isPendingEmployeeUpdate } = useUpdateEmployee() + const { mutateAsync: createEmployee, isPending: isPendingCreateEmployee } = + useEmployeesCreateMutation() + const { mutateAsync: mutateEmployee, isPending: isPendingEmployeeUpdate } = + useEmployeesUpdateMutation() const { mutateAsync: createEmployeeWorkAddress, isPending: isPendingCreateWA } = - useAddEmployeeWorkAddress() + useEmployeeAddressesWorkAddressesCreateMutation() const { mutateAsync: mutateEmployeeWorkAddress, isPending: isPendingWorkAddressUpdate } = - useUpdateEmployeeWorkAddress() + useEmployeeAddressesUpdateWorkAddressMutation() const { mutateAsync: createEmployeeHomeAddress, isPending: isPendingAddHA } = - useAddEmployeeHomeAddress() + useEmployeeAddressesCreateMutation() const { mutateAsync: mutateEmployeeHomeAddress, isPending: isPendingUpdateHA } = - useUpdateEmployeeHomeAddress() - const updateEmployeeOnboardingStatusMutation = useUpdateEmployeeOnboardingStatus(companyId) + useEmployeeAddressesUpdateHomeAddressMutation() + const { mutateAsync: updateEmployeeOnboardingStatusMutation } = + useEmployeesUpdateOnboardingStatusMutation() + + // TODO: Adding this for now, will have to think of a more scalable solution + // later (i.e. do we want to change form schemas to use camel case? do we + // want to just expose this function as a shared library function?) + const camelCaseKeys = obj => { + if (typeof obj !== 'object' || obj === null) { + return obj // Handle non-object inputs (e.g., primitives, null) + } + + if (Array.isArray(obj)) { + return obj.map(camelCaseKeys) // Recursively handle arrays + } + + const newObj = {} + for (const key in obj) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, no-prototype-builtins + if (obj.hasOwnProperty(key)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const camelCaseKey = key.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase()) + newObj[camelCaseKey] = camelCaseKeys(obj[key]) // Recursively handle nested objects/arrays + } + } + return newObj + } const onSubmit: SubmitHandler = async data => { await baseSubmitHandler(data, async payload => { @@ -196,34 +230,44 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { //create or update employee if (!mergedData.current.employee) { const employeeData = await createEmployee({ - company_id: companyId, - body: { ...body, self_onboarding }, + request: { + companyId, + requestBody: { + ...camelCaseKeys(body), + selfOnboarding: self_onboarding, + }, + }, }) + mergedData.current = { ...mergedData.current, employee: employeeData } onEvent(componentEvents.EMPLOYEE_CREATED, employeeData) } else { // Updating self-onboarding status if ( (self_onboarding && - mergedData.current.employee.onboarding_status === + mergedData.current.employee.onboardingStatus === EmployeeOnboardingStatus.ADMIN_ONBOARDING_INCOMPLETE) || (!self_onboarding && - mergedData.current.employee.onboarding_status === + mergedData.current.employee.onboardingStatus === EmployeeOnboardingStatus.SELF_ONBOARDING_PENDING_INVITE) ) { - const updateEmployeeOnboardingStatusResult = - await updateEmployeeOnboardingStatusMutation.mutateAsync({ - employeeId: mergedData.current.employee.uuid, - body: { - onboarding_status: self_onboarding - ? EmployeeOnboardingStatus.SELF_ONBOARDING_PENDING_INVITE - : EmployeeOnboardingStatus.ADMIN_ONBOARDING_INCOMPLETE, + const updateEmployeeOnboardingStatusResult = await updateEmployeeOnboardingStatusMutation( + { + request: { + employeeId: mergedData.current.employee.uuid, + requestBody: { + onboardingStatus: self_onboarding + ? EmployeeOnboardingStatus.SELF_ONBOARDING_PENDING_INVITE + : EmployeeOnboardingStatus.ADMIN_ONBOARDING_INCOMPLETE, + }, }, - }) + }, + ) mergedData.current.employee = { ...mergedData.current.employee, - onboarding_status: - updateEmployeeOnboardingStatusResult.onboarding_status as (typeof EmployeeOnboardingStatus)[keyof typeof EmployeeOnboardingStatus], + onboardingStatus: + // TODO: configure enums in Open API spec. + updateEmployeeOnboardingStatusResult.onboardingStatus as (typeof EmployeeOnboardingStatus)[keyof typeof EmployeeOnboardingStatus], } onEvent( componentEvents.EMPLOYEE_ONBOARDING_STATUS_UPDATED, @@ -231,8 +275,13 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { ) } const employeeData = await mutateEmployee({ - employee_id: mergedData.current.employee.uuid, - body: { ...body, version: mergedData.current.employee.version as string }, + request: { + employeeId: mergedData.current.employee.uuid, + requestBody: { + ...camelCaseKeys(body), + version: mergedData.current.employee.version as string, + }, + }, }) mergedData.current = { ...mergedData.current, employee: employeeData } onEvent(componentEvents.EMPLOYEE_UPDATED, employeeData) @@ -248,29 +297,33 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { if (!mergedData.current.homeAddress) { // Creating home address - for new employee effective_date is the same as work start date const homeAddressData = await createEmployeeHomeAddress({ - employee_id: mergedData.current.employee.uuid, - body: { - street_1, - street_2, - city, - state, - zip, - courtesy_withholding, + request: { + employeeId: mergedData.current.employee.uuid, + requestBody: { + street1: street_1, + street2: street_2, + city, + state, + zip, + courtesyWithholding: courtesy_withholding, + }, }, }) mergedData.current = { ...mergedData.current, homeAddress: homeAddressData } onEvent(componentEvents.EMPLOYEE_HOME_ADDRESS_CREATED, homeAddressData) } else { const homeAddressData = await mutateEmployeeHomeAddress({ - home_address_uuid: mergedData.current.homeAddress.uuid as string, - body: { - version: mergedData.current.homeAddress.version as string, - street_1, - street_2, - city, - state, - zip, - courtesy_withholding, + request: { + homeAddressUuid: mergedData.current.homeAddress.uuid as string, + requestBody: { + version: mergedData.current.homeAddress.version as string, + street1: street_1, + street2: street_2, + city, + state, + zip, + courtesyWithholding: courtesy_withholding, + }, }, }) mergedData.current = { ...mergedData.current, homeAddress: homeAddressData } @@ -283,8 +336,10 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { //create or update workaddress if (!mergedData.current.workAddress) { const workAddressData = await createEmployeeWorkAddress({ - employee_id: mergedData.current.employee?.uuid as string, - body: { location_uuid: work_address, effective_date: start_date }, + request: { + employeeId: mergedData.current.employee?.uuid as string, + requestBody: { locationUuid: work_address, effectiveDate: start_date }, + }, }) mergedData.current = { ...mergedData.current, workAddress: workAddressData } @@ -292,10 +347,12 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { } else { //effective_date is excluded from update operation since it cannot be changed on initial work address const workAddressData = await mutateEmployeeWorkAddress({ - work_address_uuid: mergedData.current.workAddress.uuid, - body: { - version: mergedData.current.workAddress.version, - location_uuid: work_address, + request: { + workAddressUuid: mergedData.current.workAddress.uuid, + requestBody: { + version: mergedData.current.workAddress.version, + locationUuid: work_address, + }, }, }) mergedData.current = { ...mergedData.current, workAddress: workAddressData } From 75637508ffd1b3d56c86b0041c0d241ecb577608 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Wed, 12 Feb 2025 10:28:25 -0700 Subject: [PATCH 5/9] feat: pairing --- src/api/context.tsx | 1 + src/components/Employee/Profile/Profile.tsx | 4 +++- src/contexts/GustoApiProvider/GustoApiProvider.tsx | 11 ++++++++--- src/index.ts | 1 - 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/api/context.tsx b/src/api/context.tsx index a8ad0977..a9fbf47d 100644 --- a/src/api/context.tsx +++ b/src/api/context.tsx @@ -24,6 +24,7 @@ export function GustoApiContextProvider({ const queryClient = new QueryClient() queryClient.setQueryDefaults(['@gusto/embedded-api'], { retry: false }) queryClient.setMutationDefaults(['@gusto/embedded-api'], { retry: false }) + console.log("queryClient", queryClient) return ( diff --git a/src/components/Employee/Profile/Profile.tsx b/src/components/Employee/Profile/Profile.tsx index 3790a6ee..d3c6e3fa 100644 --- a/src/components/Employee/Profile/Profile.tsx +++ b/src/components/Employee/Profile/Profile.tsx @@ -89,6 +89,8 @@ export function Profile(props: ProfileProps & BaseComponentInterface) { } const Root = ({ isAdmin = false, ...props }: ProfileProps) => { + console.log("inside react sdk root") + useI18n('Employee.Profile') useI18n('Employee.HomeAddress') const { companyId, employeeId, children, className = '', defaultValues } = props @@ -103,7 +105,7 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { { enabled: employeeId != undefined }, ) const { data: homeAddresses } = useEmployeeAddressesGetHomeAddresses( - { employeeId: employeeId ?? '' }, + { employeeId: 'hello' }, { enabled: employeeId != undefined }, ) diff --git a/src/contexts/GustoApiProvider/GustoApiProvider.tsx b/src/contexts/GustoApiProvider/GustoApiProvider.tsx index bceacfea..9789e40c 100644 --- a/src/contexts/GustoApiProvider/GustoApiProvider.tsx +++ b/src/contexts/GustoApiProvider/GustoApiProvider.tsx @@ -26,7 +26,6 @@ export interface GustoApiProps { currency?: string theme?: DeepPartial children?: React.ReactNode - queryClient?: GustoEmbedded } const GustoApiProvider: React.FC = ({ @@ -37,10 +36,16 @@ const GustoApiProvider: React.FC = ({ currency = 'USD', theme, children, - queryClient, }) => { const context = useMemo(() => ({ GustoClient: new GustoClient(config) }), [config]) + let gustoClient; + if (config.baseUrl) { + gustoClient = new GustoEmbedded({ + serverURL: `http://localhost:7777/${config.baseUrl}` + }) + } + if (dictionary) { for (const language in dictionary) { for (const ns in dictionary[language]) { @@ -65,7 +70,7 @@ const GustoApiProvider: React.FC = ({ - + {children} diff --git a/src/index.ts b/src/index.ts index 267f10be..1ea9fe9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,3 @@ export * from '@/api/queries/helpers' export * from '@/components' export * from '@/contexts' export { componentEvents } from '@/shared/constants' -export { GustoEmbedded } from '@gusto/embedded-api' From f357644291447a46e82e87f60d72f33ed594a75b Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 14 Feb 2025 13:43:58 -0700 Subject: [PATCH 6/9] chore: move queryClient init outside of component --- src/api/context.tsx | 9 ++++----- src/components/Employee/Profile/Profile.tsx | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/api/context.tsx b/src/api/context.tsx index a9fbf47d..19dfbc21 100644 --- a/src/api/context.tsx +++ b/src/api/context.tsx @@ -12,6 +12,10 @@ type GustoApiContextType = { const defaultGustoClient = new GustoEmbedded() const GustoApiContext = createContext(null) +const queryClient = new QueryClient() +queryClient.setQueryDefaults(['@gusto/embedded-api'], { retry: false }) +queryClient.setMutationDefaults(['@gusto/embedded-api'], { retry: false }) + export function GustoApiContextProvider({ children, context, @@ -21,11 +25,6 @@ export function GustoApiContextProvider({ gustoClient?: GustoEmbedded children: React.ReactNode }) { - const queryClient = new QueryClient() - queryClient.setQueryDefaults(['@gusto/embedded-api'], { retry: false }) - queryClient.setMutationDefaults(['@gusto/embedded-api'], { retry: false }) - console.log("queryClient", queryClient) - return ( diff --git a/src/components/Employee/Profile/Profile.tsx b/src/components/Employee/Profile/Profile.tsx index d3c6e3fa..613ea5a8 100644 --- a/src/components/Employee/Profile/Profile.tsx +++ b/src/components/Employee/Profile/Profile.tsx @@ -89,8 +89,6 @@ export function Profile(props: ProfileProps & BaseComponentInterface) { } const Root = ({ isAdmin = false, ...props }: ProfileProps) => { - console.log("inside react sdk root") - useI18n('Employee.Profile') useI18n('Employee.HomeAddress') const { companyId, employeeId, children, className = '', defaultValues } = props From 62c9a84cdca0ec3b36cc762ba826f147b83b111e Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 14 Feb 2025 13:45:02 -0700 Subject: [PATCH 7/9] feat: clear employee cache when updating onboarding status --- src/components/Employee/Profile/Profile.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Employee/Profile/Profile.tsx b/src/components/Employee/Profile/Profile.tsx index 613ea5a8..e8317ce7 100644 --- a/src/components/Employee/Profile/Profile.tsx +++ b/src/components/Employee/Profile/Profile.tsx @@ -1,4 +1,5 @@ import { valibotResolver } from '@hookform/resolvers/valibot' +import { useQueryClient } from '@tanstack/react-query' import { getLocalTimeZone, parseDate, today } from '@internationalized/date' import { useRef } from 'react' import { Form } from 'react-aria-components' @@ -31,6 +32,7 @@ import { useEmployeesRetrieve, useEmployeeAddressesGetHomeAddresses, useEmployeeAddressesGet, + invalidateEmployeesGet, } from '@gusto/embedded-api/react-query' import { Schemas } from '@/types/schema' @@ -89,6 +91,7 @@ export function Profile(props: ProfileProps & BaseComponentInterface) { } const Root = ({ isAdmin = false, ...props }: ProfileProps) => { + const queryClient = useQueryClient() useI18n('Employee.Profile') useI18n('Employee.HomeAddress') const { companyId, employeeId, children, className = '', defaultValues } = props @@ -198,7 +201,9 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { const { mutateAsync: mutateEmployeeHomeAddress, isPending: isPendingUpdateHA } = useEmployeeAddressesUpdateHomeAddressMutation() const { mutateAsync: updateEmployeeOnboardingStatusMutation } = - useEmployeesUpdateOnboardingStatusMutation() + useEmployeesUpdateOnboardingStatusMutation({ + onSettled: () => invalidateEmployeesGet(queryClient, [companyId]), + }) // TODO: Adding this for now, will have to think of a more scalable solution // later (i.e. do we want to change form schemas to use camel case? do we From b4f71078b6d1e75f605b10e1c5066659e21894e3 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 14 Feb 2025 13:45:59 -0700 Subject: [PATCH 8/9] Revert "feat: link to local generated client for faster feedback" This reverts commit 7359177be59d20fdeedaeddf88efcdf18cf3a6ce. --- package-lock.json | 64 ++++++++++++++++++++++------------------------- package.json | 2 +- vite.config.ts | 1 - 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14ba08c2..11e7084d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { - "@gusto/embedded-api": "file:../gusto-typescript-client/gusto_embedded", + "@gusto/embedded-api": "^0.1.5", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", @@ -72,37 +72,6 @@ "typescript": "^5.6.3" } }, - "../gusto-typescript-client/gusto_embedded": { - "name": "@gusto/embedded-api", - "version": "0.1.18", - "devDependencies": { - "@eslint/js": "^9.19.0", - "@tanstack/react-query": "^5.61.4", - "@types/react": "^18.3.12", - "eslint": "^9.19.0", - "globals": "^15.14.0", - "typescript": "^5.4.5", - "typescript-eslint": "^8.22.0", - "zod": "^3.23.4" - }, - "peerDependencies": { - "@tanstack/react-query": "^5", - "react": "^18 || ^19", - "react-dom": "^18 || ^19", - "zod": ">= 3" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", @@ -1639,8 +1608,26 @@ } }, "node_modules/@gusto/embedded-api": { - "resolved": "../gusto-typescript-client/gusto_embedded", - "link": true + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@gusto/embedded-api/-/embedded-api-0.1.5.tgz", + "integrity": "sha512-HuptmXiFVuRXY8fq5w8JVY9VAVOClWaY37KP4PYtcrD9WNp+Kr1MZ5FrSfOPQSj6Z91eN+E2bXj2+gppW6NG+g==", + "peerDependencies": { + "@tanstack/react-query": "^5", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "zod": ">= 3" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } }, "node_modules/@hookform/error-message": { "version": "2.0.1", @@ -19003,6 +18990,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 02c5a815..9ff1749d 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "@gusto/embedded-api": "file:../gusto-typescript-client/gusto_embedded", + "@gusto/embedded-api": "^0.1.5", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.6", diff --git a/vite.config.ts b/vite.config.ts index 99a0fdf7..9f408691 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,7 +35,6 @@ export default defineConfig({ }), ], resolve: { - preserveSymlinks: true, alias: { '@': resolve(__dirname, './src'), }, From c28da663055a00f620f61014d3223bdc8000f1aa Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 14 Feb 2025 13:58:57 -0700 Subject: [PATCH 9/9] chore: update to latest version of generated client and update function names --- package-lock.json | 6 ++--- src/components/Employee/Profile/Profile.tsx | 22 +++++++++---------- .../GustoApiProvider/GustoApiProvider.tsx | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11e7084d..74510c6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1608,9 +1608,9 @@ } }, "node_modules/@gusto/embedded-api": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@gusto/embedded-api/-/embedded-api-0.1.5.tgz", - "integrity": "sha512-HuptmXiFVuRXY8fq5w8JVY9VAVOClWaY37KP4PYtcrD9WNp+Kr1MZ5FrSfOPQSj6Z91eN+E2bXj2+gppW6NG+g==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@gusto/embedded-api/-/embedded-api-0.1.19.tgz", + "integrity": "sha512-7gZzu2f5zXJF2IleRfbWiPS29qLCdmRxalXSrnfApDHiHOSMZ5uvBI+z7B9ESZECbqdfDxgVNwlm6kqYx3waug==", "peerDependencies": { "@tanstack/react-query": "^5", "react": "^18 || ^19", diff --git a/src/components/Employee/Profile/Profile.tsx b/src/components/Employee/Profile/Profile.tsx index e8317ce7..b0c743f0 100644 --- a/src/components/Employee/Profile/Profile.tsx +++ b/src/components/Employee/Profile/Profile.tsx @@ -21,18 +21,16 @@ import { EmployeeSelfOnboardingStatuses, } from '@/shared/constants' import { - useEmployeeAddressesUpdateHomeAddressMutation, useEmployeeAddressesCreateMutation, useEmployeeAddressesUpdateWorkAddressMutation, - useEmployeeAddressesWorkAddressesCreateMutation, + useEmployeeAddressesUpdateMutation, useEmployeesCreateMutation, useEmployeesUpdateMutation, - useLocationsGetAllSuspense, + useLocationsGetSuspense, useEmployeesUpdateOnboardingStatusMutation, - useEmployeesRetrieve, - useEmployeeAddressesGetHomeAddresses, + useEmployeesList, useEmployeeAddressesGet, - invalidateEmployeesGet, + invalidateEmployeesList, } from '@gusto/embedded-api/react-query' import { Schemas } from '@/types/schema' @@ -96,8 +94,8 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { useI18n('Employee.HomeAddress') const { companyId, employeeId, children, className = '', defaultValues } = props const { onEvent, baseSubmitHandler } = useBase() - const { data: companyLocations } = useLocationsGetAllSuspense({ companyId }) - const { data: employee } = useEmployeesRetrieve( + const { data: companyLocations } = useLocationsGetSuspense({ companyId }) + const { data: employee } = useEmployeesList( { employeeId: employeeId ?? '' }, { enabled: employeeId != undefined }, ) @@ -105,7 +103,7 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { { employeeId: employeeId ?? '' }, { enabled: employeeId != undefined }, ) - const { data: homeAddresses } = useEmployeeAddressesGetHomeAddresses( + const { data: homeAddresses } = useEmployeeAddressesGet( { employeeId: 'hello' }, { enabled: employeeId != undefined }, ) @@ -193,16 +191,16 @@ const Root = ({ isAdmin = false, ...props }: ProfileProps) => { const { mutateAsync: mutateEmployee, isPending: isPendingEmployeeUpdate } = useEmployeesUpdateMutation() const { mutateAsync: createEmployeeWorkAddress, isPending: isPendingCreateWA } = - useEmployeeAddressesWorkAddressesCreateMutation() + useEmployeeAddressesUpdateMutation() const { mutateAsync: mutateEmployeeWorkAddress, isPending: isPendingWorkAddressUpdate } = useEmployeeAddressesUpdateWorkAddressMutation() const { mutateAsync: createEmployeeHomeAddress, isPending: isPendingAddHA } = useEmployeeAddressesCreateMutation() const { mutateAsync: mutateEmployeeHomeAddress, isPending: isPendingUpdateHA } = - useEmployeeAddressesUpdateHomeAddressMutation() + useEmployeeAddressesUpdateMutation() const { mutateAsync: updateEmployeeOnboardingStatusMutation } = useEmployeesUpdateOnboardingStatusMutation({ - onSettled: () => invalidateEmployeesGet(queryClient, [companyId]), + onSettled: () => invalidateEmployeesList(queryClient, [companyId]), }) // TODO: Adding this for now, will have to think of a more scalable solution diff --git a/src/contexts/GustoApiProvider/GustoApiProvider.tsx b/src/contexts/GustoApiProvider/GustoApiProvider.tsx index 9789e40c..cc8903c2 100644 --- a/src/contexts/GustoApiProvider/GustoApiProvider.tsx +++ b/src/contexts/GustoApiProvider/GustoApiProvider.tsx @@ -39,10 +39,10 @@ const GustoApiProvider: React.FC = ({ }) => { const context = useMemo(() => ({ GustoClient: new GustoClient(config) }), [config]) - let gustoClient; + let gustoClient if (config.baseUrl) { gustoClient = new GustoEmbedded({ - serverURL: `http://localhost:7777/${config.baseUrl}` + serverURL: `http://localhost:7777/${config.baseUrl}`, }) }