fix: нейтральный текст при пустом маршруте в заявке 1С
Tests / frontend-test (push) Successful in 1m19s
Tests / frontend-test (push) Successful in 1m19s
This commit is contained in:
@@ -129,13 +129,14 @@ $SITE_COLUMN_GROUP = 'Портал заявок'
|
|||||||
|
|
||||||
# Типы, которые НЕ делаем эталоном (столбцом сайта), а создаём столбцом списка:
|
# Типы, которые НЕ делаем эталоном (столбцом сайта), а создаём столбцом списка:
|
||||||
# - Lookup (эталон привязан к одному списку, а Ticket у каждого свой);
|
# - Lookup (эталон привязан к одному списку, а Ticket у каждого свой);
|
||||||
# - Number с decimals и UserMulti (создаются через XML; web-level XML в PnP падает
|
# - Number с decimals, UserMulti и MultiChoice (создаются через XML; web-level XML
|
||||||
# NRE, и это одноразовые поля конкретной формы — переиспользовать нечего).
|
# в PnP падает NRE, и это одноразовые поля конкретной формы — переиспользовать нечего).
|
||||||
function Test-NeedsListColumn {
|
function Test-NeedsListColumn {
|
||||||
param($Col)
|
param($Col)
|
||||||
return ($Col.type -eq 'Lookup') `
|
return ($Col.type -eq 'Lookup') `
|
||||||
-or ($Col.type -eq 'Number' -and $null -ne $Col.decimals) `
|
-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)
|
param([string]$List, $Col)
|
||||||
|
|
||||||
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
|
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) {
|
if ($Col.type -eq 'Choice' -and $Col.choices) {
|
||||||
try {
|
try {
|
||||||
Set-PnPField -List $List -Identity $Col.internalName -Values @{ Choices = [string[]]$Col.choices } | Out-Null
|
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
|
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 { "<CHOICE>$(ConvertTo-XmlSafe $_)</CHOICE>" }) -join ''
|
||||||
|
$xml = "<Field Type='MultiChoice' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' FillInChoice='FALSE'><CHOICES>$choicesXml</CHOICES></Field>"
|
||||||
|
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 }
|
'Url' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type URL -AddToDefaultView | Out-Null }
|
||||||
'Number' {
|
'Number' {
|
||||||
if ($null -ne $Col.decimals) {
|
if ($null -ne $Col.decimals) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
PeopleField,
|
PeopleField,
|
||||||
PeopleFieldMulti,
|
PeopleFieldMulti,
|
||||||
|
MultiChoiceField,
|
||||||
} from "../../../shared/ui";
|
} from "../../../shared/ui";
|
||||||
import { MultiChoiceField } from "./MultiChoiceField";
|
|
||||||
import {
|
import {
|
||||||
ISectionProps,
|
ISectionProps,
|
||||||
PERSON_PLACEHOLDER,
|
PERSON_PLACEHOLDER,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
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 { PositionPicker } from '../../../shared/dictionary';
|
||||||
import { LanguagesField } from './LanguagesField';
|
import { LanguagesField } from './LanguagesField';
|
||||||
import { MultiChoiceField } from './MultiChoiceField';
|
|
||||||
import {
|
import {
|
||||||
ISectionProps,
|
ISectionProps,
|
||||||
EMPLOYMENT_TYPE_OPTIONS,
|
EMPLOYMENT_TYPE_OPTIONS,
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
data.TaskName.trim() &&
|
data.TaskName.trim() &&
|
||||||
data.WorkType &&
|
data.WorkType &&
|
||||||
data.Organization &&
|
data.Organization &&
|
||||||
data.DB1C &&
|
data.DB1C.length > 0 &&
|
||||||
data.Question.trim()
|
data.Question.trim()
|
||||||
);
|
);
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
@@ -412,7 +412,10 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
<ApprovalMatrix
|
<ApprovalMatrix
|
||||||
editor={matrixEditor}
|
editor={matrixEditor}
|
||||||
search={(q) => approval.searchUsers(q)}
|
search={(q) => approval.searchUsers(q)}
|
||||||
emptyText="Маршрут для этого вида работ пока не настроен — обратитесь к администратору маршрутов согласования."
|
// Пустой маршрут — штатная ситуация: для части видов работ согласование
|
||||||
|
// не нужно, заявка сразу уходит в Planner. Сбой загрузки маршрута
|
||||||
|
// показывается отдельно (matrix.error) и блокирует отправку.
|
||||||
|
emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function buildPlannerNotes(
|
|||||||
lines.push(`Телефон: ${data.Phone}`);
|
lines.push(`Телефон: ${data.Phone}`);
|
||||||
lines.push(`Отдел: ${data.Department}`);
|
lines.push(`Отдел: ${data.Department}`);
|
||||||
lines.push(`Должность: ${data.Position}`);
|
lines.push(`Должность: ${data.Position}`);
|
||||||
lines.push(`База 1С: ${data.DB1C}`);
|
lines.push(`База 1С: ${data.DB1C.join(', ')}`);
|
||||||
lines.push(`Виды работ: ${data.WorkType}`);
|
lines.push(`Виды работ: ${data.WorkType}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('Вопрос:');
|
lines.push('Вопрос:');
|
||||||
|
|||||||
@@ -3,10 +3,24 @@ import { dataListTitle } from '../../shared/listTitles';
|
|||||||
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
// TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName).
|
// TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName).
|
||||||
|
// DB1C — отдельно, это множественный выбор (см. toStringArray).
|
||||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||||
'WorkType', 'Organization', 'DB1C', 'FIO', 'Email', 'Phone', 'Department', 'Position', 'Manager', 'Question',
|
'WorkType', 'Organization', 'FIO', 'Email', 'Phone', 'Department', 'Position', 'Manager', 'Question',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значение колонки множественного выбора → string[]. Строка — заявки, сохранённые,
|
||||||
|
* пока «База 1С» была одиночным выбором (или колонку ещё не переключили);
|
||||||
|
* { results } — ответ в verbose-формате OData.
|
||||||
|
*/
|
||||||
|
function toStringArray(raw: unknown): string[] {
|
||||||
|
if (Array.isArray(raw)) return raw.filter((v): v is string => 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 {
|
export interface ILoadedRequest {
|
||||||
data: IFormData;
|
data: IFormData;
|
||||||
status: string;
|
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 list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = (await list
|
const item = (await list
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
.select(...SCALAR_FIELDS, 'DB1C', 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||||
|
|
||||||
const data: IFormData = { ...EMPTY_DATA };
|
const data: IFormData = { ...EMPTY_DATA };
|
||||||
const writable = data as unknown as Record<string, string>;
|
const writable = data as unknown as Record<string, string>;
|
||||||
@@ -36,6 +50,7 @@ export async function loadTicketRequest(itemId: number, listId?: string): Promis
|
|||||||
writable[key] = value === null || value === undefined ? '' : String(value);
|
writable[key] = value === null || value === undefined ? '' : String(value);
|
||||||
});
|
});
|
||||||
data.TaskName = (item.Title as string) || '';
|
data.TaskName = (item.Title as string) || '';
|
||||||
|
data.DB1C = toStringArray(item.DB1C);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export interface IFormData {
|
|||||||
TaskName: string;
|
TaskName: string;
|
||||||
WorkType: string;
|
WorkType: string;
|
||||||
Organization: string;
|
Organization: string;
|
||||||
DB1C: string;
|
/** Базы 1С — множественный выбор (колонка MultiChoice), минимум одна. */
|
||||||
|
DB1C: string[];
|
||||||
FIO: string;
|
FIO: string;
|
||||||
Email: string;
|
Email: string;
|
||||||
Phone: string;
|
Phone: string;
|
||||||
@@ -45,7 +46,7 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
TaskName: '',
|
TaskName: '',
|
||||||
WorkType: '',
|
WorkType: '',
|
||||||
Organization: '',
|
Organization: '',
|
||||||
DB1C: '',
|
DB1C: [],
|
||||||
FIO: '',
|
FIO: '',
|
||||||
Email: '',
|
Email: '',
|
||||||
Phone: '',
|
Phone: '',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"ОСН Беркут",
|
"ОСН Беркут",
|
||||||
"CT Global"
|
"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С: Документооборот (ДО)",
|
"1С: Документооборот (ДО)",
|
||||||
"1С: Автотранспортное предприятие (АТП)",
|
"1С: Автотранспортное предприятие (АТП)",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
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 { ISectionProps } from '../model';
|
||||||
import { WORK_TYPE_OPTIONS, ORGANIZATION_OPTIONS, DB1C_OPTIONS, QUESTION_MAX } from '../config';
|
import { WORK_TYPE_OPTIONS, ORGANIZATION_OPTIONS, DB1C_OPTIONS, QUESTION_MAX } from '../config';
|
||||||
|
|
||||||
@@ -32,13 +32,12 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => (
|
|||||||
placeholder="— выберите —"
|
placeholder="— выберите —"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="База 1С" htmlFor="DB1C" required>
|
<FormField label="База 1С" fieldKey="DB1C" required>
|
||||||
<Select
|
<MultiChoiceField
|
||||||
id="DB1C"
|
|
||||||
value={data.DB1C}
|
value={data.DB1C}
|
||||||
onChange={(v) => set('DB1C', v)}
|
onChange={(v) => set('DB1C', v)}
|
||||||
options={DB1C_SELECT_OPTIONS}
|
options={DB1C_SELECT_OPTIONS}
|
||||||
placeholder="— выберите —"
|
placeholder="Выберите одну или несколько баз"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
|
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export interface IColumnDef {
|
|||||||
lookupShowField?: string;
|
lookupShowField?: string;
|
||||||
/** Для type === 'User': кого можно выбирать. По умолчанию — как в SharePoint. */
|
/** Для type === 'User': кого можно выбирать. По умолчанию — как в SharePoint. */
|
||||||
userSelectionMode?: 'PeopleOnly' | 'PeopleAndGroups';
|
userSelectionMode?: 'PeopleOnly' | 'PeopleAndGroups';
|
||||||
/** Для type === 'User': разрешить выбор нескольких (создаётся колонка UserMulti). */
|
/**
|
||||||
|
* Разрешить выбор нескольких: для type === 'User' создаётся колонка UserMulti,
|
||||||
|
* для type === 'Choice' — MultiChoice (значение в коде — string[]).
|
||||||
|
*/
|
||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import styles from './MultiChoiceField.module.scss';
|
import styles from './MultiChoiceField.module.scss';
|
||||||
import { useFormReadOnly, ISelectOption } from '../../../shared/ui';
|
import { useFormReadOnly, ISelectOption } from '@tnsu/ui-kit';
|
||||||
|
|
||||||
export interface IMultiChoiceFieldProps {
|
export interface IMultiChoiceFieldProps {
|
||||||
value: string[];
|
value: string[];
|
||||||
@@ -36,6 +36,7 @@ export type { ISelectOption, IConfirmOptions } from '@tnsu/ui-kit';
|
|||||||
export { FormField } from './FormField/FormField';
|
export { FormField } from './FormField/FormField';
|
||||||
export { FieldChangesProvider } from './FieldChanges/FieldChangesContext';
|
export { FieldChangesProvider } from './FieldChanges/FieldChangesContext';
|
||||||
export { FieldChangesBanner } from './FieldChanges/FieldChangesBanner';
|
export { FieldChangesBanner } from './FieldChanges/FieldChangesBanner';
|
||||||
|
export { MultiChoiceField } from './MultiChoiceField/MultiChoiceField';
|
||||||
export { ItemsTable } from './ItemsTable/ItemsTable';
|
export { ItemsTable } from './ItemsTable/ItemsTable';
|
||||||
export type { IItemsTableProps } from './ItemsTable/ItemsTable';
|
export type { IItemsTableProps } from './ItemsTable/ItemsTable';
|
||||||
export type { ILineItem } from './ItemsTable/ILineItem';
|
export type { ILineItem } from './ItemsTable/ILineItem';
|
||||||
|
|||||||
Reference in New Issue
Block a user