Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06044bbce9 | |||
| e9b0df4822 |
@@ -6,7 +6,7 @@ import { ICategory } from '../models/types';
|
||||
*/
|
||||
export const CATEGORIES: ICategory[] = [
|
||||
{ id: 'recruiting', title: 'Отдел рекрутинга', icon: 'people' },
|
||||
{ id: 'it', title: 'IT', icon: 'monitor' },
|
||||
{ id: 'it', title: 'IT/1C', icon: 'monitor' },
|
||||
{ id: 'accounting', title: 'Бухгалтерия', icon: 'calculator' },
|
||||
{ id: 'hr', title: 'Отдел кадров', icon: 'clipboard' },
|
||||
{ id: 'warehouse', title: 'Склад / Снабжение', icon: 'building' }
|
||||
|
||||
@@ -368,7 +368,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
<FormCard
|
||||
title={requestType.title}
|
||||
subtitle="Категория: IT"
|
||||
subtitle="Категория: IT/1C"
|
||||
belowSubtitle={
|
||||
<InitiatorLine
|
||||
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
// Варианты Choice-полей — см. forms/Ticket1C/provisioning.json (единый источник правды).
|
||||
|
||||
export const WORK_TYPE_OPTIONS: string[] = [
|
||||
'Создание пользователя в 1С',
|
||||
'Расширение прав доступа / предоставление доступа к документам других пользователей',
|
||||
'Открытие даты запрета',
|
||||
'Прочее',
|
||||
'Не работает 1С',
|
||||
'Блокировка пользователя',
|
||||
];
|
||||
|
||||
export const ORGANIZATION_OPTIONS: string[] = [
|
||||
'AFL Engineering',
|
||||
'CL MANAGEMENT',
|
||||
'KazPromService',
|
||||
'QAZGEOMAX',
|
||||
'TANSU Construction',
|
||||
'АСТЕЛ-К',
|
||||
'ГеоТрек',
|
||||
'Монтажник-Т',
|
||||
'ОСН Беркут',
|
||||
'CT Global',
|
||||
];
|
||||
|
||||
export const DB1C_OPTIONS: string[] = [
|
||||
'1С: Зарплата и управление персоналом (ЗУП)',
|
||||
'1С: Документооборот (ДО)',
|
||||
'1С: Автотранспортное предприятие (АТП)',
|
||||
'1С: БСО TANSU Construction',
|
||||
'1С: Бухгалтерия AFL Engineering',
|
||||
'1С: Бухгалтерия CL MANAGEMENT',
|
||||
'1С: Бухгалтерия KazPromService',
|
||||
'1С: Бухгалтерия QAZGEOMAX',
|
||||
'1С: Бухгалтерия АСТЕЛ-К',
|
||||
'1С: Бухгалтерия ГеоТрек',
|
||||
'1С: Бухгалтерия Монтажник-Т',
|
||||
'1С: Бухгалтерия ОСН Беркут',
|
||||
'1С: Бухгалтерия CT Global',
|
||||
];
|
||||
// Варианты «Видов работ», «Организации» и «Базы 1С» в коде не хранятся — форма
|
||||
// читает их из колонок списка SP (см. shared/fieldChoices). Чтобы добавить пункт,
|
||||
// допишите его в параметрах столбца списка. Варианты в provisioning.json —
|
||||
// только стартовые, для создания списка с нуля.
|
||||
|
||||
export const QUESTION_MAX = 1000;
|
||||
|
||||
// Пользовательские тексты состояний загрузки вариантов полей.
|
||||
export const CHOICES_LOADING_PLACEHOLDER = 'Загрузка вариантов…';
|
||||
export const CHOICES_EMPTY_PLACEHOLDER = 'Нет вариантов — обратитесь к администратору';
|
||||
export const CHOICES_ERROR_PREFIX = 'Не удалось загрузить варианты полей';
|
||||
|
||||
// Значения из property pane старой web part (см. живую страницу SharePoint).
|
||||
export const PLANNER_PLAN_ID = '168WqDV8CEKkW3cL6olCRZgACwYa';
|
||||
export const PLANNER_BUCKET_NAME = 'Входящие неотсортированные';
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"Открытие даты запрета",
|
||||
"Прочее",
|
||||
"Не работает 1С",
|
||||
"Блокировка пользователя"
|
||||
"Блокировка пользователя",
|
||||
"Маршруты в ДО"
|
||||
] },
|
||||
{ "internalName": "Organization", "title": "Организация", "type": "Choice", "required": true, "choices": [
|
||||
"AFL Engineering",
|
||||
|
||||
@@ -1,52 +1,76 @@
|
||||
import * as React from 'react';
|
||||
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';
|
||||
import { Alert, CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
|
||||
import { useFieldChoices, IFieldChoicesState } from '../../../shared/fieldChoices';
|
||||
import { dataListTitle } from '../../../shared/listTitles';
|
||||
import { FORM_ID, ISectionProps } from '../model';
|
||||
import {
|
||||
QUESTION_MAX,
|
||||
CHOICES_LOADING_PLACEHOLDER,
|
||||
CHOICES_EMPTY_PLACEHOLDER,
|
||||
CHOICES_ERROR_PREFIX
|
||||
} from '../config';
|
||||
|
||||
const toOptions = (values: string[]): ISelectOption[] => values.map((v) => ({ value: v, label: v }));
|
||||
function choicesPlaceholder(state: IFieldChoicesState, readyText: string): string {
|
||||
if (state.isLoading) return CHOICES_LOADING_PLACEHOLDER;
|
||||
if (state.options.length === 0) return CHOICES_EMPTY_PLACEHOLDER;
|
||||
return readyText;
|
||||
}
|
||||
|
||||
const WORK_TYPE_SELECT_OPTIONS = toOptions(WORK_TYPE_OPTIONS);
|
||||
const ORGANIZATION_SELECT_OPTIONS = toOptions(ORGANIZATION_OPTIONS);
|
||||
const DB1C_SELECT_OPTIONS = toOptions(DB1C_OPTIONS);
|
||||
// Сохранённое значение могли убрать из вариантов колонки — без этого Select
|
||||
// показал бы пустое поле, и заявитель не увидел бы, что было выбрано.
|
||||
function withCurrentValue(options: ISelectOption[], value: string): ISelectOption[] {
|
||||
if (!value || options.some((o) => o.value === value)) return options;
|
||||
return [{ value, label: value }, ...options];
|
||||
}
|
||||
|
||||
export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
<CollapseSection title="Заявка" icon="clipboard">
|
||||
<FormField label="Наименование задачи" htmlFor="TaskName" required full>
|
||||
<TextInput id="TaskName" value={data.TaskName} onChange={(v) => set('TaskName', v)} />
|
||||
</FormField>
|
||||
<FormField label="Виды работ" htmlFor="WorkType" required>
|
||||
<Select
|
||||
id="WorkType"
|
||||
value={data.WorkType}
|
||||
onChange={(v) => set('WorkType', v)}
|
||||
options={WORK_TYPE_SELECT_OPTIONS}
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Организация" htmlFor="Organization" required>
|
||||
<Select
|
||||
id="Organization"
|
||||
value={data.Organization}
|
||||
onChange={(v) => set('Organization', v)}
|
||||
options={ORGANIZATION_SELECT_OPTIONS}
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="База 1С" fieldKey="DB1C" required>
|
||||
<MultiChoiceField
|
||||
value={data.DB1C}
|
||||
onChange={(v) => set('DB1C', v)}
|
||||
options={DB1C_SELECT_OPTIONS}
|
||||
placeholder="Выберите одну или несколько баз"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
|
||||
<TextArea
|
||||
id="Question"
|
||||
value={data.Question}
|
||||
onChange={(v) => set('Question', v.slice(0, QUESTION_MAX))}
|
||||
rows={6}
|
||||
/>
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
|
||||
// Варианты — из текущей партиции: туда пишутся новые заявки.
|
||||
const listTitle = dataListTitle(FORM_ID);
|
||||
const workTypes = useFieldChoices(listTitle, 'WorkType');
|
||||
const organizations = useFieldChoices(listTitle, 'Organization');
|
||||
const databases = useFieldChoices(listTitle, 'DB1C');
|
||||
const choicesError = workTypes.error || organizations.error || databases.error;
|
||||
|
||||
return (
|
||||
<CollapseSection title="Заявка" icon="clipboard">
|
||||
{choicesError && <Alert variant="err">{`${CHOICES_ERROR_PREFIX}: ${choicesError}`}</Alert>}
|
||||
<FormField label="Наименование задачи" htmlFor="TaskName" required full>
|
||||
<TextInput id="TaskName" value={data.TaskName} onChange={(v) => set('TaskName', v)} />
|
||||
</FormField>
|
||||
<FormField label="Виды работ" htmlFor="WorkType" required>
|
||||
<Select
|
||||
id="WorkType"
|
||||
value={data.WorkType}
|
||||
onChange={(v) => set('WorkType', v)}
|
||||
options={withCurrentValue(workTypes.options, data.WorkType)}
|
||||
placeholder={choicesPlaceholder(workTypes, '— выберите —')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Организация" htmlFor="Organization" required>
|
||||
<Select
|
||||
id="Organization"
|
||||
value={data.Organization}
|
||||
onChange={(v) => set('Organization', v)}
|
||||
options={withCurrentValue(organizations.options, data.Organization)}
|
||||
placeholder={choicesPlaceholder(organizations, '— выберите —')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="База 1С" fieldKey="DB1C" required>
|
||||
<MultiChoiceField
|
||||
value={data.DB1C}
|
||||
onChange={(v) => set('DB1C', v)}
|
||||
options={databases.options}
|
||||
placeholder={choicesPlaceholder(databases, 'Выберите одну или несколько баз')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
|
||||
<TextArea
|
||||
id="Question"
|
||||
value={data.Question}
|
||||
onChange={(v) => set('Question', v.slice(0, QUESTION_MAX))}
|
||||
rows={6}
|
||||
/>
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -345,7 +345,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
<FormCard
|
||||
title={requestType.title}
|
||||
subtitle="Категория: IT"
|
||||
subtitle="Категория: IT/1C"
|
||||
belowSubtitle={
|
||||
<InitiatorLine
|
||||
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import '@pnp/sp/fields';
|
||||
import { getSP } from '../pnp/pnpjs';
|
||||
|
||||
/** Ответ SP по колонке Choice: массив в nometadata, { results } — в verbose-формате OData. */
|
||||
interface IChoiceFieldResponse {
|
||||
Choices?: string[] | { results?: string[] };
|
||||
}
|
||||
|
||||
// Кеш на время жизни страницы: варианты администратор меняет редко, а форму за
|
||||
// сессию открывают по нескольку раз — повторно ходить в SP незачем. Хранится
|
||||
// промис, чтобы параллельные вызовы (три поля одной формы) не дублировали запрос.
|
||||
const cache = new Map<string, Promise<string[]>>();
|
||||
|
||||
/**
|
||||
* Варианты колонки Choice/MultiChoice прямо из списка SharePoint.
|
||||
*
|
||||
* Колонка списка — единственный источник вариантов: чтобы добавить пункт,
|
||||
* достаточно дописать его в параметрах столбца, без правки кода и деплоя.
|
||||
*/
|
||||
export function getFieldChoices(listTitle: string, internalName: string): Promise<string[]> {
|
||||
const key = `${listTitle}|${internalName}`.toLowerCase();
|
||||
const cached = cache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const request = loadFieldChoices(listTitle, internalName);
|
||||
// Сбой не кешируем — при следующем открытии формы запрос уйдёт заново.
|
||||
request.catch(() => cache.delete(key));
|
||||
cache.set(key, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
async function loadFieldChoices(listTitle: string, internalName: string): Promise<string[]> {
|
||||
const field = (await getSP()
|
||||
.web.lists.getByTitle(listTitle)
|
||||
.fields.getByInternalNameOrTitle(internalName)
|
||||
.select('Choices')()) as IChoiceFieldResponse;
|
||||
|
||||
const choices = Array.isArray(field.Choices) ? field.Choices : field.Choices?.results || [];
|
||||
return choices.filter((choice) => choice.trim() !== '');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { getFieldChoices } from './fieldChoices.service';
|
||||
export { useFieldChoices } from './useFieldChoices';
|
||||
export type { IFieldChoicesState } from './useFieldChoices';
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { ISelectOption } from '@tnsu/ui-kit';
|
||||
import { friendlyErrorMessage } from '../diagnostics';
|
||||
import { getFieldChoices } from './fieldChoices.service';
|
||||
|
||||
export interface IFieldChoicesState {
|
||||
/** Варианты колонки в формате Select/MultiChoiceField; пусто, пока грузится или при ошибке. */
|
||||
options: ISelectOption[];
|
||||
isLoading: boolean;
|
||||
/** Текст ошибки загрузки; undefined — ошибки нет. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const LOADING_STATE: IFieldChoicesState = { options: [], isLoading: true };
|
||||
|
||||
/** Варианты колонки Choice списка SharePoint — см. getFieldChoices. */
|
||||
export function useFieldChoices(listTitle: string, internalName: string): IFieldChoicesState {
|
||||
const [state, setState] = React.useState<IFieldChoicesState>(LOADING_STATE);
|
||||
|
||||
React.useEffect(() => {
|
||||
let alive = true;
|
||||
setState(LOADING_STATE);
|
||||
getFieldChoices(listTitle, internalName)
|
||||
.then((choices) => {
|
||||
if (alive) setState({ options: choices.map((c) => ({ value: c, label: c })), isLoading: false });
|
||||
})
|
||||
.catch((e) => {
|
||||
if (alive) setState({ options: [], isLoading: false, error: friendlyErrorMessage(e) });
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [listTitle, internalName]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user