чат по заявке через комментарии к строке списка (#14)
Tests / frontend-test (push) Successful in 1m15s
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
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user