Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ Each flow handles a specific Remote employment operation. For detailed API docum
- [**Contract Amendment**](./src/flows/ContractAmendment/README.md) - Modify existing employment contracts
- [**Termination**](./src/flows/Termination/README.md) - Handle employee terminations
- [**Invoice Schedule**](./src/flows/InvoiceSchedule/README.md) - Standalone screen to create a contractor invoice schedule
- [**Contract Document**](./src/flows/ContractDocument/README.md) - Standalone flow to create a contractor's contract document

## Authentication

Expand Down
10 changes: 10 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { CostCalculatorWithPremiumBenefits } from './CostCalculatorWithPremiumBe
import { TerminationForm } from './Termination';
import { ContractAmendment } from './ContractAmendment';
import { InvoiceSchedule } from './InvoiceSchedule';
import { ContractDocument } from './ContractDocument';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import { OnboardingForm } from './flows/Onboarding/Onboarding';
Expand All @@ -46,6 +47,7 @@ import CostCalculatorWithReplaceableComponentsCode from './CostCalculatorWithRep
import TerminationCode from './Termination?raw';
import ContractAmendmentCode from './ContractAmendment?raw';
import InvoiceScheduleCode from './InvoiceSchedule?raw';
import ContractDocumentCode from './ContractDocument?raw';
import { ContractorOnboardingForm } from './ContractorOnboarding';
import { CreateCompanyForm } from './CreateCompany';
import { MagicLinkTest } from './MagicLinkTest';
Expand Down Expand Up @@ -136,6 +138,14 @@ const additionalDemos = [
component: InvoiceSchedule,
sourceCode: InvoiceScheduleCode,
},
{
id: 'contract-document',
title: 'Contract Document',
description:
"Standalone flow to create a contractor's contract document (details, then preview)",
component: ContractDocument,
sourceCode: ContractDocumentCode,
},
{
id: 'onboarding',
title: 'Onboarding',
Expand Down
20 changes: 20 additions & 0 deletions example/src/ContractDocument.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ContractDocumentFlow } from '@remoteoss/remote-flows';
import { RemoteFlows } from './RemoteFlows';
import './css/main.css';

export function ContractDocument() {
return (
<RemoteFlows proxy={{ url: window.location.origin }}>
<div style={{ width: 640, padding: 20, margin: '80px auto' }}>
<ContractDocumentFlow
render={() => (
<div className='onboarding-form-container'>
<h2>Create contract document</h2>
<p>To be continued…</p>
</div>
)}
/>
</div>
</RemoteFlows>
);
}
19 changes: 19 additions & 0 deletions src/flows/ContractDocument/ContractDocumentFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useId } from 'react';
import { ContractDocumentContext } from '@/src/flows/ContractDocument/context';
import { useContractDocument } from '@/src/flows/ContractDocument/hooks';
import { ContractDocumentFlowProps } from '@/src/flows/ContractDocument/types';

/**
* Standalone contract-document flow: contract details, then contract preview. Mount it
* anywhere — it does not depend on the contractor onboarding flow.
*/
export const ContractDocumentFlow = ({ render }: ContractDocumentFlowProps) => {
const formId = useId();
const contractDocumentBag = useContractDocument();

return (
<ContractDocumentContext.Provider value={{ formId, contractDocumentBag }}>
{render(contractDocumentBag)}
</ContractDocumentContext.Provider>
);
};
83 changes: 83 additions & 0 deletions src/flows/ContractDocument/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Contract Document Docs

Welcome to the Contract Document flow docs.

A standalone flow for creating a contractor's contract document — the contract details and
contract preview screens of contractor onboarding, mountable on their own. It does not depend
on the onboarding flow.

If you want these screens **inside** contractor onboarding, that is part of the
[Contractor Onboarding](../ContractorOnboarding/README.md) flow instead.

> **Work in progress.** This release ships the two-step shell only: the steps exist and can
> be navigated, but no form is rendered and no request is made yet.

# Table of Contents

- [Getting Started](#getting-started)
- [Full Example](#full-example)
- [Components API](#components-api)
- [ContractDocumentFlow](#contractdocumentflow)
- [The bag](#the-bag)

## Getting Started

After installation, import the main CSS file in your application:

```css
@import '@remoteoss/remote-flows/styles.css';
```

### Full Example

```tsx
import { ContractDocumentFlow, RemoteFlows } from '@remoteoss/remote-flows';

export function CreateContractDocument() {
return (
<RemoteFlows auth={/* your token fetcher */}>
<ContractDocumentFlow
render={(contractDocumentBag) => {
const { stepState, back, next } = contractDocumentBag;

return (
<>
<h2>{stepState.currentStep.name}</h2>
<button type='button' onClick={back}>
Back
</button>
<button type='button' onClick={next}>
Next
</button>
</>
);
}}
/>
</RemoteFlows>
);
}
```

## Components API

### ContractDocumentFlow

| Prop | Type | Required | Description |
| -------- | -------------------- | -------- | ----------------------------------- |
| `render` | `(bag) => ReactNode` | Yes | Render prop receiving the flow bag. |

## The bag

`useContractDocument()` is the headless equivalent of the render prop, for fully custom UIs.
Both surfaces expose the same bag:

| Key | Description |
| -------------- | ----------------------------------------------------------------------- |
| `stepState` | Current step (`contract_details` or `contract_preview`) and step count. |
| `steps` | Every step of the flow, in order, with its label. |
| `next` | Moves to the next step. |
| `back` | Moves to the previous step. |
| `goTo` | Moves to a specific step. |
| `fields` | Form fields for the current step. Empty for now. |
| `isLoading` | True while the flow is loading data. Always `false` for now. |
| `isSubmitting` | True while a submission is in flight. Always `false` for now. |
24 changes: 24 additions & 0 deletions src/flows/ContractDocument/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContext, useContext } from 'react';
import { useContractDocument } from '@/src/flows/ContractDocument/hooks';

export const ContractDocumentContext = createContext<{
formId: string | undefined;
contractDocumentBag: ReturnType<typeof useContractDocument> | null;
}>({
formId: undefined,
contractDocumentBag: null,
});

export const useContractDocumentContext = () => {
const context = useContext(ContractDocumentContext);
if (!context.formId || !context.contractDocumentBag) {
throw new Error(
'useContractDocumentContext must be used within a ContractDocumentFlow',
);
}

return {
formId: context.formId,
contractDocumentBag: context.contractDocumentBag,
} as const;
};
48 changes: 48 additions & 0 deletions src/flows/ContractDocument/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Fields } from '@remoteoss/remote-json-schema-form-kit';
import { ContractDocumentStepKeys } from '@/src/flows/ContractDocument/types';
import { STEPS, STEPS_ARRAY } from '@/src/flows/ContractDocument/utils';
import { useStepState } from '@/src/flows/useStepState';

/**
* Headless hook powering the standalone contract-document flow: the contract details and
* contract preview screens of contractor onboarding, mountable on their own.
*/
export const useContractDocument = () => {
const { stepState, nextStep, previousStep, goToStep } =
useStepState<ContractDocumentStepKeys>(STEPS);

return {
/**
* Current step state containing the current step and total number of steps.
*/
stepState,
/**
* Every step of the flow, in order.
*/
steps: STEPS_ARRAY,
/**
* Moves to the next step.
*/
next: nextStep,
/**
* Moves to the previous step.
*/
back: previousStep,
/**
* Moves to a specific step.
*/
goTo: goToStep,
/**
* Form fields for the current step.
*/
fields: [] as Fields,
/**
* True while the flow is loading data.
*/
isLoading: false,
/**
* True while a submission is in flight.
*/
isSubmitting: false,
};
};
6 changes: 6 additions & 0 deletions src/flows/ContractDocument/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { ContractDocumentFlow } from './ContractDocumentFlow';
export { useContractDocument } from './hooks';
export type {
ContractDocumentFlowProps,
ContractDocumentStepKeys,
} from './types';
28 changes: 28 additions & 0 deletions src/flows/ContractDocument/tests/ContractDocumentFlow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react';
import { queryClient, TestProviders } from '@/src/tests/testHelpers';
import { ContractDocumentFlow } from '@/src/flows/ContractDocument/ContractDocumentFlow';

describe('ContractDocumentFlow', () => {
beforeEach(() => {
queryClient.clear();
});

it('renders the render prop on the contract details step', () => {
render(
<ContractDocumentFlow
render={({ stepState }) => (
<>
<h2>Create contract document</h2>
<p>{stepState.currentStep.name}</p>
</>
)}
/>,
{ wrapper: TestProviders },
);

expect(
Comment thread
jordividaller marked this conversation as resolved.
Resolved
screen.getByRole('heading', { name: 'Create contract document' }),
).toBeInTheDocument();
expect(screen.getByText('contract_details')).toBeInTheDocument();
});
});
43 changes: 43 additions & 0 deletions src/flows/ContractDocument/tests/useContractDocument.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { act, renderHook } from '@testing-library/react';
import { queryClient, TestProviders } from '@/src/tests/testHelpers';
import { useContractDocument } from '@/src/flows/ContractDocument/hooks';

describe('useContractDocument', () => {
beforeEach(() => {
queryClient.clear();
});

it('exposes the two-step shell', () => {
const { result } = renderHook(() => useContractDocument(), {
wrapper: TestProviders,
});

expect(result.current.stepState.currentStep.name).toBe('contract_details');
expect(result.current.stepState.totalSteps).toBe(2);
expect(result.current.steps).toEqual([
{ index: 0, name: 'contract_details', label: 'Contract Details' },
{ index: 1, name: 'contract_preview', label: 'Contract Preview' },
]);
expect(result.current.fields).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.isSubmitting).toBe(false);
});

it('moves between steps with next, back and goTo', () => {
const { result } = renderHook(() => useContractDocument(), {
wrapper: TestProviders,
});

act(() => result.current.next());
expect(result.current.stepState.currentStep.name).toBe('contract_preview');

act(() => result.current.next());
expect(result.current.stepState.currentStep.name).toBe('contract_preview');

act(() => result.current.back());
expect(result.current.stepState.currentStep.name).toBe('contract_details');

act(() => result.current.goTo('contract_preview'));
expect(result.current.stepState.currentStep.name).toBe('contract_preview');
});
});
10 changes: 10 additions & 0 deletions src/flows/ContractDocument/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { useContractDocument } from '@/src/flows/ContractDocument/hooks';

export type ContractDocumentStepKeys = 'contract_details' | 'contract_preview';

export type ContractDocumentFlowProps = {
render: (
contractDocumentBag: ReturnType<typeof useContractDocument>,
) => React.ReactNode;
};
20 changes: 20 additions & 0 deletions src/flows/ContractDocument/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Step } from '@/src/flows/useStepState';
import { ContractDocumentStepKeys } from '@/src/flows/ContractDocument/types';

export const STEPS: Record<
ContractDocumentStepKeys,
Step<ContractDocumentStepKeys>
> = {
contract_details: { index: 0, name: 'contract_details' },
contract_preview: { index: 1, name: 'contract_preview' },
};

const STEP_LABELS: Record<ContractDocumentStepKeys, string> = {
contract_details: 'Contract Details',
contract_preview: 'Contract Preview',
};

export const STEPS_ARRAY = Object.values(STEPS).map((step) => ({
...step,
label: STEP_LABELS[step.name],
}));
10 changes: 10 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export type {
UseInvoiceScheduleOptions,
} from '@/src/flows/InvoiceSchedule';

export {
ContractDocumentFlow,
useContractDocument,
} from '@/src/flows/ContractDocument';

export type {
ContractDocumentFlowProps,
ContractDocumentStepKeys,
} from '@/src/flows/ContractDocument';

export {
ContractAmendmentConfirmationForm,
ContractAmendmentFlow,
Expand Down
Loading