всякие фиксы
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ApprovalRouteApi } from '../../services/api';
|
import { ApprovalRouteApi } from '../../services/api';
|
||||||
import { SharePointListsClient } from '../../services/sharePointLists';
|
import { SharePointListsClient, SharePointSiteSuggestion } from '../../services/sharePointLists';
|
||||||
import { RequestType } from '../../models';
|
import { RequestType } from '../../models';
|
||||||
import { texts } from '../../texts';
|
import { texts } from '../../texts';
|
||||||
import { useRequestTypes } from '../../hooks/useRequestTypes';
|
import { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||||
@@ -10,6 +10,7 @@ import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
|||||||
import {
|
import {
|
||||||
ActionsCell,
|
ActionsCell,
|
||||||
Alert,
|
Alert,
|
||||||
|
AsyncSearchableSelect,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
@@ -34,10 +35,12 @@ interface IFormState {
|
|||||||
pageUrl: string;
|
pageUrl: string;
|
||||||
section: string;
|
section: string;
|
||||||
siteUrl: string;
|
siteUrl: string;
|
||||||
|
/** Заголовок сайта — только для отображения в триггере поиска, на бэкенд не уходит. */
|
||||||
|
siteTitle: string;
|
||||||
listName: string;
|
listName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listName: '' };
|
const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', siteTitle: '', listName: '' };
|
||||||
|
|
||||||
export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sharePoint }) => {
|
export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sharePoint }) => {
|
||||||
const list = useRequestTypes(api);
|
const list = useRequestTypes(api);
|
||||||
@@ -47,6 +50,10 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
|||||||
const [form, setForm] = React.useState<IFormState>(EMPTY_FORM);
|
const [form, setForm] = React.useState<IFormState>(EMPTY_FORM);
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
const sharePointLists = useSharePointLists(sharePoint, form.siteUrl);
|
const sharePointLists = useSharePointLists(sharePoint, form.siteUrl);
|
||||||
|
const searchSites = React.useCallback(
|
||||||
|
(query: string, page: number) => sharePoint.searchSites(query, page),
|
||||||
|
[sharePoint]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteTarget = useDeleteTarget<RequestType>(item => api.requestTypes.remove(item.id), list.reload);
|
const deleteTarget = useDeleteTarget<RequestType>(item => api.requestTypes.remove(item.id), list.reload);
|
||||||
|
|
||||||
@@ -63,6 +70,7 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
|||||||
pageUrl: item.pageUrl,
|
pageUrl: item.pageUrl,
|
||||||
section: item.section,
|
section: item.section,
|
||||||
siteUrl: item.siteUrl ?? '',
|
siteUrl: item.siteUrl ?? '',
|
||||||
|
siteTitle: item.siteUrl ?? '',
|
||||||
listName: item.listName ?? ''
|
listName: item.listName ?? ''
|
||||||
});
|
});
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
@@ -117,6 +125,8 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
|||||||
{ key: 'name', header: texts.requestTypes.columnName, render: r => <PrimaryCell>{r.name}</PrimaryCell> },
|
{ key: 'name', header: texts.requestTypes.columnName, render: r => <PrimaryCell>{r.name}</PrimaryCell> },
|
||||||
{ key: 'url', header: texts.requestTypes.columnPageUrl, render: r => r.pageUrl },
|
{ key: 'url', header: texts.requestTypes.columnPageUrl, render: r => r.pageUrl },
|
||||||
{ key: 'section', header: texts.requestTypes.columnSection, render: r => r.section },
|
{ key: 'section', header: texts.requestTypes.columnSection, render: r => r.section },
|
||||||
|
{ key: 'listName', header: texts.requestTypes.columnListName, render: r => r.listName ?? texts.common.empty },
|
||||||
|
{ key: 'siteUrl', header: texts.requestTypes.columnSiteUrl, render: r => r.siteUrl ?? texts.common.empty },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
@@ -167,10 +177,18 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={texts.requestTypes.siteUrlLabel} full hint={texts.requestTypes.siteUrlHint}>
|
<Field label={texts.requestTypes.siteUrlLabel} full hint={texts.requestTypes.siteUrlHint}>
|
||||||
<TextInput
|
<AsyncSearchableSelect<SharePointSiteSuggestion>
|
||||||
value={form.siteUrl}
|
value={form.siteUrl}
|
||||||
onChange={e => setForm({ ...form, siteUrl: e.target.value, listName: '' })}
|
selectedLabel={form.siteTitle || form.siteUrl}
|
||||||
|
allowClear
|
||||||
|
clearLabel={texts.select.notSelected}
|
||||||
placeholder={texts.requestTypes.siteUrlPlaceholder}
|
placeholder={texts.requestTypes.siteUrlPlaceholder}
|
||||||
|
searchPlaceholder={texts.requestTypes.siteUrlSearchPlaceholder}
|
||||||
|
search={searchSites}
|
||||||
|
getValue={s => s.url}
|
||||||
|
getLabel={s => s.title}
|
||||||
|
getSublabel={s => s.url}
|
||||||
|
onChange={(siteUrl, item) => setForm({ ...form, siteUrl, siteTitle: item?.title ?? siteUrl, listName: '' })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { toEmployeeOption } from '../../utils/employee';
|
|||||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||||
import { useSharePointFields } from '../../hooks/useSharePointFields';
|
import { useSharePointFields } from '../../hooks/useSharePointFields';
|
||||||
import { ApprovalRouteApi } from '../../services/api';
|
import { ApprovalRouteApi } from '../../services/api';
|
||||||
import { SharePointListsClient } from '../../services/sharePointLists';
|
import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists';
|
||||||
import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui';
|
import { Checkbox, Field, FormGrid, FormModal, RadioGroup, SearchableSelect, Select, TextInput } from '../ui';
|
||||||
|
|
||||||
export interface IRouteStepFormModalProps {
|
export interface IRouteStepFormModalProps {
|
||||||
@@ -123,6 +123,25 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
|||||||
return options;
|
return options;
|
||||||
}, [sharePointFields.items, form.fieldName]);
|
}, [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 submitForm = (): void => {
|
||||||
const orderNo = Number(form.orderNo);
|
const orderNo = Number(form.orderNo);
|
||||||
if (!orderNo || orderNo < 1) {
|
if (!orderNo || orderNo < 1) {
|
||||||
@@ -141,6 +160,10 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
|||||||
submit.fail(texts.routeSteps.validationCondition);
|
submit.fail(texts.routeSteps.validationCondition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (form.hasCondition && isNumericField && !Number.isFinite(Number(form.value.trim()))) {
|
||||||
|
submit.fail(texts.routeSteps.validationConditionNumber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
orderNo,
|
orderNo,
|
||||||
@@ -293,13 +316,14 @@ export const RouteStepFormModal: React.FC<IRouteStepFormModalProps> = ({
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={texts.routeSteps.conditionOperationLabel} required>
|
<Field label={texts.routeSteps.conditionOperationLabel} required>
|
||||||
<Select
|
<Select
|
||||||
options={OPERATION_OPTIONS}
|
options={operationOptions}
|
||||||
value={form.operation}
|
value={form.operation}
|
||||||
onChange={e => setForm({ ...form, operation: e.target.value as ComparisonOperator })}
|
onChange={e => setForm({ ...form, operation: e.target.value as ComparisonOperator })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={texts.routeSteps.conditionValueLabel} required>
|
<Field label={texts.routeSteps.conditionValueLabel} required>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
type={isNumericField ? 'number' : 'text'}
|
||||||
value={form.value}
|
value={form.value}
|
||||||
onChange={e => setForm({ ...form, value: e.target.value })}
|
onChange={e => setForm({ ...form, value: e.target.value })}
|
||||||
placeholder={texts.routeSteps.conditionValuePlaceholder}
|
placeholder={texts.routeSteps.conditionValuePlaceholder}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PortalDictionaryClient } from '../../services/portalDictionary';
|
|||||||
import { SharePointListsClient } from '../../services/sharePointLists';
|
import { SharePointListsClient } from '../../services/sharePointLists';
|
||||||
import { RequestType, RouteStepStatus, TemplateRoute, TemplateRouteStep } from '../../models';
|
import { RequestType, RouteStepStatus, TemplateRoute, TemplateRouteStep } from '../../models';
|
||||||
import { routeStepStatusLabels, routeTypeLabels, texts } from '../../texts';
|
import { routeStepStatusLabels, routeTypeLabels, texts } from '../../texts';
|
||||||
import { describeStepExecutor } from '../../utils/stepExecutor';
|
import { describeStepCondition, describeStepExecutor } from '../../utils/stepExecutor';
|
||||||
import { useRouteSteps } from '../../hooks/useRouteSteps';
|
import { useRouteSteps } from '../../hooks/useRouteSteps';
|
||||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||||
@@ -121,6 +121,11 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: r => (r.allowReassignToOtherApprovers ? <Badge variant="ok">{texts.routeSteps.reassignAllowed}</Badge> : <Badge variant="neutral">{texts.common.no}</Badge>)
|
render: r => (r.allowReassignToOtherApprovers ? <Badge variant="ok">{texts.routeSteps.reassignAllowed}</Badge> : <Badge variant="neutral">{texts.common.no}</Badge>)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'condition',
|
||||||
|
header: texts.routeSteps.columnCondition,
|
||||||
|
render: r => describeStepCondition(r) ?? texts.common.empty
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { spfi, SPFI, SPFx } from '@pnp/sp';
|
|||||||
import '@pnp/sp/webs';
|
import '@pnp/sp/webs';
|
||||||
import '@pnp/sp/lists';
|
import '@pnp/sp/lists';
|
||||||
import '@pnp/sp/fields';
|
import '@pnp/sp/fields';
|
||||||
|
import '@pnp/sp/search';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
// Списки, из которых типы заявок берут list_name/field_name, живут на произвольном
|
// Списки, из которых типы заявок берут list_name/field_name, живут на произвольном
|
||||||
@@ -9,6 +10,10 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
// размещён сам веб-парт. Поэтому клиент всегда создаётся под конкретный site_url
|
// размещён сам веб-парт. Поэтому клиент всегда создаётся под конкретный site_url
|
||||||
// (spfi(siteUrl).using(SPFx(context))), а не через общий getSP() под текущий сайт.
|
// (spfi(siteUrl).using(SPFx(context))), а не через общий getSP() под текущий сайт.
|
||||||
// См. приложенный референс (sp-lists-frontend.md), раздел 8 «Другой сайт».
|
// См. приложенный референс (sp-lists-frontend.md), раздел 8 «Другой сайт».
|
||||||
|
//
|
||||||
|
// Поиск сайтов (searchSites), наоборот, идёт через поисковый индекс SharePoint —
|
||||||
|
// он общий на весь тенант независимо от того, с какого сайта пришёл запрос,
|
||||||
|
// поэтому там клиент строится под текущий сайт веб-части (SPFx(context) без url).
|
||||||
|
|
||||||
export interface SharePointList {
|
export interface SharePointList {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,6 +23,26 @@ export interface SharePointList {
|
|||||||
export interface SharePointListField {
|
export interface SharePointListField {
|
||||||
internalName: string;
|
internalName: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/** SharePoint TypeAsString (Text, Number, Currency, DateTime, Choice, ...). */
|
||||||
|
typeAsString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePointSiteSuggestion {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePointSiteSearchResult {
|
||||||
|
items: SharePointSiteSuggestion[];
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharePoint TypeAsString для колонок с числовыми значениями — только для них
|
||||||
|
// имеют смысл операторы «больше»/«меньше» (см. RouteStepFormModal).
|
||||||
|
const NUMERIC_FIELD_TYPES = new Set(['Number', 'Currency']);
|
||||||
|
|
||||||
|
export function isNumericFieldType(typeAsString: string): boolean {
|
||||||
|
return NUMERIC_FIELD_TYPES.has(typeAsString);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IListItem {
|
interface IListItem {
|
||||||
@@ -28,6 +53,12 @@ interface IListItem {
|
|||||||
interface IFieldItem {
|
interface IFieldItem {
|
||||||
InternalName: string;
|
InternalName: string;
|
||||||
Title: string;
|
Title: string;
|
||||||
|
TypeAsString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISearchResultRow {
|
||||||
|
Title?: string;
|
||||||
|
Path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clientFor(context: WebPartContext, siteUrl: string): SPFI {
|
function clientFor(context: WebPartContext, siteUrl: string): SPFI {
|
||||||
@@ -48,14 +79,42 @@ export class SharePointListsClient {
|
|||||||
return items.map(x => ({ id: x.Id, title: x.Title }));
|
return items.map(x => ({ id: x.Id, title: x.Title }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовки столбцов (внутреннее + отображаемое имя) списка — исключая служебные поля. */
|
/** Заголовки столбцов (внутреннее + отображаемое имя + тип) списка — исключая служебные поля. */
|
||||||
public async listFields(siteUrl: string, listTitle: string): Promise<SharePointListField[]> {
|
public async listFields(siteUrl: string, listTitle: string): Promise<SharePointListField[]> {
|
||||||
const sp = clientFor(this.context, siteUrl);
|
const sp = clientFor(this.context, siteUrl);
|
||||||
const items = await sp.web.lists.getByTitle(listTitle).fields
|
const items = await sp.web.lists.getByTitle(listTitle).fields
|
||||||
.select('InternalName', 'Title')
|
.select('InternalName', 'Title', 'TypeAsString')
|
||||||
.filter('Hidden eq false and ReadOnlyField eq false')
|
.filter('Hidden eq false and ReadOnlyField eq false')
|
||||||
.orderBy('Title')() as IFieldItem[];
|
.orderBy('Title')() as IFieldItem[];
|
||||||
|
|
||||||
return items.map(x => ({ internalName: x.InternalName, title: x.Title }));
|
return items.map(x => ({ internalName: x.InternalName, title: x.Title, typeAsString: x.TypeAsString }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поиск сайтов (site collections) по всему тенанту через поисковый индекс
|
||||||
|
* SharePoint (contentclass:STS_Site) — не требует заранее знать site_url.
|
||||||
|
* Результаты обрезаны правами доступа текущего пользователя, как и любой
|
||||||
|
* SharePoint Search запрос.
|
||||||
|
*/
|
||||||
|
public async searchSites(query: string, page: number, pageSize = 20): Promise<SharePointSiteSearchResult> {
|
||||||
|
const sp = spfi().using(SPFx(this.context));
|
||||||
|
const escaped = query.trim().replace(/["*()]/g, '');
|
||||||
|
const queryText = escaped ? `contentclass:STS_Site ${escaped}*` : 'contentclass:STS_Site';
|
||||||
|
|
||||||
|
const result = await sp.search({
|
||||||
|
Querytext: queryText,
|
||||||
|
SelectProperties: ['Title', 'Path'],
|
||||||
|
RowLimit: pageSize,
|
||||||
|
StartRow: (page - 1) * pageSize,
|
||||||
|
TrimDuplicates: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = (result.PrimarySearchResults ?? []) as ISearchResultRow[];
|
||||||
|
const items = rows
|
||||||
|
.filter(r => Boolean(r.Path))
|
||||||
|
.map(r => ({ title: r.Title || r.Path as string, url: r.Path as string }));
|
||||||
|
|
||||||
|
const totalRows = (result as unknown as { TotalRows?: number }).TotalRows ?? items.length;
|
||||||
|
return { items, hasMore: page * pageSize < totalRows };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ export const comparisonOperatorLabels: Record<ComparisonOperator, string> = {
|
|||||||
[ComparisonOperator.Equal]: 'Равно (=)'
|
[ComparisonOperator.Equal]: 'Равно (=)'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const comparisonOperatorSymbols: Record<ComparisonOperator, string> = {
|
||||||
|
[ComparisonOperator.None]: '',
|
||||||
|
[ComparisonOperator.GreaterThan]: '>',
|
||||||
|
[ComparisonOperator.LessThan]: '<',
|
||||||
|
[ComparisonOperator.Equal]: '='
|
||||||
|
};
|
||||||
|
|
||||||
export const texts = {
|
export const texts = {
|
||||||
common: {
|
common: {
|
||||||
home: 'Главная',
|
home: 'Главная',
|
||||||
@@ -104,6 +111,8 @@ export const texts = {
|
|||||||
columnName: 'Название',
|
columnName: 'Название',
|
||||||
columnPageUrl: 'Url страницы',
|
columnPageUrl: 'Url страницы',
|
||||||
columnSection: 'Раздел',
|
columnSection: 'Раздел',
|
||||||
|
columnListName: 'Список SharePoint',
|
||||||
|
columnSiteUrl: 'Сайт SharePoint',
|
||||||
createTitle: 'Новый тип заявки',
|
createTitle: 'Новый тип заявки',
|
||||||
editTitle: 'Редактировать тип заявки',
|
editTitle: 'Редактировать тип заявки',
|
||||||
nameLabel: 'Название',
|
nameLabel: 'Название',
|
||||||
@@ -116,6 +125,7 @@ export const texts = {
|
|||||||
siteUrlLabel: 'Сайт SharePoint',
|
siteUrlLabel: 'Сайт SharePoint',
|
||||||
siteUrlHint: 'Сайт, где живёт список — например, https://tnsukz.sharepoint.com/sites/hrm.',
|
siteUrlHint: 'Сайт, где живёт список — например, https://tnsukz.sharepoint.com/sites/hrm.',
|
||||||
siteUrlPlaceholder: 'https://tnsukz.sharepoint.com/sites/hrm',
|
siteUrlPlaceholder: 'https://tnsukz.sharepoint.com/sites/hrm',
|
||||||
|
siteUrlSearchPlaceholder: 'Поиск сайта по названию...',
|
||||||
listNameLabel: 'Список SharePoint',
|
listNameLabel: 'Список SharePoint',
|
||||||
listNameHint: 'Источник полей (field_name) для условий в шагах маршрута.',
|
listNameHint: 'Источник полей (field_name) для условий в шагах маршрута.',
|
||||||
listNamePlaceholder: 'Сначала укажите сайт SharePoint',
|
listNamePlaceholder: 'Сначала укажите сайт SharePoint',
|
||||||
@@ -184,6 +194,7 @@ export const texts = {
|
|||||||
columnExecutor: 'Исполнитель',
|
columnExecutor: 'Исполнитель',
|
||||||
columnStatus: 'Статус',
|
columnStatus: 'Статус',
|
||||||
columnReassign: 'Переназначение',
|
columnReassign: 'Переназначение',
|
||||||
|
columnCondition: 'Условие',
|
||||||
reassignAllowed: 'Разрешено',
|
reassignAllowed: 'Разрешено',
|
||||||
executorRole: (roleLabel: string): string => `Роль: ${roleLabel}`,
|
executorRole: (roleLabel: string): string => `Роль: ${roleLabel}`,
|
||||||
executorHead: (headLabel: string): string => `Руководитель: ${headLabel}`,
|
executorHead: (headLabel: string): string => `Руководитель: ${headLabel}`,
|
||||||
@@ -222,6 +233,7 @@ export const texts = {
|
|||||||
validationExecutor: 'Выберите сотрудника-исполнителя.',
|
validationExecutor: 'Выберите сотрудника-исполнителя.',
|
||||||
validationPosition: 'Укажите должность.',
|
validationPosition: 'Укажите должность.',
|
||||||
validationCondition: 'Заполните поле списка, оператор и значение условия.',
|
validationCondition: 'Заполните поле списка, оператор и значение условия.',
|
||||||
|
validationConditionNumber: 'Выбранное поле числовое — значение условия должно быть числом.',
|
||||||
deleteTitle: 'Удалить шаг?',
|
deleteTitle: 'Удалить шаг?',
|
||||||
deleteText: (orderNo: number): string => `Шаг #${orderNo} будет удалён безвозвратно.`
|
deleteText: (orderNo: number): string => `Шаг #${orderNo} будет удалён безвозвратно.`
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Employee, HeadType, ProjectRole, StepExecutorKind, TemplateRouteStep } from '../models';
|
import { ComparisonOperator, Employee, HeadType, ProjectRole, StepExecutorKind, TemplateRouteStep } from '../models';
|
||||||
import { headTypeLabels, projectRoleLabels, texts } from '../texts';
|
import { comparisonOperatorSymbols, headTypeLabels, projectRoleLabels, texts } from '../texts';
|
||||||
import { findEmployeeByEmail } from './employee';
|
import { findEmployeeByEmail } from './employee';
|
||||||
|
|
||||||
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
|
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
|
||||||
@@ -36,3 +36,9 @@ export function describeStepExecutor(step: TemplateRouteStep, employees: Employe
|
|||||||
}
|
}
|
||||||
return { primary: texts.common.empty };
|
return { primary: texts.common.empty };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Колонка «Условие» в таблице шагов — undefined, если у шага условия нет. */
|
||||||
|
export function describeStepCondition(step: TemplateRouteStep): string | undefined {
|
||||||
|
if (!step.fieldName || !step.operation || step.operation === ComparisonOperator.None || !step.value) return undefined;
|
||||||
|
return `${step.fieldName} ${comparisonOperatorSymbols[step.operation]} ${step.value}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user