Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06044bbce9 | |||
| e9b0df4822 |
@@ -6,7 +6,7 @@ import { ICategory } from '../models/types';
|
|||||||
*/
|
*/
|
||||||
export const CATEGORIES: ICategory[] = [
|
export const CATEGORIES: ICategory[] = [
|
||||||
{ id: 'recruiting', title: 'Отдел рекрутинга', icon: 'people' },
|
{ id: 'recruiting', title: 'Отдел рекрутинга', icon: 'people' },
|
||||||
{ id: 'it', title: 'IT', icon: 'monitor' },
|
{ id: 'it', title: 'IT/1C', icon: 'monitor' },
|
||||||
{ id: 'accounting', title: 'Бухгалтерия', icon: 'calculator' },
|
{ id: 'accounting', title: 'Бухгалтерия', icon: 'calculator' },
|
||||||
{ id: 'hr', title: 'Отдел кадров', icon: 'clipboard' },
|
{ id: 'hr', title: 'Отдел кадров', icon: 'clipboard' },
|
||||||
{ id: 'warehouse', title: 'Склад / Снабжение', icon: 'building' }
|
{ id: 'warehouse', title: 'Склад / Снабжение', icon: 'building' }
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: IT"
|
subtitle="Категория: IT/1C"
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
||||||
|
|||||||
@@ -1,45 +1,15 @@
|
|||||||
// Варианты Choice-полей — см. forms/Ticket1C/provisioning.json (единый источник правды).
|
// Варианты «Видов работ», «Организации» и «Базы 1С» в коде не хранятся — форма
|
||||||
|
// читает их из колонок списка SP (см. shared/fieldChoices). Чтобы добавить пункт,
|
||||||
export const WORK_TYPE_OPTIONS: string[] = [
|
// допишите его в параметрах столбца списка. Варианты в provisioning.json —
|
||||||
'Создание пользователя в 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',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const QUESTION_MAX = 1000;
|
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).
|
// Значения из property pane старой web part (см. живую страницу SharePoint).
|
||||||
export const PLANNER_PLAN_ID = '168WqDV8CEKkW3cL6olCRZgACwYa';
|
export const PLANNER_PLAN_ID = '168WqDV8CEKkW3cL6olCRZgACwYa';
|
||||||
export const PLANNER_BUCKET_NAME = 'Входящие неотсортированные';
|
export const PLANNER_BUCKET_NAME = 'Входящие неотсортированные';
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"Открытие даты запрета",
|
"Открытие даты запрета",
|
||||||
"Прочее",
|
"Прочее",
|
||||||
"Не работает 1С",
|
"Не работает 1С",
|
||||||
"Блокировка пользователя"
|
"Блокировка пользователя",
|
||||||
|
"Маршруты в ДО"
|
||||||
] },
|
] },
|
||||||
{ "internalName": "Organization", "title": "Организация", "type": "Choice", "required": true, "choices": [
|
{ "internalName": "Organization", "title": "Организация", "type": "Choice", "required": true, "choices": [
|
||||||
"AFL Engineering",
|
"AFL Engineering",
|
||||||
|
|||||||
@@ -1,52 +1,76 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
|
import { Alert, CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
|
||||||
import { ISectionProps } from '../model';
|
import { useFieldChoices, IFieldChoicesState } from '../../../shared/fieldChoices';
|
||||||
import { WORK_TYPE_OPTIONS, ORGANIZATION_OPTIONS, DB1C_OPTIONS, QUESTION_MAX } from '../config';
|
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);
|
// Сохранённое значение могли убрать из вариантов колонки — без этого Select
|
||||||
const ORGANIZATION_SELECT_OPTIONS = toOptions(ORGANIZATION_OPTIONS);
|
// показал бы пустое поле, и заявитель не увидел бы, что было выбрано.
|
||||||
const DB1C_SELECT_OPTIONS = toOptions(DB1C_OPTIONS);
|
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 }) => (
|
export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
|
||||||
<CollapseSection title="Заявка" icon="clipboard">
|
// Варианты — из текущей партиции: туда пишутся новые заявки.
|
||||||
<FormField label="Наименование задачи" htmlFor="TaskName" required full>
|
const listTitle = dataListTitle(FORM_ID);
|
||||||
<TextInput id="TaskName" value={data.TaskName} onChange={(v) => set('TaskName', v)} />
|
const workTypes = useFieldChoices(listTitle, 'WorkType');
|
||||||
</FormField>
|
const organizations = useFieldChoices(listTitle, 'Organization');
|
||||||
<FormField label="Виды работ" htmlFor="WorkType" required>
|
const databases = useFieldChoices(listTitle, 'DB1C');
|
||||||
<Select
|
const choicesError = workTypes.error || organizations.error || databases.error;
|
||||||
id="WorkType"
|
|
||||||
value={data.WorkType}
|
return (
|
||||||
onChange={(v) => set('WorkType', v)}
|
<CollapseSection title="Заявка" icon="clipboard">
|
||||||
options={WORK_TYPE_SELECT_OPTIONS}
|
{choicesError && <Alert variant="err">{`${CHOICES_ERROR_PREFIX}: ${choicesError}`}</Alert>}
|
||||||
placeholder="— выберите —"
|
<FormField label="Наименование задачи" htmlFor="TaskName" required full>
|
||||||
/>
|
<TextInput id="TaskName" value={data.TaskName} onChange={(v) => set('TaskName', v)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Организация" htmlFor="Organization" required>
|
<FormField label="Виды работ" htmlFor="WorkType" required>
|
||||||
<Select
|
<Select
|
||||||
id="Organization"
|
id="WorkType"
|
||||||
value={data.Organization}
|
value={data.WorkType}
|
||||||
onChange={(v) => set('Organization', v)}
|
onChange={(v) => set('WorkType', v)}
|
||||||
options={ORGANIZATION_SELECT_OPTIONS}
|
options={withCurrentValue(workTypes.options, data.WorkType)}
|
||||||
placeholder="— выберите —"
|
placeholder={choicesPlaceholder(workTypes, '— выберите —')}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="База 1С" fieldKey="DB1C" required>
|
<FormField label="Организация" htmlFor="Organization" required>
|
||||||
<MultiChoiceField
|
<Select
|
||||||
value={data.DB1C}
|
id="Organization"
|
||||||
onChange={(v) => set('DB1C', v)}
|
value={data.Organization}
|
||||||
options={DB1C_SELECT_OPTIONS}
|
onChange={(v) => set('Organization', v)}
|
||||||
placeholder="Выберите одну или несколько баз"
|
options={withCurrentValue(organizations.options, data.Organization)}
|
||||||
/>
|
placeholder={choicesPlaceholder(organizations, '— выберите —')}
|
||||||
</FormField>
|
/>
|
||||||
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
|
</FormField>
|
||||||
<TextArea
|
<FormField label="База 1С" fieldKey="DB1C" required>
|
||||||
id="Question"
|
<MultiChoiceField
|
||||||
value={data.Question}
|
value={data.DB1C}
|
||||||
onChange={(v) => set('Question', v.slice(0, QUESTION_MAX))}
|
onChange={(v) => set('DB1C', v)}
|
||||||
rows={6}
|
options={databases.options}
|
||||||
/>
|
placeholder={choicesPlaceholder(databases, 'Выберите одну или несколько баз')}
|
||||||
</FormField>
|
/>
|
||||||
</CollapseSection>
|
</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
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: IT"
|
subtitle="Категория: IT/1C"
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
listTitle={editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID)}
|
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