Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/Employee/Dashboard/DashboardComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Dashboard } from './Dashboard'
import { HomeAddress } from '@/components/Employee/HomeAddress/management/HomeAddress'
import { WorkAddress } from '@/components/Employee/WorkAddress/management/WorkAddress'
import { FederalTaxes } from '@/components/Employee/FederalTaxes/management/FederalTaxes'
import { Profile } from '@/components/Employee/Profile/management/Profile'
import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow'
import { ensureRequired } from '@/helpers/ensureRequired'

Expand All @@ -28,3 +29,8 @@ export function FederalTaxesContextual() {
const { employeeId, onEvent } = useFlow<DashboardContextInterface>()
return <FederalTaxes employeeId={ensureRequired(employeeId)} onEvent={onEvent} />
}

export function ProfileContextual() {
const { employeeId, onEvent } = useFlow<DashboardContextInterface>()
return <Profile employeeId={ensureRequired(employeeId)} onEvent={onEvent} />
}
13 changes: 13 additions & 0 deletions src/components/Employee/Dashboard/dashboardStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HomeAddressContextual,
WorkAddressContextual,
FederalTaxesContextual,
ProfileContextual,
type DashboardContextInterface,
} from './DashboardComponents'
import { componentEvents } from '@/shared/constants'
Expand All @@ -19,6 +20,17 @@ const returnToIndex = reduce(

export const dashboardStateMachine = {
index: state<MachineTransition>(
transition(
componentEvents.EMPLOYEE_UPDATE,
'profile',
reduce(
(ctx: DashboardContextInterface): DashboardContextInterface => ({
...ctx,
component: ProfileContextual,
header: { type: 'minimal' },
}),
),
),
Comment thread
dmortal marked this conversation as resolved.
transition(
componentEvents.EMPLOYEE_HOME_ADDRESS,
'homeAddress',
Expand Down Expand Up @@ -59,4 +71,5 @@ export const dashboardStateMachine = {
transition(componentEvents.CANCEL, 'index', returnToIndex),
transition(componentEvents.EMPLOYEE_FEDERAL_TAXES_DONE, 'index', returnToIndex),
),
profile: state<MachineTransition>(transition(componentEvents.CANCEL, 'index', returnToIndex)),
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PaymentMethodBankAccount } from '@gusto/embedded-api/models/components/paymentmethodbankaccount'
import { FederalTaxes } from '../FederalTaxes/onboarding/FederalTaxes'
import { StateTaxes } from '../StateTaxes/StateTaxes'
import type { ProfileDefaultValues } from '../Profile'
import type { ProfileDefaultValues } from '../Profile/onboarding/Profile'
import type { CompensationDefaultValues } from '../Compensation'
import { EmployeeList } from '../EmployeeList/onboarding/EmployeeList'
import { ensureRequired } from '@/helpers/ensureRequired'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CompensationContextual } from '@/components/Employee/Compensation'
import { DeductionsContextual } from '@/components/Employee/Deductions'
import { EmployeeDocumentsContextual } from '@/components/Employee/EmployeeDocuments'
import { PaymentMethodContextual } from '@/components/Employee/PaymentMethod'
import { ProfileContextual } from '@/components/Employee/Profile'
import { ProfileContextual } from '@/components/Employee/Profile/onboarding/Profile'
import { OnboardingSummaryContextual } from '@/components/Employee/OnboardingSummary'

type EventPayloads = {
Expand Down
1 change: 0 additions & 1 deletion src/components/Employee/Profile/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container {
width: 100%;
}
143 changes: 143 additions & 0 deletions src/components/Employee/Profile/management/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { useEmployeeDetailsForm } from '../shared/useEmployeeDetailsForm'
import styles from './Profile.module.scss'
import {
BaseBoundaries,
BaseLayout,
type BaseComponentInterface,
type CommonComponentInterface,
} from '@/components/Base'
import { ActionsLayout } from '@/components/Common'
import { Form } from '@/components/Common/Form'
import { Grid } from '@/components/Common/Grid/Grid'
import { SDKFormProvider } from '@/partner-hook-utils/form/SDKFormProvider'
import { useI18n, useComponentDictionary } from '@/i18n'
import { componentEvents } from '@/shared/constants'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'

export interface ProfileProps extends CommonComponentInterface<'Employee.Profile'> {
employeeId: string
onEvent: BaseComponentInterface['onEvent']
}

export function Profile({
FallbackComponent,
...props
}: ProfileProps & Pick<BaseComponentInterface, 'FallbackComponent'>) {
return (
<BaseBoundaries componentName="Employee.Profile" FallbackComponent={FallbackComponent}>
<ProfileRoot {...props} />
</BaseBoundaries>
)
}

function ProfileRoot({ employeeId, className, dictionary, onEvent }: ProfileProps) {
useI18n('Employee.Profile')
useComponentDictionary('Employee.Profile', dictionary)
const { t } = useTranslation('Employee.Profile')
const Components = useComponentContext()

const employeeDetails = useEmployeeDetailsForm({
employeeId,
withSelfOnboardingField: false,
optionalFieldsToRequire: {
update: ['firstName', 'lastName', 'email', 'dateOfBirth', 'ssn'],
},
})
Comment thread
dmortal marked this conversation as resolved.

const [showSuccess, setShowSuccess] = useState(false)

if (employeeDetails.isLoading) {
return <BaseLayout isLoading error={employeeDetails.errorHandling.errors} />
}

const Fields = employeeDetails.form.Fields

const handleSubmit = async () => {
setShowSuccess(false)
const result = await employeeDetails.actions.onSubmit({
onEmployeeUpdated: emp => {
onEvent(componentEvents.EMPLOYEE_UPDATED, emp)
},
})
if (!result) return
setShowSuccess(true)
}

const handleCancel = () => {
onEvent(componentEvents.CANCEL)
}

const alert = showSuccess ? (
<Components.Alert
status="success"
label={t('successAlert')}
onDismiss={() => {
setShowSuccess(false)
}}
/>
) : undefined

return (
<section className={classNames(styles.container, className)}>
<BaseLayout error={employeeDetails.errorHandling.errors}>
<SDKFormProvider formHookResult={employeeDetails}>
<Form onSubmit={handleSubmit}>
{alert}
<Components.Heading as="h1">{t('title')}</Components.Heading>
<Grid
gap={{ base: 20, small: 8 }}
gridTemplateColumns={{ base: '1fr', small: ['1fr', 200] }}
>
<Fields.FirstName
label={t('firstName')}
validationMessages={{
REQUIRED: t('validations.firstName'),
INVALID_NAME: t('validations.firstName'),
}}
/>
<Fields.MiddleInitial label={t('middleInitial')} />
</Grid>
<Fields.LastName
label={t('lastName')}
validationMessages={{
REQUIRED: t('validations.lastName'),
INVALID_NAME: t('validations.lastName'),
}}
/>
<Fields.Email
label={t('email')}
description={t('emailDescription')}
validationMessages={{
REQUIRED: t('validations.email'),
INVALID_EMAIL: t('validations.email'),
EMAIL_REQUIRED_FOR_SELF_ONBOARDING: t('validations.email'),
}}
/>
<Fields.Ssn
label={t('ssnLabel')}
validationMessages={{
INVALID_SSN: t('validations.ssn', { ns: 'common' }),
REQUIRED: t('validations.ssnRequired', { ns: 'common' }),
}}
/>
<Fields.DateOfBirth
label={t('dobLabel')}
validationMessages={{ REQUIRED: t('validations.dob', { ns: 'common' }) }}
/>
<ActionsLayout>
<Components.Button variant="secondary" onClick={handleCancel}>
{t('cancelCta')}
</Components.Button>
<Components.Button type="submit" isLoading={employeeDetails.status.isPending}>
{t('saveCta')}
</Components.Button>
</ActionsLayout>
</Form>
</SDKFormProvider>
</BaseLayout>
</section>
)
}
Comment on lines +1 to +143
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Major | test-coverage

New management Profile component (143 lines) has no test file. The analogous FederalTaxes/management/FederalTaxes.test.tsx covers rendering, form validation, cancel/save flows, and event emissions (CANCEL, EMPLOYEE_FEDERAL_TAXES_UPDATED). This component has similar testable behaviors: form rendering, handleSubmit with success alert, handleCancel emitting CANCEL event, loading state, and error handling — all untested.

Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { FormProvider, useForm, useWatch } from 'react-hook-form'
import { Trans, useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { type Employee } from '@gusto/embedded-api/models/components/employee'
import type { ProfileProps } from './Profile'
import { useEmployeeDetailsForm } from '../shared/useEmployeeDetailsForm'
import type { EmployeeDetailsOptionalFieldsToRequire } from '../shared/useEmployeeDetailsForm'
import { useCurrentHomeAddressForm } from '../shared/useHomeAddressForm'
import { useCurrentWorkAddressForm } from '../shared/useWorkAddressForm'
import type { UseEmployeeDetailsFormReady } from '../shared/useEmployeeDetailsForm'
import type { UseHomeAddressFormReady } from '../shared/useHomeAddressForm'
import type { UseWorkAddressFormReady } from '../shared/useWorkAddressForm'
import styles from './AdminProfile.module.scss'
import { useEmployeeDetailsForm } from './shared/useEmployeeDetailsForm'
import type { EmployeeDetailsOptionalFieldsToRequire } from './shared/useEmployeeDetailsForm'
import { useCurrentHomeAddressForm } from './shared/useHomeAddressForm'
import { useCurrentWorkAddressForm } from './shared/useWorkAddressForm'
import type { UseEmployeeDetailsFormReady } from './shared/useEmployeeDetailsForm'
import type { UseHomeAddressFormReady } from './shared/useHomeAddressForm'
import type { UseWorkAddressFormReady } from './shared/useWorkAddressForm'
import type { ProfileProps } from './Profile'
import { SDKFormProvider } from '@/partner-hook-utils/form/SDKFormProvider'
import { composeSubmitHandler } from '@/partner-hook-utils/form/composeSubmitHandler'
import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
Expand Down Expand Up @@ -83,8 +83,7 @@ export function AdminProfile({
)

const employeeDetails = useEmployeeDetailsForm({
companyId,
employeeId: resolvedEmployeeId,
...(resolvedEmployeeId ? { employeeId: resolvedEmployeeId } : { companyId }),
withSelfOnboardingField: true,
optionalFieldsToRequire,
defaultValues: employeeDetailsDefaults,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { useEmployeeAddressesGetWorkAddresses } from '@gusto/embedded-api/react-query/employeeAddressesGetWorkAddresses'
import type { ProfileProps } from './Profile'
import { useEmployeeDetailsForm } from '../shared/useEmployeeDetailsForm'
import type { EmployeeDetailsOptionalFieldsToRequire } from '../shared/useEmployeeDetailsForm'
import { useCurrentHomeAddressForm } from '../shared/useHomeAddressForm'
import styles from './EmployeeProfile.module.scss'
import { useEmployeeDetailsForm } from './shared/useEmployeeDetailsForm'
import type { EmployeeDetailsOptionalFieldsToRequire } from './shared/useEmployeeDetailsForm'
import { useCurrentHomeAddressForm } from './shared/useHomeAddressForm'
import type { ProfileProps } from './Profile'
import { SDKFormProvider } from '@/partner-hook-utils/form/SDKFormProvider'
import { composeSubmitHandler } from '@/partner-hook-utils/form/composeSubmitHandler'
import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler'
Expand Down Expand Up @@ -44,8 +44,7 @@ export function EmployeeProfile({
const [resolvedEmployeeId, setResolvedEmployeeId] = useState(employeeId)

const employeeDetails = useEmployeeDetailsForm({
companyId,
employeeId: resolvedEmployeeId,
...(resolvedEmployeeId ? { employeeId: resolvedEmployeeId } : { companyId }),
withSelfOnboardingField: false,
optionalFieldsToRequire: SELF_OPTIONAL_FIELDS,
defaultValues: defaultValues?.employee,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OnboardingContextInterface } from '../OnboardingFlow/OnboardingFlowComponents'
import type { OnboardingContextInterface } from '../../OnboardingFlow/OnboardingFlowComponents'
import { AdminProfile } from './AdminProfile'
import { EmployeeProfile } from './EmployeeProfile'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
}),
{ wrapper: GustoTestProvider },
Expand All @@ -408,7 +407,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
}),
{ wrapper: GustoTestProvider },
Expand All @@ -433,7 +431,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
defaultValues: {
selfOnboarding: true,
Expand Down Expand Up @@ -461,7 +458,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
optionalFieldsToRequire: {
create: ['email'],
Expand Down Expand Up @@ -501,7 +497,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
optionalFieldsToRequire: { update: ['ssn'] },
}),
Expand Down Expand Up @@ -536,7 +531,6 @@ describe('useEmployeeDetailsForm', () => {
const { result } = renderHook(
() =>
useEmployeeDetailsForm({
companyId: 'company-1',
employeeId: 'emp-1',
optionalFieldsToRequire: { update: ['ssn'] },
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ export interface EmployeeDetailsSubmitCallbacks {
onOnboardingStatusUpdated?: (status: unknown) => void
}

export interface UseEmployeeDetailsFormProps {
companyId: string
employeeId?: string
type UseEmployeeDetailsFormSharedProps = {
withSelfOnboardingField?: boolean
optionalFieldsToRequire?: EmployeeDetailsOptionalFieldsToRequire
defaultValues?: Partial<EmployeeDetailsFormData>
validationMode?: UseFormProps['mode']
shouldFocusError?: boolean
}

export type UseEmployeeDetailsFormProps =
| (UseEmployeeDetailsFormSharedProps & { companyId: string; employeeId?: never })
Comment on lines 54 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Major | undocumented-changes

Significant undocumented change: UseEmployeeDetailsFormProps was refactored from a single interface to a discriminated union type. This changes the hook's public API contract (callers can no longer pass both companyId and employeeId together). This breaking change to the hook interface is not mentioned in the PR description.

| (UseEmployeeDetailsFormSharedProps & { employeeId: string; companyId?: never })

Comment on lines +57 to +60
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i worry a little bit about this for the component usage 🤔

ex. if you are in create mode, you provide the companyId, then you need to remove the company id once you have the employee id?

This is the case you get in employee profile in the partial update case. You set the company id on the hook and then optionally supply the employee id after the update is successful to make sure it gets placed into update mode. In that case, this would introduce the overhead of also needing to omit the company id when the employee id got supplied

Profile usage:

  const employeeDetails = useEmployeeDetailsForm({
    companyId,
    employeeId: resolvedEmployeeId,
    withSelfOnboardingField: true,
    optionalFieldsToRequire,
    defaultValues: employeeDetailsDefaults,
    shouldFocusError: false,
  })

I think if i was articulating the proper scenarios here

Create mode

  • Provide a company id
  • Don't provide an employee id

Update mode

  • Provide an employee id
  • Company id can be present but isn't used

So basically i think that company id should be required if no employee id is present. But if an employee id is present, company id can also be there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

export interface EmployeeDetailsFields {
FirstName: typeof FirstNameField
MiddleInitial: typeof MiddleInitialField
Expand Down Expand Up @@ -182,6 +184,9 @@ export function useEmployeeDetailsForm({
let updatedEmployee: Employee

if (isCreateMode) {
if (!companyId) {
throw new SDKInternalError('companyId is required to create an employee')
}
const result = await createEmployeeMutation.mutateAsync({
request: {
companyId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { FlowContextInterface } from '@/components/Flow/useFlow'
import { useFlow } from '@/components/Flow/useFlow'
import type { BaseComponentInterface } from '@/components/Base'
import { Landing as LandingComponent } from '@/components/Employee/Landing'
import { Profile as ProfileComponent } from '@/components/Employee/Profile'
import { Profile as ProfileComponent } from '@/components/Employee/Profile/onboarding/Profile'
import { FederalTaxes as FederalTaxesComponent } from '@/components/Employee/FederalTaxes/onboarding/FederalTaxes'
import { StateTaxes as StateTaxesComponent } from '@/components/Employee/StateTaxes'
import { PaymentMethod as PaymentMethodComponent } from '@/components/Employee/PaymentMethod'
Expand Down
1 change: 1 addition & 0 deletions src/components/Employee/exports/employeeManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { DashboardFlow } from '../Dashboard'
export { WorkAddress } from '../WorkAddress/management/WorkAddress'
export type { WorkAddressProps } from '../WorkAddress/management/WorkAddress'
export { FederalTaxes, type FederalTaxesProps } from '../FederalTaxes/management/FederalTaxes'
export { Profile, type ProfileProps } from '../Profile/management/Profile'
export { TerminateEmployee } from '../Terminations/TerminateEmployee/TerminateEmployee'
export { TerminationSummary } from '../Terminations/TerminationSummary/TerminationSummary'
export { TerminationFlow } from '../Terminations/TerminationFlow/TerminationFlow'
2 changes: 1 addition & 1 deletion src/components/Employee/exports/employeeOnboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { Landing } from '../Landing'
export { DocumentSigner } from '../DocumentSigner'
export { EmploymentEligibility } from '../DocumentSigner/EmploymentEligibility'

export { Profile } from '../Profile'
export { Profile } from '../Profile/onboarding/Profile'
export { Compensation } from '../Compensation'
export { FederalTaxes, type FederalTaxesProps } from '../FederalTaxes/onboarding/FederalTaxes'
export { StateTaxes } from '../StateTaxes'
Expand Down
2 changes: 1 addition & 1 deletion src/components/Employee/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { EmployeeList } from './EmployeeList/onboarding/EmployeeList'
export { Deductions } from './Deductions'
export { OnboardingSummary } from './OnboardingSummary'
export { Profile } from './Profile'
export { Profile } from './Profile/onboarding/Profile'
export { Compensation } from './Compensation'
export { FederalTaxes } from './FederalTaxes'
export type { FederalTaxesProps } from './FederalTaxes'
Expand Down
Loading
Loading