0ecb1ded78
Tests / frontend-test (push) Successful in 1m15s
Обсуждение заявки поверх штатных комментариев SharePoint (PnPjs item.comments) — без новых колонок и провижининга, права на переписку раздаёт сам SharePoint по правам на элемент. Встроено в ApproveFeature и EditFeature (единственные входы в просмотр заявки для всех типов), поэтому доступно автору, всем согласующим (включая уже решивших и ещё не дошедших до своего шага) и исполнителям, а не только текущему шагу маршрута, как DecisionPanel. Автора комментария всегда ставит реальная учётка — отладочная подмена личности на запись в SharePoint не влияет, в UI есть предупреждение. --------- Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/14
129 lines
6.2 KiB
TypeScript
129 lines
6.2 KiB
TypeScript
import { IChatAttachment, IChatMessage } from './models/IChatMessage';
|
|
import { displayFileName } from './chatFileNames';
|
|
|
|
/**
|
|
* Часть комментария SharePoint, из которой строится сообщение чата.
|
|
*
|
|
* Объявлена своим типом, а не взята как ICommentInfo из @pnp/sp/comments:
|
|
* этот модуль намеренно НЕ знает про PnPjs (он чистый и покрыт тестами), а
|
|
* ICommentInfo структурно подходит под этот интерфейс — сервис передаёт его
|
|
* без приведения типов.
|
|
*/
|
|
export interface ICommentSource {
|
|
id: string;
|
|
text: string;
|
|
createdDate: string;
|
|
author?: { name?: string; email?: string };
|
|
}
|
|
|
|
/**
|
|
* Служебная пометка о прикреплённых файлах в КОНЦЕ текста комментария:
|
|
* Проверьте, пожалуйста, акт
|
|
* [rp-files]20260824-153012-1__акт.pdf|20260824-153012-2__скан.jpg
|
|
*
|
|
* Почему в тексте, а не отдельным хранилищем: у комментария SharePoint нет
|
|
* своих полей — только текст. Класть связь «сообщение → файлы» больше некуда,
|
|
* не заводя отдельный список и не превращая каждую загрузку чата в лишний
|
|
* запрос к библиотеке. Имена файлов складские (с префиксом), но не полные
|
|
* адреса — адрес библиотеки резолвится на лету, поэтому её можно переместить,
|
|
* и ссылки в старой переписке не сломаются.
|
|
*
|
|
* Разделитель имён — «|»: этот символ SharePoint в именах файлов запрещает
|
|
* (см. FORBIDDEN_CHARS), значит в имени он встретиться не может.
|
|
*/
|
|
const FILE_MARKER = '[rp-files]';
|
|
|
|
/** Пометка всегда последняя и всегда в одну строку — отсюда привязка к концу. */
|
|
const FILE_MARKER_PATTERN = /\s*\[rp-files\]([^\r\n]*)$/;
|
|
|
|
/**
|
|
* Собрать текст комментария из сообщения и складских имён файлов.
|
|
* Сообщение может быть пустым — тогда комментарий состоит из одной пометки
|
|
* (так выглядит отправка «только файл»).
|
|
*/
|
|
export function buildCommentText(text: string, storedFileNames: string[]): string {
|
|
const trimmed = text.trim();
|
|
if (storedFileNames.length === 0) return trimmed;
|
|
|
|
const marker = `${FILE_MARKER}${storedFileNames.join('|')}`;
|
|
return trimmed ? `${trimmed}\n${marker}` : marker;
|
|
}
|
|
|
|
function parseStoredNames(raw: string): string[] {
|
|
const found = FILE_MARKER_PATTERN.exec(raw);
|
|
if (!found) return [];
|
|
return found[1]
|
|
.split('|')
|
|
.map((name) => name.trim())
|
|
.filter((name) => !!name);
|
|
}
|
|
|
|
/** Есть ли в переписке файлы — по этому признаку сервис решает, нужен ли адрес библиотеки. */
|
|
export function hasAttachments(comments: ICommentSource[]): boolean {
|
|
return comments.some((c) => !!c.text && FILE_MARKER_PATTERN.test(c.text));
|
|
}
|
|
|
|
/**
|
|
* Дата комментария приходит строкой ISO. Если часового пояса в ней нет,
|
|
* new Date() трактует строку как МЕСТНОЕ время, а SharePoint отдаёт UTC —
|
|
* сообщения уехали бы на 5 часов (Астана — UTC+5). Поэтому при отсутствии
|
|
* пояса дописываем Z явно.
|
|
*/
|
|
function parseCommentDate(raw: string): Date {
|
|
const hasTimeZone = /(Z|[+-]\d{2}:\d{2})$/.test(raw);
|
|
return new Date(hasTimeZone ? raw : `${raw}Z`);
|
|
}
|
|
|
|
function toAttachments(storedNames: string[], folderUrl: string): IChatAttachment[] {
|
|
return storedNames.map((storedName) => ({
|
|
fileName: displayFileName(storedName),
|
|
url: folderUrl ? `${folderUrl}/${storedName}` : ''
|
|
}));
|
|
}
|
|
|
|
function toMessage(
|
|
comment: ICommentSource,
|
|
currentUserEmail: string,
|
|
folderUrl: string
|
|
): IChatMessage {
|
|
const authorEmail = comment.author?.email || '';
|
|
const authorName = comment.author?.name || authorEmail || 'Неизвестный автор';
|
|
const storedNames = parseStoredNames(comment.text);
|
|
return {
|
|
id: comment.id,
|
|
text: comment.text.replace(FILE_MARKER_PATTERN, '').trim(),
|
|
authorName,
|
|
authorEmail,
|
|
createdAt: parseCommentDate(comment.createdDate),
|
|
isOwn:
|
|
!!authorEmail &&
|
|
!!currentUserEmail &&
|
|
authorEmail.toLowerCase() === currentUserEmail.toLowerCase(),
|
|
attachments: toAttachments(storedNames, folderUrl)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Комментарии SharePoint → сообщения чата, от старых к новым (как в любой
|
|
* переписке: свежее внизу).
|
|
*
|
|
* Порядок ответа API не документирован и мы на него не опираемся — сортируем
|
|
* по дате сами. Комментарии без текста и без файлов отбрасываем: показывать
|
|
* пустой «пузырь» нечего, а в списке они появляются, если кто-то отправил
|
|
* комментарий не из портала (например, из стандартной формы SharePoint).
|
|
*
|
|
* folderUrl — папка заявки в библиотеке «Файлы чата»; пустая строка допустима,
|
|
* когда в переписке файлов нет (тогда адрес библиотеки и не запрашивался).
|
|
*/
|
|
export function toChatMessages(
|
|
comments: ICommentSource[],
|
|
currentUserEmail: string,
|
|
folderUrl: string = ''
|
|
): IChatMessage[] {
|
|
return comments
|
|
.filter((c) => !!c.text && !!c.text.trim())
|
|
.map((c) => toMessage(c, currentUserEmail, folderUrl))
|
|
.filter((m) => !!m.text || m.attachments.length > 0)
|
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
}
|