diff --git a/provisioning/Provision-Lists.ps1 b/provisioning/Provision-Lists.ps1 index 6046cb2..103a3f2 100644 --- a/provisioning/Provision-Lists.ps1 +++ b/provisioning/Provision-Lists.ps1 @@ -129,13 +129,14 @@ $SITE_COLUMN_GROUP = 'Портал заявок' # Типы, которые НЕ делаем эталоном (столбцом сайта), а создаём столбцом списка: # - Lookup (эталон привязан к одному списку, а Ticket у каждого свой); -# - Number с decimals и UserMulti (создаются через XML; web-level XML в PnP падает -# NRE, и это одноразовые поля конкретной формы — переиспользовать нечего). +# - Number с decimals, UserMulti и MultiChoice (создаются через XML; web-level XML +# в PnP падает NRE, и это одноразовые поля конкретной формы — переиспользовать нечего). function Test-NeedsListColumn { param($Col) return ($Col.type -eq 'Lookup') ` -or ($Col.type -eq 'Number' -and $null -ne $Col.decimals) ` - -or ($Col.type -eq 'User' -and $Col.allowMultiple) + -or ($Col.type -eq 'User' -and $Col.allowMultiple) ` + -or ($Col.type -eq 'Choice' -and $Col.allowMultiple) } # Создаёт/обновляет СТОЛБЕЦ САЙТА (эталон) — один раз на весь сайт. Только простые @@ -184,6 +185,14 @@ function Add-ListColumn { param([string]$List, $Col) if (Test-FieldExists -List $List -InternalName $Col.internalName) { + # Тип существующей колонки скрипт НЕ меняет (данные!) — одиночный выбор в + # множественный переключают вручную: параметры столбца → «флажки». + if ($Col.type -eq 'Choice' -and $Col.allowMultiple) { + $existingType = (Get-PnPField -List $List -Identity $Col.internalName).TypeAsString + if ($existingType -ne 'MultiChoice') { + Write-Warning " [!] $($Col.internalName): в списке одиночный выбор ($existingType), а нужен множественный — переключите столбец на «флажки» вручную" + } + } if ($Col.type -eq 'Choice' -and $Col.choices) { try { Set-PnPField -List $List -Identity $Col.internalName -Values @{ Choices = [string[]]$Col.choices } | Out-Null @@ -217,7 +226,16 @@ function Add-ListColumn { Set-PnPField -List $List -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null } } - 'Choice' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null } + 'Choice' { + if ($Col.allowMultiple) { + $choicesXml = ($Col.choices | ForEach-Object { "$(ConvertTo-XmlSafe $_)" }) -join '' + $xml = "$choicesXml" + Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null + } + else { + Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null + } + } 'Url' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type URL -AddToDefaultView | Out-Null } 'Number' { if ($null -ne $Col.decimals) { diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx index 0bbc790..8a4f483 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx @@ -8,8 +8,8 @@ import { Select, PeopleField, PeopleFieldMulti, + MultiChoiceField, } from "../../../shared/ui"; -import { MultiChoiceField } from "./MultiChoiceField"; import { ISectionProps, PERSON_PLACEHOLDER, diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx index a9f9e4c..08caac9 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { CollapseSection, FormField, TextInput, TextArea, Select } from '../../../shared/ui'; +import { CollapseSection, FormField, TextInput, TextArea, Select, MultiChoiceField } from '../../../shared/ui'; import { PositionPicker } from '../../../shared/dictionary'; import { LanguagesField } from './LanguagesField'; -import { MultiChoiceField } from './MultiChoiceField'; import { ISectionProps, EMPLOYMENT_TYPE_OPTIONS, diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 4c57ad5..3d58f6c 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -226,7 +226,7 @@ const Ticket1CForm: React.FC = ({ data.TaskName.trim() && data.WorkType && data.Organization && - data.DB1C && + data.DB1C.length > 0 && data.Question.trim() ); const validate = (): string | undefined => { @@ -412,7 +412,10 @@ const Ticket1CForm: React.FC = ({ approval.searchUsers(q)} - emptyText="Маршрут для этого вида работ пока не настроен — обратитесь к администратору маршрутов согласования." + // Пустой маршрут — штатная ситуация: для части видов работ согласование + // не нужно, заявка сразу уходит в Planner. Сбой загрузки маршрута + // показывается отдельно (matrix.error) и блокирует отправку. + emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С." /> typeof v === 'string' && v !== ''); + if (raw && typeof raw === 'object' && Array.isArray((raw as { results?: unknown }).results)) { + return toStringArray((raw as { results: unknown[] }).results); + } + return typeof raw === 'string' && raw !== '' ? [raw] : []; +} + export interface ILoadedRequest { data: IFormData; status: string; @@ -26,7 +40,7 @@ export async function loadTicketRequest(itemId: number, listId?: string): Promis const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = (await list .items.getById(itemId) - .select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record; + .select(...SCALAR_FIELDS, 'DB1C', 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; @@ -36,6 +50,7 @@ export async function loadTicketRequest(itemId: number, listId?: string): Promis writable[key] = value === null || value === undefined ? '' : String(value); }); data.TaskName = (item.Title as string) || ''; + data.DB1C = toStringArray(item.DB1C); return { data, diff --git a/src/webparts/requestPortal/forms/Ticket1C/model.ts b/src/webparts/requestPortal/forms/Ticket1C/model.ts index d12850a..b3aae3b 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/model.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/model.ts @@ -16,7 +16,8 @@ export interface IFormData { TaskName: string; WorkType: string; Organization: string; - DB1C: string; + /** Базы 1С — множественный выбор (колонка MultiChoice), минимум одна. */ + DB1C: string[]; FIO: string; Email: string; Phone: string; @@ -45,7 +46,7 @@ export const EMPTY_DATA: IFormData = { TaskName: '', WorkType: '', Organization: '', - DB1C: '', + DB1C: [], FIO: '', Email: '', Phone: '', diff --git a/src/webparts/requestPortal/forms/Ticket1C/provisioning.json b/src/webparts/requestPortal/forms/Ticket1C/provisioning.json index eadb406..26f9524 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/provisioning.json +++ b/src/webparts/requestPortal/forms/Ticket1C/provisioning.json @@ -25,7 +25,7 @@ "ОСН Беркут", "CT Global" ] }, - { "internalName": "DB1C", "title": "База 1С", "type": "Choice", "required": true, "choices": [ + { "internalName": "DB1C", "title": "База 1С", "type": "Choice", "allowMultiple": true, "required": true, "choices": [ "1С: Зарплата и управление персоналом (ЗУП)", "1С: Документооборот (ДО)", "1С: Автотранспортное предприятие (АТП)", diff --git a/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx index b275f80..db82bd5 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CollapseSection, FormField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui'; +import { CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui'; import { ISectionProps } from '../model'; import { WORK_TYPE_OPTIONS, ORGANIZATION_OPTIONS, DB1C_OPTIONS, QUESTION_MAX } from '../config'; @@ -32,13 +32,12 @@ export const RequestSection: React.FC = ({ data, set }) => ( placeholder="— выберите —" /> - -