Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
describe('textToDocumentNode()', () => {
test('should convert single-line text to a document node', () => {
const result = textToDocumentNode('Hello world');
const result = textToDocumentNode('Hello world', '');
expect(result).toEqual({
type: 'doc',
content: [
Expand All @@ -30,26 +30,26 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
});

test('should convert multi-line text to multiple paragraphs', () => {
const result = textToDocumentNode('Line 1\nLine 2\nLine 3');
const result = textToDocumentNode('Line 1\nLine 2\nLine 3', '');
expect(result.content).toHaveLength(3);
expect(result.content[0].content).toEqual([{ type: 'text', text: 'Line 1' }]);
expect(result.content[1].content).toEqual([{ type: 'text', text: 'Line 2' }]);
expect(result.content[2].content).toEqual([{ type: 'text', text: 'Line 3' }]);
});

test('should handle empty lines as paragraphs without content key', () => {
const result = textToDocumentNode('Before\n\nAfter');
const result = textToDocumentNode('Before\n\nAfter', '');
expect(result.content).toHaveLength(3);
expect(result.content[1]).toEqual({ type: 'paragraph' });
});

test('should handle empty string', () => {
const result = textToDocumentNode('');
const result = textToDocumentNode('', '');
expect(result).toEqual({ type: 'doc', content: [] });
});

test('should parse @[id:name] mentions into mention nodes', () => {
const result = textToDocumentNode('Hello @[123:Jane Doe] how are you?');
const result = textToDocumentNode('Hello @[123:Jane Doe] how are you?', '');
expect(result.content).toHaveLength(1);
expect(result.content[0].content).toEqual([
{ type: 'text', text: 'Hello ' },
Expand All @@ -67,7 +67,7 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
});

test('should parse multiple mentions in one line', () => {
const result = textToDocumentNode('@[1:Alice] and @[2:Bob]');
const result = textToDocumentNode('@[1:Alice] and @[2:Bob]', '');
expect(result.content[0].content).toHaveLength(3);
expect(result.content[0].content[0]).toEqual({
type: 'mention',
Expand All @@ -81,11 +81,23 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
});

test('should handle mentions across multiple lines', () => {
const result = textToDocumentNode('Hi @[1:Alice]\nBye @[2:Bob]');
const result = textToDocumentNode('Hi @[1:Alice]\nBye @[2:Bob]', '');
expect(result.content).toHaveLength(2);
expect(result.content[0].content[1].type).toBe('mention');
expect(result.content[1].content[1].type).toBe('mention');
});

test('should pass authorId through to each mention node', () => {
const result = textToDocumentNode('@[1:Alice] and @[2:Bob]', '99');
expect(result.content[0].content[0]).toEqual({
type: 'mention',
attrs: { authorId: '99', mentionId: '1', mentionedUserId: '1', mentionedUserName: 'Alice' },
});
expect(result.content[0].content[2]).toEqual({
type: 'mention',
attrs: { authorId: '99', mentionId: '2', mentionedUserId: '2', mentionedUserName: 'Bob' },
});
});
});

describe('transformCommentToMessages()', () => {
Expand Down Expand Up @@ -141,13 +153,13 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
expect(messages[1].author.name).toBe('Reply User');
});

test('should use tagged_message over message and parse mentions', () => {
test('should use tagged_message over message and parse mentions with author id', () => {
const messages = transformCommentToMessages(mockComment as unknown as Comment);
expect(messages[0].message.content[0].content).toEqual([
{ type: 'text', text: 'Hello ' },
{
type: 'mention',
attrs: { authorId: '', mentionId: '456', mentionedUserId: '456', mentionedUserName: 'Jane' },
attrs: { authorId: '123', mentionId: '456', mentionedUserId: '456', mentionedUserName: 'Jane' },
},
]);
});
Expand All @@ -160,6 +172,17 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
text: 'fallback message',
});
});

test('should leave updatedAt undefined when comment has not been edited', () => {
const messages = transformCommentToMessages(mockComment as unknown as Comment);
expect(messages[0].updatedAt).toBeUndefined();
});

test('should set updatedAt to modified_at when comment has been edited', () => {
const editedComment = { ...mockComment, modified_at: '2024-01-02T00:00:00Z' };
const messages = transformCommentToMessages(editedComment as unknown as Comment);
expect(messages[0].updatedAt).toBe(new Date('2024-01-02T00:00:00Z').getTime());
});
});

describe('transformAnnotationToMessages()', () => {
Expand Down Expand Up @@ -224,6 +247,26 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
const messages = transformAnnotationToMessages(noDescription as unknown as Annotation);
expect(messages[0].message).toEqual({ type: 'doc', content: [] });
});

test('should leave updatedAt undefined when annotation has not been edited', () => {
const messages = transformAnnotationToMessages(mockAnnotation as unknown as Annotation);
expect(messages[0].updatedAt).toBeUndefined();
});

test('should set updatedAt to modified_at when annotation has been edited', () => {
const editedAnnotation = { ...mockAnnotation, modified_at: '2024-02-05T00:00:00Z' };
const messages = transformAnnotationToMessages(editedAnnotation as unknown as Annotation);
expect(messages[0].updatedAt).toBe(new Date('2024-02-05T00:00:00Z').getTime());
});

test('should set authorId on mentions to the annotation creator id', () => {
const annotationWithMention = { ...mockAnnotation, description: { message: 'cc @[789:Replier]' } };
const messages = transformAnnotationToMessages(annotationWithMention as unknown as Annotation);
const [, mentionNode] = messages[0].message.content[0].content as Array<{
attrs?: { authorId: string };
}>;
expect(mentionNode.attrs?.authorId).toBe('456');
});
});

describe('transformTaskToProps()', () => {
Expand Down Expand Up @@ -380,6 +423,36 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
const result = transformVersionToProps(version as unknown as BoxItemVersion);
expect(result.authorName).toBe('Trasher');
});

test('should prefer the action-specific user over modified_by for promotion', () => {
const version = {
...mockVersion,
action_type: 'promoted',
modified_by: { id: '300', name: 'Uploader', type: 'user' },
promoted_by: { id: '500', name: 'Promoter', type: 'user' },
version_promoted: 'promoted',
};
const result = transformVersionToProps(version as unknown as BoxItemVersion);
expect(result.authorName).toBe('Promoter');
expect(result.actionType).toBe('promote');
});

test('should derive action from version flags when action_type is missing', () => {
const trashedVersion = { ...mockVersion, action_type: undefined, trashed_at: '2024-04-02T00:00:00Z' };
expect(transformVersionToProps(trashedVersion as unknown as BoxItemVersion).actionType).toBe('delete');

const restoredVersion = { ...mockVersion, action_type: undefined, restored_at: '2024-04-02T00:00:00Z' };
expect(transformVersionToProps(restoredVersion as unknown as BoxItemVersion).actionType).toBe('restore');

const promotedVersion = { ...mockVersion, action_type: undefined, version_promoted: 'promoted' };
expect(transformVersionToProps(promotedVersion as unknown as BoxItemVersion).actionType).toBe('promote');
});

test('should default to upload when action_type is unknown and no flags are set', () => {
const version = { ...mockVersion, action_type: 'unrecognized' };
const result = transformVersionToProps(version as unknown as BoxItemVersion);
expect(result.actionType).toBe('upload');
});
});

describe('transformAppActivityToProps()', () => {
Expand Down
46 changes: 28 additions & 18 deletions src/elements/content-sidebar/activity-feed-v2/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ import {

const MENTION_REGEX = /@\[(\d+):([^\]]+)\]/g;

/**
* Parses a line of text into TipTap content nodes, converting @[id:name]
* mention markup into MentionNode elements and plain text into TextNode elements.
*/
const parseLine = (line: string): (MentionNode | TextNode)[] => {
const parseLine = (line: string, authorId: string): (MentionNode | TextNode)[] => {
const nodes: (MentionNode | TextNode)[] = [];
let lastIndex = 0;

Expand All @@ -63,7 +59,7 @@ const parseLine = (line: string): (MentionNode | TextNode)[] => {
nodes.push({
type: 'mention',
attrs: {
authorId: '',
authorId,
mentionId: userId,
mentionedUserId: userId,
mentionedUserName: userName,
Expand All @@ -81,18 +77,14 @@ const parseLine = (line: string): (MentionNode | TextNode)[] => {
return nodes;
};

/**
* Converts a tagged_message string to a TipTap DocumentNode.
* Parses @[userId:userName] mention markup into MentionNode elements.
*/
export const textToDocumentNode = (text: string): DocumentNode => {
export const textToDocumentNode = (text: string, authorId: string): DocumentNode => {
if (!text) {
return { type: 'doc', content: [] };
}

const lines = text.split('\n');
const content: ParagraphNode[] = lines.map(line => {
const nodes = parseLine(line);
const nodes = parseLine(line, authorId);
return {
type: 'paragraph' as const,
...(nodes.length > 0 ? { content: nodes } : {}),
Expand All @@ -108,6 +100,11 @@ const toUnixMs = (isoDate?: string | null): number | undefined => {
return Number.isNaN(ms) ? undefined : ms;
};

const toUpdatedAt = (createdAt: string, modifiedAt: string): number | undefined => {
if (modifiedAt === createdAt) return undefined;
return toUnixMs(modifiedAt);
};

const toUserAuthor = (user?: User | null): TextMessageAuthorType => ({
avatarUrl: user?.avatar_url,
email: user?.email ?? user?.login ?? '',
Expand All @@ -128,8 +125,9 @@ const commentToTextMessage = (comment: Comment): TextMessageType => ({
author: toUserAuthor(comment.created_by),
createdAt: toUnixMs(comment.created_at) ?? 0,
id: comment.id,
message: textToDocumentNode(comment.tagged_message || comment.message || ''),
message: textToDocumentNode(comment.tagged_message || comment.message || '', comment.created_by?.id ?? ''),
permissions: toPermissions(comment.permissions),
updatedAt: toUpdatedAt(comment.created_at, comment.modified_at),
});

export const transformCommentToMessages = (comment: Comment): TextMessageType[] => {
Expand Down Expand Up @@ -163,8 +161,9 @@ export const transformAnnotationToMessages = (annotation: Annotation): TextMessa
author: toUserAuthor(annotation.created_by),
createdAt: toUnixMs(annotation.created_at) ?? 0,
id: annotation.id,
message: textToDocumentNode(messageText),
message: textToDocumentNode(messageText, annotation.created_by?.id ?? ''),
permissions: toPermissions(annotation.permissions),
updatedAt: toUpdatedAt(annotation.created_at, annotation.modified_at),
};
const replies = (annotation.replies ?? []).map(reply => commentToTextMessage(reply));
return [root, ...replies];
Expand Down Expand Up @@ -204,7 +203,7 @@ export const transformTaskToProps = (task: TaskNew, currentUserId?: string): Tas
taskType: task.task_type === 'APPROVAL' ? TaskType.APPROVAL : TaskType.GENERAL,
});

const mapVersionActionType = (actionType?: string): VersionItemProps['actionType'] => {
const mapActionTypeString = (actionType?: string): VersionItemProps['actionType'] | undefined => {
switch (actionType) {
case 'delete':
case 'trashed':
Expand All @@ -217,15 +216,26 @@ const mapVersionActionType = (actionType?: string): VersionItemProps['actionType
return 'restore';
case 'upload':
case 'created':
default:
return 'upload';
default:
return undefined;
}
};

const getVersionAction = (version: BoxItemVersion): VersionItemProps['actionType'] => {
if (version.version_promoted) return 'promote';
if (version.restored_at) return 'restore';
if (version.trashed_at) return 'delete';
return mapActionTypeString(version.action_type) ?? 'upload';
};

const getVersionUser = (version: BoxItemVersion): User | undefined =>
version.restored_by || version.trashed_by || version.promoted_by || version.modified_by || undefined;

export const transformVersionToProps = (version: BoxItemVersion): VersionItemProps => {
const user = version.modified_by ?? version.trashed_by ?? version.restored_by ?? version.promoted_by;
const user = getVersionUser(version);
return {
actionType: mapVersionActionType(version.action_type),
actionType: getVersionAction(version),
authorName: user?.name ?? version.uploader_display_name,
avatarUrl: user?.avatar_url,
createdAt: toUnixMs(version.created_at),
Expand Down
Loading