чат по заявке через комментарии к строке списка (#14)
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:
2026-08-24 11:37:34 +00:00
parent a146877327
commit 0ecb1ded78
19 changed files with 1357 additions and 5 deletions
@@ -0,0 +1,156 @@
import {
ICommentSource,
buildCommentText,
hasAttachments,
toChatMessages
} from './chatMessages';
const ME = 'ivan@tnsu.kz';
function comment(over: Partial<ICommentSource>): ICommentSource {
return {
id: '1',
text: 'Сообщение',
createdDate: '2026-08-21T09:00:00Z',
author: { name: 'Иванов Иван', email: ME },
...over
};
}
describe('toChatMessages', () => {
it('сортирует от старых к новым — свежие в чате внизу', () => {
const messages = toChatMessages(
[
comment({ id: '2', createdDate: '2026-08-21T10:00:00Z', text: 'второе' }),
comment({ id: '1', createdDate: '2026-08-21T09:00:00Z', text: 'первое' })
],
ME
);
expect(messages.map((m) => m.text)).toEqual(['первое', 'второе']);
});
it('свои сообщения помечаются по email автора, регистр не важен', () => {
const messages = toChatMessages(
[
comment({ id: '1', author: { name: 'Иванов Иван', email: 'IVAN@TNSU.KZ' } }),
comment({ id: '2', author: { name: 'Петров Пётр', email: 'petr@tnsu.kz' } })
],
ME
);
expect(messages[0].isOwn).toBe(true);
expect(messages[1].isOwn).toBe(false);
});
it('без текущего пользователя своих сообщений нет (а не все свои)', () => {
const messages = toChatMessages([comment({})], '');
expect(messages[0].isOwn).toBe(false);
});
it('пустые комментарии отбрасываются — пустой «пузырь» показывать нечего', () => {
const messages = toChatMessages(
[comment({ id: '1', text: '' }), comment({ id: '2', text: ' ' }), comment({ id: '3' })],
ME
);
expect(messages).toHaveLength(1);
expect(messages[0].id).toBe('3');
});
it('дата без часового пояса читается как UTC, а не как местное время', () => {
const withZone = toChatMessages([comment({ createdDate: '2026-08-21T09:00:00Z' })], ME);
const withoutZone = toChatMessages([comment({ createdDate: '2026-08-21T09:00:00' })], ME);
expect(withoutZone[0].createdAt.getTime()).toBe(withZone[0].createdAt.getTime());
});
it('автор без ФИО подписывается email, без email — заглушкой', () => {
const messages = toChatMessages(
[
comment({ id: '1', author: { email: 'petr@tnsu.kz' } }),
comment({ id: '2', author: undefined })
],
ME
);
expect(messages[0].authorName).toBe('petr@tnsu.kz');
expect(messages[1].authorName).toBe('Неизвестный автор');
});
});
describe('файлы в сообщении', () => {
const FOLDER = '/sites/requests/Файлы чата/hr-vacation-12';
const FILE = '20260824-153012-1__акт.pdf';
const SECOND_FILE = '20260824-153012-2__скан.jpg';
it('пометка о файлах не показывается в тексте сообщения', () => {
const messages = toChatMessages(
[comment({ text: `Посмотрите акт\n[rp-files]${FILE}` })],
ME,
FOLDER
);
expect(messages[0].text).toBe('Посмотрите акт');
});
it('файл получает исходное имя и ссылку в папке заявки', () => {
const messages = toChatMessages(
[comment({ text: `[rp-files]${FILE}|${SECOND_FILE}` })],
ME,
FOLDER
);
expect(messages[0].text).toBe('');
expect(messages[0].attachments).toEqual([
{ fileName: 'акт.pdf', url: `${FOLDER}/${FILE}` },
{ fileName: 'скан.jpg', url: `${FOLDER}/${SECOND_FILE}` }
]);
});
it('сообщение из одних файлов не отбрасывается как пустое', () => {
const messages = toChatMessages([comment({ text: `[rp-files]${FILE}` })], ME, FOLDER);
expect(messages).toHaveLength(1);
});
it('пометка переживает перевод строки в формате Windows', () => {
const messages = toChatMessages([comment({ text: `Текст\r\n[rp-files]${FILE}` })], ME, FOLDER);
expect(messages[0].text).toBe('Текст');
expect(messages[0].attachments).toHaveLength(1);
});
it('обычное сообщение остаётся без вложений', () => {
const messages = toChatMessages([comment({ text: 'Просто текст' })], ME);
expect(messages[0].attachments).toEqual([]);
});
it('hasAttachments отличает переписку с файлами от переписки без них', () => {
expect(hasAttachments([comment({ text: 'Просто текст' })])).toBe(false);
expect(hasAttachments([comment({ text: `[rp-files]${FILE}` })])).toBe(true);
});
});
describe('buildCommentText', () => {
it('без файлов отдаёт очищенный текст', () => {
expect(buildCommentText(' Привет ', [])).toBe('Привет');
});
it('с файлами дописывает пометку отдельной строкой', () => {
expect(buildCommentText('Смотрите', ['a__акт.pdf', 'b__скан.jpg'])).toBe(
'Смотрите\n[rp-files]a__акт.pdf|b__скан.jpg'
);
});
it('без текста остаётся одна пометка — отправка «только файл»', () => {
expect(buildCommentText('', ['a__акт.pdf'])).toBe('[rp-files]a__акт.pdf');
});
it('собранный текст читается обратно без потерь', () => {
const raw = buildCommentText('Смотрите акт', ['20260824-153012-1__акт.pdf']);
const messages = toChatMessages([comment({ text: raw })], ME, '/f');
expect(messages[0].text).toBe('Смотрите акт');
expect(messages[0].attachments[0].fileName).toBe('акт.pdf');
});
});