Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
@@ -1,23 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ComparisonOperator,
|
||||
Employee,
|
||||
HeadType,
|
||||
ProjectRole,
|
||||
RequestType,
|
||||
RouteStepStatus,
|
||||
StepExecutorKind,
|
||||
TemplateRoute,
|
||||
TemplateRouteStep
|
||||
} from '../../models';
|
||||
import { headTypeLabels, projectRoleLabels, routeStepStatusLabels, texts } from '../../texts';
|
||||
import { comparisonOperatorLabels, headTypeLabels, projectRoleLabels, routeStepStatusLabels, texts } from '../../texts';
|
||||
import { detectExecutorKind } from '../../utils/stepExecutor';
|
||||
import { toEmployeeOption } from '../../utils/employee';
|
||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||
import { useSharePointFields } from '../../hooks/useSharePointFields';
|
||||
import { ApprovalRouteApi } from '../../services/api';
|
||||
import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists';
|
||||
import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui';
|
||||
|
||||
export interface IRouteStepFormModalProps {
|
||||
api: ApprovalRouteApi;
|
||||
route: TemplateRoute;
|
||||
/** Undefined, пока грузится тип заявки маршрута. */
|
||||
requestType?: RequestType;
|
||||
sharePoint: SharePointListsClient;
|
||||
/** Undefined — создание нового шага, иначе редактирование существующего. */
|
||||
editing?: TemplateRouteStep;
|
||||
nextOrderNo: number;
|
||||
@@ -36,6 +43,10 @@ interface IFormState {
|
||||
projectRole: ProjectRole;
|
||||
headType: HeadType;
|
||||
allowReassignToOtherApprovers: boolean;
|
||||
hasCondition: boolean;
|
||||
fieldName: string;
|
||||
operation: ComparisonOperator;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution]
|
||||
@@ -49,6 +60,10 @@ const HEAD_TYPE_OPTIONS = Object.values(HeadType)
|
||||
.filter(v => v !== HeadType.None)
|
||||
.map(v => ({ value: v, label: headTypeLabels[v] }));
|
||||
|
||||
const OPERATION_OPTIONS = Object.values(ComparisonOperator)
|
||||
.filter(v => v !== ComparisonOperator.None)
|
||||
.map(v => ({ value: v, label: comparisonOperatorLabels[v] }));
|
||||
|
||||
function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number): IFormState {
|
||||
if (!editing) {
|
||||
return {
|
||||
@@ -59,7 +74,11 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
|
||||
position: '',
|
||||
projectRole: ProjectRole.ProjectManager,
|
||||
headType: HeadType.Chief,
|
||||
allowReassignToOtherApprovers: false
|
||||
allowReassignToOtherApprovers: false,
|
||||
hasCondition: false,
|
||||
fieldName: '',
|
||||
operation: ComparisonOperator.GreaterThan,
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -70,7 +89,11 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
|
||||
position: editing.position ?? '',
|
||||
projectRole: editing.projectRole && editing.projectRole !== ProjectRole.None ? editing.projectRole : ProjectRole.ProjectManager,
|
||||
headType: editing.headType && editing.headType !== HeadType.None ? editing.headType : HeadType.Chief,
|
||||
allowReassignToOtherApprovers: editing.allowReassignToOtherApprovers
|
||||
allowReassignToOtherApprovers: editing.allowReassignToOtherApprovers,
|
||||
hasCondition: Boolean(editing.fieldName),
|
||||
fieldName: editing.fieldName ?? '',
|
||||
operation: editing.operation && editing.operation !== ComparisonOperator.None ? editing.operation : ComparisonOperator.GreaterThan,
|
||||
value: editing.value ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,6 +101,8 @@ function toFormState(editing: TemplateRouteStep | undefined, nextOrderNo: number
|
||||
export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
api,
|
||||
route,
|
||||
requestType,
|
||||
sharePoint,
|
||||
editing,
|
||||
nextOrderNo,
|
||||
employees,
|
||||
@@ -87,6 +112,35 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
}) => {
|
||||
const [form, setForm] = React.useState<IFormState>(() => toFormState(editing, nextOrderNo));
|
||||
const submit = useAsyncSubmit();
|
||||
const hasListName = Boolean(requestType?.listName);
|
||||
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listName);
|
||||
|
||||
const fieldNameOptions = React.useMemo(() => {
|
||||
const options = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
||||
if (form.fieldName && !options.some(o => o.value === form.fieldName)) {
|
||||
options.unshift({ value: form.fieldName, label: form.fieldName });
|
||||
}
|
||||
return options;
|
||||
}, [sharePointFields.items, form.fieldName]);
|
||||
|
||||
// Тип столбца известен, только если он есть среди подгруженных полей списка —
|
||||
// для значения, введённого до появления поля в списке (или после его удаления),
|
||||
// ограничения ниже не действуют, чтобы не блокировать уже сохранённые данные.
|
||||
const selectedField = sharePointFields.items.find(f => f.internalName === form.fieldName);
|
||||
const isNumericField = selectedField ? isNumericFieldType(selectedField.typeAsString) : true;
|
||||
|
||||
const operationOptions = isNumericField
|
||||
? OPERATION_OPTIONS
|
||||
: OPERATION_OPTIONS.filter(o => o.value === ComparisonOperator.Equal);
|
||||
|
||||
// Строковая колонка допускает только «равно» — если пользователь сменил
|
||||
// field_name на нечисловую колонку, а раньше был выбран «больше»/«меньше», сбрасываем.
|
||||
React.useEffect(() => {
|
||||
if (!isNumericField && form.operation !== ComparisonOperator.Equal) {
|
||||
setForm(f => ({ ...f, operation: ComparisonOperator.Equal }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNumericField]);
|
||||
|
||||
const submitForm = (): void => {
|
||||
const orderNo = Number(form.orderNo);
|
||||
@@ -102,6 +156,14 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
submit.fail(texts.routeSteps.validationPosition);
|
||||
return;
|
||||
}
|
||||
if (form.hasCondition && (!form.fieldName.trim() || !form.value.trim())) {
|
||||
submit.fail(texts.routeSteps.validationCondition);
|
||||
return;
|
||||
}
|
||||
if (form.hasCondition && isNumericField && !Number.isFinite(Number(form.value.trim()))) {
|
||||
submit.fail(texts.routeSteps.validationConditionNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
orderNo,
|
||||
@@ -110,7 +172,10 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
executorEmail: form.kind === 'email' ? form.executorEmail.trim() : null,
|
||||
position: form.kind === 'position' ? form.position.trim() : null,
|
||||
projectRole: form.kind === 'projectRole' ? form.projectRole : null,
|
||||
headType: form.kind === 'headType' ? form.headType : null
|
||||
headType: form.kind === 'headType' ? form.headType : null,
|
||||
fieldName: form.hasCondition ? form.fieldName.trim() : null,
|
||||
operation: form.hasCondition ? form.operation : null,
|
||||
value: form.hasCondition ? form.value.trim() : null
|
||||
};
|
||||
|
||||
const request = editing
|
||||
@@ -217,6 +282,55 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
||||
onChange={v => setForm({ ...form, allowReassignToOtherApprovers: v })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={texts.routeSteps.conditionLabel} full hint={!hasListName ? texts.routeSteps.conditionDisabledHint : undefined}>
|
||||
<Checkbox
|
||||
label={texts.routeSteps.conditionCheckbox}
|
||||
checked={form.hasCondition}
|
||||
disabled={!hasListName}
|
||||
onChange={v => setForm({ ...form, hasCondition: v })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{form.hasCondition && hasListName && (
|
||||
<>
|
||||
<Field
|
||||
label={texts.routeSteps.conditionFieldNameLabel}
|
||||
required
|
||||
full
|
||||
hint={sharePointFields.error ? texts.routeSteps.conditionFieldNameError(sharePointFields.error) : undefined}
|
||||
>
|
||||
<Select
|
||||
options={fieldNameOptions}
|
||||
value={form.fieldName}
|
||||
disabled={sharePointFields.isLoading}
|
||||
placeholder={
|
||||
sharePointFields.isLoading
|
||||
? texts.routeSteps.conditionFieldNameLoading
|
||||
: fieldNameOptions.length === 0
|
||||
? texts.routeSteps.conditionFieldNameEmpty
|
||||
: texts.routeSteps.conditionFieldNamePlaceholder
|
||||
}
|
||||
onChange={e => setForm({ ...form, fieldName: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.routeSteps.conditionOperationLabel} required>
|
||||
<Select
|
||||
options={operationOptions}
|
||||
value={form.operation}
|
||||
onChange={e => setForm({ ...form, operation: e.target.value as ComparisonOperator })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.routeSteps.conditionValueLabel} required>
|
||||
<TextInput
|
||||
type={isNumericField ? 'number' : 'text'}
|
||||
value={form.value}
|
||||
onChange={e => setForm({ ...form, value: e.target.value })}
|
||||
placeholder={texts.routeSteps.conditionValuePlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</FormGrid>
|
||||
</FormModal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user