diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx index a6c8cd4..923c14a 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ApprovalRouteApi } from '../../services/api'; -import { SharePointListsClient } from '../../services/sharePointLists'; +import { SharePointListsClient, SharePointSiteSuggestion } from '../../services/sharePointLists'; import { RequestType } from '../../models'; import { texts } from '../../texts'; import { useRequestTypes } from '../../hooks/useRequestTypes'; @@ -10,6 +10,7 @@ import { useDeleteTarget } from '../../hooks/useDeleteTarget'; import { ActionsCell, Alert, + AsyncSearchableSelect, Breadcrumbs, Button, ConfirmDialog, @@ -34,10 +35,12 @@ interface IFormState { pageUrl: string; section: string; siteUrl: string; + /** Заголовок сайта — только для отображения в триггере поиска, на бэкенд не уходит. */ + siteTitle: 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 = ({ api, sharePoint }) => { const list = useRequestTypes(api); @@ -47,6 +50,10 @@ export const RequestTypesScreen: React.FC = ({ api, sh const [form, setForm] = React.useState(EMPTY_FORM); const submit = useAsyncSubmit(); const sharePointLists = useSharePointLists(sharePoint, form.siteUrl); + const searchSites = React.useCallback( + (query: string, page: number) => sharePoint.searchSites(query, page), + [sharePoint] + ); const deleteTarget = useDeleteTarget(item => api.requestTypes.remove(item.id), list.reload); @@ -63,6 +70,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh pageUrl: item.pageUrl, section: item.section, siteUrl: item.siteUrl ?? '', + siteTitle: item.siteUrl ?? '', listName: item.listName ?? '' }); setShowForm(true); @@ -117,6 +125,8 @@ export const RequestTypesScreen: React.FC = ({ api, sh { key: 'name', header: texts.requestTypes.columnName, render: r => {r.name} }, { key: 'url', header: texts.requestTypes.columnPageUrl, render: r => r.pageUrl }, { 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', header: '', @@ -167,10 +177,18 @@ export const RequestTypesScreen: React.FC = ({ api, sh /> - 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} + 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: '' })} /> = ({ 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); if (!orderNo || orderNo < 1) { @@ -141,6 +160,10 @@ export const RouteStepFormModal: React.FC = ({ 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, @@ -293,13 +316,14 @@ export const RouteStepFormModal: React.FC = ({