Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { ApiClientError } from '../services/httpClient';
|
||||
|
||||
export interface AsyncList<T> {
|
||||
items: T[];
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка списка с бэкенда: держит данные, признак загрузки и текст ошибки
|
||||
* в одном месте, чтобы экраны не повторяли этот код.
|
||||
*
|
||||
* load обязан быть стабильным (React.useCallback) — иначе загрузка пойдёт по кругу.
|
||||
*/
|
||||
export function useAsyncList<T>(load: () => Promise<T[]>): AsyncList<T> {
|
||||
const [items, setItems] = React.useState<T[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | undefined>();
|
||||
|
||||
const reload = React.useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
load()
|
||||
.then(setItems)
|
||||
.catch((e: ApiClientError) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [load]);
|
||||
|
||||
React.useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
return { items, isLoading, error, reload };
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { ApiClientError } from '../services/httpClient';
|
||||
|
||||
export interface AsyncSubmit {
|
||||
isBusy: boolean;
|
||||
error?: string;
|
||||
/** Показать ошибку без запроса — например, результат валидации формы. */
|
||||
fail: (message: string) => void;
|
||||
run: (action: () => Promise<void>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояние вокруг одного пишущего запроса: «выполняется» + текст ошибки.
|
||||
* Используется формами и подтверждением удаления, чтобы не дублировать
|
||||
* setBusy/setError/finally в каждом экране.
|
||||
*/
|
||||
export function useAsyncSubmit(): AsyncSubmit {
|
||||
const [isBusy, setIsBusy] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | undefined>();
|
||||
|
||||
const run = (action: () => Promise<void>): void => {
|
||||
setIsBusy(true);
|
||||
setError(undefined);
|
||||
action()
|
||||
.catch((e: ApiClientError) => setError(e.message))
|
||||
.finally(() => setIsBusy(false));
|
||||
};
|
||||
|
||||
return { isBusy, error, fail: setError, run };
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { AsyncSubmit, useAsyncSubmit } from './useAsyncSubmit';
|
||||
|
||||
export interface DeleteTarget<T> {
|
||||
/** Запись, для которой открыт диалог подтверждения. */
|
||||
target?: T;
|
||||
isBusy: boolean;
|
||||
error?: string;
|
||||
ask: (item: T) => void;
|
||||
cancel: () => void;
|
||||
confirm: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сценарий «спросили — удалили — обновили список», одинаковый на всех экранах:
|
||||
* какая запись удаляется, идёт ли запрос и что показать при ошибке.
|
||||
*/
|
||||
export function useDeleteTarget<T>(remove: (item: T) => Promise<void>, onDeleted: () => void): DeleteTarget<T> {
|
||||
const [target, setTarget] = React.useState<T | undefined>();
|
||||
const submit: AsyncSubmit = useAsyncSubmit();
|
||||
|
||||
const confirm = (): void => {
|
||||
if (!target) return;
|
||||
submit.run(() =>
|
||||
remove(target).then(() => {
|
||||
setTarget(undefined);
|
||||
onDeleted();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
target,
|
||||
isBusy: submit.isBusy,
|
||||
error: submit.error,
|
||||
ask: setTarget,
|
||||
cancel: () => setTarget(undefined),
|
||||
confirm
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { Company, Department } from '../models';
|
||||
import { ApiClientError } from '../services/httpClient';
|
||||
import { PortalDictionaryClient } from '../services/portalDictionary';
|
||||
|
||||
export interface PortalDictionaries {
|
||||
companies: Company[];
|
||||
departments: Department[];
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Компании и отделы из 1С грузим один раз на экран: они статичны, а отделов ~700 —
|
||||
* искать по ним дешевле в памяти, чем ходить на сервер на каждый символ.
|
||||
*/
|
||||
export function usePortalDictionaries(dictionary?: PortalDictionaryClient): PortalDictionaries {
|
||||
const [companies, setCompanies] = React.useState<Company[]>([]);
|
||||
const [departments, setDepartments] = React.useState<Department[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Клиент появляется асинхронно: SPFx сначала выдаёт AAD-токен для api.tnsu.kz.
|
||||
if (!dictionary) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
Promise.all([dictionary.companies(), dictionary.departments()])
|
||||
.then(([loadedCompanies, loadedDepartments]) => {
|
||||
setCompanies(loadedCompanies);
|
||||
setDepartments(loadedDepartments);
|
||||
})
|
||||
.catch((e: ApiClientError) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [dictionary]);
|
||||
|
||||
return { companies, departments, isLoading, error };
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { Employee } from '../models';
|
||||
import { ApiClientError } from '../services/httpClient';
|
||||
import { PortalDictionaryClient } from '../services/portalDictionary';
|
||||
|
||||
export interface PortalEmployees {
|
||||
items: Employee[];
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Справочник сотрудников api.tnsu.kz — грузим один раз на экран, чтобы искать
|
||||
* исполнителя/инициатора по ФИО или email на клиенте, без запроса на каждый символ.
|
||||
*/
|
||||
export function usePortalEmployees(dictionary?: PortalDictionaryClient): PortalEmployees {
|
||||
const [items, setItems] = React.useState<Employee[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Клиент появляется асинхронно: SPFx сначала выдаёт AAD-токен для api.tnsu.kz.
|
||||
if (!dictionary) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
dictionary.employees()
|
||||
.then(setItems)
|
||||
.catch((e: ApiClientError) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [dictionary]);
|
||||
|
||||
return { items, isLoading, error };
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { RequestType } from '../models';
|
||||
import { ApprovalRouteApi } from '../services/api';
|
||||
import { AsyncList, useAsyncList } from './useAsyncList';
|
||||
|
||||
/** Справочник типов заявок — нужен и своему экрану, и селектам на других. */
|
||||
export function useRequestTypes(api: ApprovalRouteApi): AsyncList<RequestType> {
|
||||
const load = React.useCallback(() => api.requestTypes.list(), [api]);
|
||||
return useAsyncList(load);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { ResolveTemplateRouteQuery, ResolveTemplateRouteResult } from '../models';
|
||||
import { ApprovalRouteApi } from '../services/api';
|
||||
import { useAsyncSubmit } from './useAsyncSubmit';
|
||||
|
||||
export interface ResolveRoute {
|
||||
result?: ResolveTemplateRouteResult;
|
||||
isBusy: boolean;
|
||||
error?: string;
|
||||
resolve: (query: ResolveTemplateRouteQuery) => void;
|
||||
fail: (message: string) => void;
|
||||
}
|
||||
|
||||
/** Диагностический запрос «как разрешится маршрут» и его состояние. */
|
||||
export function useResolveRoute(api: ApprovalRouteApi): ResolveRoute {
|
||||
const [result, setResult] = React.useState<ResolveTemplateRouteResult | undefined>();
|
||||
const submit = useAsyncSubmit();
|
||||
|
||||
const resolve = (query: ResolveTemplateRouteQuery): void => {
|
||||
// Прошлый результат убираем сразу: показывать его рядом с новыми
|
||||
// параметрами формы — вводить пользователя в заблуждение.
|
||||
setResult(undefined);
|
||||
submit.run(() => api.templateRoutes.resolve(query).then(setResult));
|
||||
};
|
||||
|
||||
return { result, isBusy: submit.isBusy, error: submit.error, resolve, fail: submit.fail };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { TemplateRouteStep } from '../models';
|
||||
import { ApprovalRouteApi } from '../services/api';
|
||||
import { AsyncList, useAsyncList } from './useAsyncList';
|
||||
|
||||
function byOrderNo(steps: TemplateRouteStep[]): TemplateRouteStep[] {
|
||||
return [...steps].sort((a, b) => a.orderNo - b.orderNo);
|
||||
}
|
||||
|
||||
/** Шаги шаблона маршрута, всегда в порядке выполнения. */
|
||||
export function useRouteSteps(api: ApprovalRouteApi, routeId: number): AsyncList<TemplateRouteStep> {
|
||||
const load = React.useCallback(
|
||||
() => api.templateRouteSteps.list(routeId).then(byOrderNo),
|
||||
[api, routeId]
|
||||
);
|
||||
return useAsyncList(load);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { TemplateRoute } from '../models';
|
||||
import { ApprovalRouteApi } from '../services/api';
|
||||
import { AsyncList, useAsyncList } from './useAsyncList';
|
||||
|
||||
/** Список маршрутов, при необходимости отфильтрованный по типу заявки. */
|
||||
export function useTemplateRoutes(api: ApprovalRouteApi, requestTypeId?: number): AsyncList<TemplateRoute> {
|
||||
const load = React.useCallback(() => api.templateRoutes.list(requestTypeId), [api, requestTypeId]);
|
||||
return useAsyncList(load);
|
||||
}
|
||||
Reference in New Issue
Block a user