|
/** |
|
* #### Tests |
|
* - **[[createWorkflow]]** |
|
* - convert rules definition into workflow object |
|
* - disallow `isComplete` as step (reserved) |
|
* - disallow `activeStep` as step (reserved) |
|
* - **[[useWorkflow]]** |
|
* - calculate and return workflow status |
|
*/ |
|
|
|
/** |
|
* Workflow steps definition. |
|
*/ |
|
interface WorkflowRule { |
|
/** |
|
* Allows creation of ordered workflow. |
|
*/ |
|
order?: number; |
|
/** |
|
* Each rule has own hook; should depend on other hooks to drive workflow. |
|
* As other hooks update, workflow will update its status. |
|
* |
|
* #### Example: |
|
* ```ts |
|
* useRule: () => { |
|
* const user = useAuth(); |
|
* const permissions = usePermissions(user); |
|
* |
|
* return user.isAuthenticated && permissions.isAdmin; |
|
* } |
|
* ``` |
|
*/ |
|
useRule: () => boolean; |
|
} |
|
|
|
/** |
|
* Contains and maintains rules and order of execution; |
|
* React hooks **must** always be called in consistent order. |
|
* |
|
* #### Example: |
|
* ```ts |
|
* { |
|
* process: [], |
|
* rules: { |
|
* isStepOneComplete: { |
|
* order: 0, |
|
* useRule: () => { |
|
* // ... |
|
* } |
|
* } |
|
* } |
|
* } |
|
* ``` |
|
* |
|
* @typeparam Steps Definition of un/ordered workflow rules. |
|
* - **key**: string. |
|
* - **value**: [[WorkflowRule]]. |
|
*/ |
|
interface Workflow<Steps extends object = object> { |
|
/** |
|
* Sorted (by order) step names. |
|
*/ |
|
process: (keyof Steps)[]; |
|
/** |
|
* See `Steps` definition. |
|
*/ |
|
rules: { |
|
[Key in keyof Steps]: WorkflowRule; |
|
}; |
|
} |
|
|
|
/** |
|
* Key/Value pair where keys are names of workflow steps, values are boolean. |
|
* Also contains `isComplete` to indicate overall workflow completion. |
|
*/ |
|
type WorkflowStatus<Steps extends object = object> = { activeStep: number; isComplete: boolean } & { |
|
[Key in keyof Steps]: boolean; |
|
}; |
|
|
|
/** |
|
* Creates a workflow to be used in [[useWorkflow]]. |
|
* |
|
* #### Example: |
|
* ```ts |
|
* const exampleWorkflow = createWorkflow({ |
|
* isItemDesignerAssigned: { |
|
* order: 0, |
|
* useRule: () => { |
|
* |
|
* } |
|
* }, |
|
* isMaterialChosen: { |
|
* order: 1, |
|
* useRule: () => { |
|
* |
|
* } |
|
* } |
|
* }); |
|
* ``` |
|
*/ |
|
export function createWorkflow<Steps extends Workflow<Steps>['rules']>(rules: Steps): Workflow<Steps> { |
|
/** |
|
* `isComplete` rule reserved to check overall workflow completion. |
|
*/ |
|
if ('isComplete' in rules) { |
|
throw new Error('`isComplete` is a reserved rule'); |
|
} |
|
|
|
/** |
|
* `activeStep` rule reserved to check the current step in ordered workflows. |
|
*/ |
|
if ('activeStep' in rules) { |
|
throw new Error('`activeStep` is a reserved rule'); |
|
} |
|
|
|
/** |
|
* Sorted by `rule.order`, mapped to step's name (ex: `isSomethingComplete`). |
|
* Rules with an order will always appear before unordered steps. |
|
*/ |
|
const process = (Object.entries(rules) as [string, WorkflowRule][]) |
|
.sort(([, ruleA], [, ruleB]) => { |
|
if (ruleA.order === undefined && ruleB.order === undefined) { |
|
return 0; |
|
} |
|
if (ruleA.order === undefined) { |
|
return 1; |
|
} |
|
if (ruleB.order === undefined) { |
|
return -1; |
|
} |
|
if (ruleA.order < ruleB.order) { |
|
return -1; |
|
} |
|
if (ruleA.order > ruleB.order) { |
|
return 1; |
|
} |
|
return 0; |
|
}) |
|
.map(([rule]) => rule) as (keyof Steps)[]; |
|
|
|
return Object.seal({ |
|
process, |
|
rules, |
|
}); |
|
} |
|
|
|
/** |
|
* #### Custom React Hook. |
|
* Used to organize code into groups of related hooks; |
|
* have your component depend on a single workflow instead of its potentially many dependent hooks. |
|
* |
|
* #### Example |
|
* ```ts |
|
* function ExampleComponent() { |
|
* const workflow = useWorkflow(exampleWorkflow); |
|
* |
|
* if (workflow.isItemDesignerAssigned) { |
|
* return React.createElement('h1', undefined, 'Item designer is assigned'); |
|
* } |
|
* |
|
* if (workflow.isMaterialChosen) { |
|
* return React.createElement('h1', undefined, 'Material is chosen'); |
|
* } |
|
* } |
|
* ``` |
|
*/ |
|
export function useWorkflow<Steps extends Workflow<Steps>['rules']>(workflow: Workflow<Steps>): WorkflowStatus<Steps> { |
|
/** |
|
* Reduce sorted rule names into WorkflowStatus<Steps>. |
|
* During reduction, all hooks used. |
|
*/ |
|
return workflow.process.reduce( |
|
(status, step, index) => { |
|
const rule = workflow.rules[step]; |
|
const isRuleComplete = rule.useRule(); |
|
|
|
/** |
|
* If previous and current step marked complete, |
|
* set `status.aciveStep` to current index. |
|
* After last complete step, update will not occur. |
|
*/ |
|
if (status[workflow.process[index - 1]] && isRuleComplete) { |
|
status.activeStep = index; |
|
} |
|
|
|
return Object.assign(status, { |
|
isComplete: status.isComplete && isRuleComplete, |
|
[step]: isRuleComplete, |
|
}); |
|
}, |
|
{ activeStep: 0, isComplete: true } as WorkflowStatus<Steps> |
|
); |
|
} |