UNPKG

@excentone/spfx-react

Version:

Contains custom ReactJs components and hooks intended to use when developing SharePoint Framework (SPFx) Web components.

312 lines (310 loc) 11.5 kB
import { useCounter } from "../useCounter"; import { assertNever } from "@fluentui/react"; import { isEqual } from "@microsoft/sp-lodash-subset"; import { isFunction } from "@excentone/spfx-utilities"; import { useEffect, useMemo, useReducer, useState } from "react"; const initialIndex = -1; const reducer = (prev, action) => { const { type, index, jobs, setJob, status } = action; const next = [...prev]; switch (type) { case 'PREPEND': next.splice(0, 0, ...jobs); return next; case 'APPEND': next.push(...jobs); return next; case 'INSERT': next.splice(index, 0, ...jobs); return next; case 'UPDATE': if (0 <= index && index < next.length) { const { handler, skip, ...details } = next[index]; const nextJob = isFunction(setJob) ? setJob(details) : setJob; if (!isEqual(details, nextJob)) { next.splice(index, 1, { handler, skip, ...nextJob }); return next; } } return prev; case 'SET_STATUS': return prev.map(job => { job.status = status || 'Idle'; return job; }); case 'SET': return jobs ? jobs.map(job => { job.status = 'Idle'; return job; }) : []; case 'CLEAR': return []; default: assertNever(type); } }; export const usePipeline = (options, deps = []) => { const { name, onJobStart, onJobSuccess, onJobFailed, onJobFinished, onPipelineStart, onPipelineFailed, onPipelineSuccess, onPipelineFinished, jobExecutionDelay, getErrorMessage, jobs: jobList, values: initialValues, } = useMemo(() => ({ ...{ jobs: [], values: {}, jobExecutionDelay: 500 }, ...options }), [options, ...(deps || [])]); const [status, setStatus] = useState('Idle'); const [events, setEvents] = useState(null); const [jobs, dispatch] = useReducer(reducer, []); const [result, setResult] = useState({ values: null, result: 'Unknown', errors: [] }); const [currentIndex, { set, reset, increment }] = useCounter({ defaultValue: initialIndex, initialValue: initialIndex }); useEffect(() => { if (status === 'Executing' && result.values && jobs.length && initialIndex < currentIndex && currentIndex < jobs.length) { const job = jobs[currentIndex]; const { handler, skip, ...details } = job; let jobDetails = { ...details, status: 'Executing', values: result.values }; const updateResult = () => setResult(prev => { let next = { ...prev, values: jobDetails.values }; if (currentIndex === jobs.length - 1) { next = { ...next, result: next.errors.length ? next.errors.every(e => e.jobDetails.ignoreOnError) ? 'Success with errors' : 'Failed' : 'Success' }; } else if (jobDetails.hasError && !jobDetails.ignoreOnError) { next = { ...next, result: 'Failed' }; } return next; }); jobDetails.skipped = isFunction(skip) ? skip(result.values, jobDetails) : skip; if (jobDetails.skipped) { dispatch({ type: 'UPDATE', index: currentIndex, setJob: prev => ({ ...prev, ...jobDetails, title: `${prev.title} - Skipped`, status: 'Completed', values: result.values }) }); updateResult(); return; } if (onJobStart) onJobStart(jobDetails, currentIndex); if (events && events.onJobStart) events.onJobStart(jobDetails, currentIndex); handler(result.values, currentIndex, details, setJob => dispatch({ type: 'UPDATE', setJob, index: currentIndex })) .then(values => { jobDetails.values = values; if (onJobSuccess) onJobSuccess(jobDetails, currentIndex); if (events && events.onJobSuccess) events.onJobSuccess(jobDetails, currentIndex); }) .catch(async (error) => { jobDetails = { ...jobDetails, hasError: true, errorMessage: await getErrorMessage(error), values: result.values }; const pipelineError = { values: result.values, error, jobDetails, currentIndex }; if (onJobFailed) onJobFailed(pipelineError); if (events && events.onJobFailed) events.onJobFailed(pipelineError); setResult(prev => { const next = { ...prev }; next.errors.push(pipelineError); return next; }); }) .finally(() => { if (onJobFinished) onJobFinished(jobDetails, currentIndex); if (events && events.onJobFinished) events.onJobFinished(jobDetails, currentIndex); dispatch({ type: 'UPDATE', index: currentIndex, setJob: prev => ({ ...prev, ...jobDetails, status: 'Completed', }) }); updateResult(); }); } }, [currentIndex]); useEffect(() => { if (status === 'Executing' && result.values && jobs.length && initialIndex < currentIndex && currentIndex < jobs.length) { const job = jobs[currentIndex]; if (job.status === 'Completed' && job.values === result.values) { const incrementOrSetMax = () => { if (job.hasError && !job.ignoreOnError) set(jobs.length); else increment(); }; if (jobExecutionDelay) setTimeout(incrementOrSetMax, jobExecutionDelay); else incrementOrSetMax(); } } }, [jobs[currentIndex], result.values]); useEffect(() => { switch (status) { case 'Idle': break; case 'Pending': if (result.result === 'Unknown' && result.values) { const initialState = { currentIndex: currentIndex + 1, jobs: jobs, status, name, }; if (onPipelineStart) onPipelineStart(result.values, initialState); if (events && events.onPipelineStart) events.onPipelineStart(result.values, initialState); setStatus('Executing'); increment(); } break; case 'Executing': if (!jobs.length) { setStatus('Idle'); break; } if (currentIndex < jobs.length || result.result === 'Unknown') break; if (result.result === 'Failed') { if (result.errors.length) { if (onPipelineFailed) onPipelineFailed(result.errors[0]); if (events && events.onPipelineFailed) events.onPipelineFailed(result.errors[0]); } } else { if (onPipelineSuccess) onPipelineSuccess(result.values); if (events && events.onPipelineSuccess) events.onPipelineSuccess(result.values); } setStatus('Completed'); break; case 'Completed': { const completedState = { currentIndex: Math.min(currentIndex, jobs.length - 1), jobs: jobs, status, name, }; if (onPipelineFinished) onPipelineFinished(result, completedState); if (events && events.onPipelineFinished) events.onPipelineFinished(result, completedState); setStatus('Idle'); const resetStates = () => { setEvents(null); dispatch({ type: 'SET', jobs: jobList }); setResult(prev => ({ ...prev, errors: [], result: 'Unknown', values: null })); reset(); }; setTimeout(resetStates, 2000); } break; default: assertNever(status); } }, [status, result.result, currentIndex]); useEffect(() => { if (!isEqual(jobs, jobList)) dispatch({ type: 'SET', jobs: jobList }); }, [...(deps || [])]); const operations = useMemo(() => { const execute = (values, callbacks) => { if (status === 'Idle') { if (callbacks) setEvents(callbacks); dispatch({ type: 'SET_STATUS', status: 'Pending' }); setResult(prev => ({ ...prev, values })); setStatus('Pending'); } }; const executeAsync = (values) => (new Promise((resolve, reject) => execute(values, { onPipelineSuccess: () => resolve(), onPipelineFailed: reject }))); return ({ run: execute, runAsync: executeAsync, start: () => execute(initialValues), startAsync: () => executeAsync(initialValues) }); }, [initialValues, status]); return { name, jobs, status, currentIndex, ...operations }; }; //# sourceMappingURL=usePipeline.js.map