feat: варианты полей заявки 1С из колонок SP + вид работ «Маршруты в ДО»

«Виды работ», «Организация» и «База 1С» теперь читаются из колонок списка
(общий хук shared/fieldChoices), а не из констант: новый пункт добавляется
в параметрах столбца без правки кода и деплоя. Пилот — на форме 1С, при
успехе переводим остальные формы. В стартовые варианты provisioning.json
добавлен «Маршруты в ДО».

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-25 14:37:42 +05:00
parent d539cdbfd2
commit e9b0df4822
6 changed files with 162 additions and 88 deletions
@@ -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;
}