diff --git a/src/elements/content-sidebar/ActivitySidebar.js b/src/elements/content-sidebar/ActivitySidebar.js index 81eb17ea49..aff4faf1a0 100644 --- a/src/elements/content-sidebar/ActivitySidebar.js +++ b/src/elements/content-sidebar/ActivitySidebar.js @@ -18,6 +18,7 @@ import AddTaskButton from './AddTaskButton'; import API from '../../api'; import messages from '../common/messages'; import SidebarContent from './SidebarContent'; +import { getParsedFileActivitiesResponse } from '../../api/Feed'; import { WithAnnotatorContextProps, withAnnotatorContext } from '../common/annotator-context'; import { EVENT_DATA_READY, EVENT_JS_READY } from '../common/logger/constants'; import { getBadUserError } from '../../utils/error'; @@ -39,6 +40,11 @@ import { FEED_ITEM_TYPE_COMMENT, FEED_ITEM_TYPE_TASK, FEED_ITEM_TYPE_VERSION, + FILE_ACTIVITY_TYPE_ANNOTATION, + FILE_ACTIVITY_TYPE_APP_ACTIVITY, + FILE_ACTIVITY_TYPE_COMMENT, + FILE_ACTIVITY_TYPE_TASK, + FILE_ACTIVITY_TYPE_VERSION, ORIGIN_ACTIVITY_SIDEBAR, SIDEBAR_VIEW_ACTIVITY, TASK_COMPLETION_RULE_ALL, @@ -1108,26 +1114,61 @@ class ActivitySidebar extends React.PureComponent { handleItemsFiltered = (status?: ActivityFilterItemType) => { const { onFilterChange } = this.props; - - this.setState({ feedItemsStatusFilter: status }); + this.setState({ feedItemsStatusFilter: status }, this.getFilteredFeedItems); onFilterChange(status); }; + getFilters = (status?: ActivityFilterItemType) => { + let filters = [ + FILE_ACTIVITY_TYPE_ANNOTATION, + FILE_ACTIVITY_TYPE_APP_ACTIVITY, + FILE_ACTIVITY_TYPE_COMMENT, + FILE_ACTIVITY_TYPE_TASK, + FILE_ACTIVITY_TYPE_VERSION, + ]; + + if (status === ACTIVITY_FILTER_OPTION_UNRESOLVED || status === ACTIVITY_FILTER_OPTION_RESOLVED) { + filters = [FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_VERSION]; + } + + if (status === FILE_ACTIVITY_TYPE_TASK) { + filters = [FILE_ACTIVITY_TYPE_TASK, FILE_ACTIVITY_TYPE_VERSION]; + } + + return filters; + }; + getFilteredFeedItems = (): FeedItems | typeof undefined => { const { feedItems, feedItemsStatusFilter } = this.state; - if (!feedItems || !feedItemsStatusFilter || feedItemsStatusFilter === ACTIVITY_FILTER_OPTION_ALL) { - return feedItems; - } - // Filter is completed on two properties (status and type) because filtering on comments (resolved vs. unresolved) - // requires looking at item status to see if it is open or resolved. To filter all tasks, we need to look at the - // item type. Item type is also used to keep versions in the feed. Task also has a status but it's status will be - // "NOT_STARTED" or "COMPLETED" so it will not conflict with comment's status. - return feedItems.filter(item => { - return ( - item.status === feedItemsStatusFilter || - item.type === FEED_ITEM_TYPE_VERSION || - item.type === feedItemsStatusFilter + const { api, file } = this.props; + const { permissions = {} } = file; + const feedAPI = api.getFeedAPI(false); + feedAPI.file = file; + + // Clear the feedItems before fetching new data to trigger loading indicator + this.setState({ feedItems: undefined }); + + // Get the file activity filters + const filters = this.getFilters(feedItemsStatusFilter); + + feedAPI.fetchFileActivities(permissions, filters, true).then(items => { + const parsedFeedItems = getParsedFileActivitiesResponse(items); + + if (!feedItems || !feedItemsStatusFilter || feedItemsStatusFilter === ACTIVITY_FILTER_OPTION_ALL) { + return this.setState({ feedItems: parsedFeedItems }); + } + + // Filter is completed on two properties (status and type) because filtering on comments (resolved vs. unresolved) + // requires looking at item status to see if it is open or resolved. Item type is also used to keep versions in the feed. + // Task also has a status but it's status will be "NOT_STARTED" or "COMPLETED" so it will not conflict with comment's status. + const filteredActivity = parsedFeedItems.filter( + item => + item.status === feedItemsStatusFilter || + item.type === FEED_ITEM_TYPE_VERSION || + item.type === feedItemsStatusFilter, ); + + return this.setState({ feedItems: filteredActivity }); }); }; @@ -1234,7 +1275,13 @@ class ActivitySidebar extends React.PureComponent { getUserProfileUrl, onTaskView, } = this.props; - const { activityFeedError, approverSelectorContacts, contactsLoaded, mentionSelectorContacts } = this.state; + const { + activityFeedError, + approverSelectorContacts, + contactsLoaded, + feedItems, + mentionSelectorContacts, + } = this.state; const isNewThreadedRepliesEnabled = isFeatureEnabled(features, 'activityFeed.newThreadedReplies.enabled'); const shouldUseUAA = isFeatureEnabled(features, 'activityFeed.uaaIntegration.enabled'); @@ -1253,7 +1300,7 @@ class ActivitySidebar extends React.PureComponent { approverSelectorContacts={approverSelectorContacts} currentUser={currentUser} currentUserError={currentUserError} - feedItems={this.getFilteredFeedItems()} + feedItems={feedItems} file={file} getApproverWithQuery={this.getApprover} getAvatarUrl={this.getAvatarUrl} diff --git a/src/elements/content-sidebar/__tests__/ActivitySidebar.test.js b/src/elements/content-sidebar/__tests__/ActivitySidebar.test.js index 64aa0f3f71..37abc4abae 100644 --- a/src/elements/content-sidebar/__tests__/ActivitySidebar.test.js +++ b/src/elements/content-sidebar/__tests__/ActivitySidebar.test.js @@ -4,14 +4,18 @@ import cloneDeep from 'lodash/cloneDeep'; import { ActivitySidebarComponent, activityFeedInlineError } from '../ActivitySidebar'; import ActivitySidebarFilter from '../ActivitySidebarFilter'; import { filterableActivityFeedItems, formattedReplies } from '../fixtures'; -import { FEED_ITEM_TYPE_COMMENT } from '../../../constants'; +import { + FEED_ITEM_TYPE_COMMENT, + FILE_ACTIVITY_TYPE_ANNOTATION, + FILE_ACTIVITY_TYPE_APP_ACTIVITY, + FILE_ACTIVITY_TYPE_COMMENT, + FILE_ACTIVITY_TYPE_TASK, + FILE_ACTIVITY_TYPE_VERSION, +} from '../../../constants'; jest.mock('lodash/debounce', () => jest.fn(i => i)); jest.mock('lodash/uniqueId', () => () => 'uniqueId'); -// const mockReplace = jest.fn(); -// jest.mock('lodash/uniqueId', () => () => 'uniqueId'); - const userError = 'Bad box user!'; describe('elements/content-sidebar/ActivitySidebar', () => { @@ -26,6 +30,7 @@ describe('elements/content-sidebar/ActivitySidebar', () => { deleteTaskNew: jest.fn(), deleteThreadedComment: jest.fn(), feedItems: jest.fn(), + fetchFileActivities: jest.fn(), fetchReplies: jest.fn(), fetchThreadedComment: jest.fn(), updateAnnotation: jest.fn(), @@ -1500,35 +1505,58 @@ describe('elements/content-sidebar/ActivitySidebar', () => { }); }); + describe('getFilters()', () => { + test.each` + status | expected + ${undefined} | ${[FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_APP_ACTIVITY, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_TASK, FILE_ACTIVITY_TYPE_VERSION]} + ${'open'} | ${[FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_VERSION]} + ${'resolved'} | ${[FILE_ACTIVITY_TYPE_ANNOTATION, FILE_ACTIVITY_TYPE_COMMENT, FILE_ACTIVITY_TYPE_VERSION]} + ${'task'} | ${[FILE_ACTIVITY_TYPE_TASK, FILE_ACTIVITY_TYPE_VERSION]} + `('should return the correct filters when status equals to $status', ({ status, expected }) => { + const wrapper = getWrapper(); + const instance = wrapper.instance(); + + instance.setState({ + feedItemsStatusFilter: status, + }); + + expect(instance.getFilters(status)).toMatchObject(expected); + }); + }); + describe('getFilteredFeedItems()', () => { const { - annotationOpen: expectedAnnotationOpen, - annotationResolved: expectedAnnotationResolved, - commentOpen: expectedCommentOpen, - commentResolved: expectedCommentResolved, - taskItem: expectedTaskItem, - versionItem: expectedVersionItem, + annotationOpen, + annotationResolved, + commentOpen, + commentResolved, + taskItem, + versionItem, } = filterableActivityFeedItems; + const expectedAnnotationOpen = cloneDeep(annotationOpen.source.annotation); + const expectedAnnotationResolved = cloneDeep(annotationResolved.source.annotation); + const expectedCommentOpen = cloneDeep(commentOpen.source.comment); + const expectedCommentResolved = cloneDeep(commentResolved.source.comment); + const expectedTaskItem = { assigned_to: cloneDeep(taskItem.source.task.assigned_to) }; + const expectedVersionItem = cloneDeep(versionItem.source.versions); + test.each` - status | expected - ${undefined} | ${[expectedAnnotationOpen, expectedAnnotationResolved, expectedCommentOpen, expectedCommentResolved, expectedTaskItem, expectedVersionItem]} - ${'open'} | ${[expectedAnnotationOpen, expectedCommentOpen, expectedVersionItem]} - ${'resolved'} | ${[expectedAnnotationResolved, expectedCommentResolved, expectedVersionItem]} - ${'task'} | ${[expectedTaskItem, expectedVersionItem]} + status | entries | expected + ${undefined} | ${[annotationOpen, annotationResolved, commentOpen, commentResolved, taskItem, versionItem]} | ${[expectedVersionItem, expectedTaskItem, expectedCommentResolved, expectedCommentOpen, expectedAnnotationResolved, expectedAnnotationOpen]} + ${'open'} | ${[annotationOpen, annotationResolved, commentOpen, commentResolved, versionItem]} | ${[expectedVersionItem, expectedCommentOpen, expectedAnnotationOpen]} + ${'resolved'} | ${[annotationOpen, annotationResolved, commentOpen, commentResolved, versionItem]} | ${[expectedVersionItem, expectedCommentResolved, expectedAnnotationResolved]} + ${'task'} | ${[taskItem, versionItem]} | ${[expectedVersionItem, expectedTaskItem]} `( - 'should filter feed items of type "comment" or "annotation" based on status equal to $status', - ({ status, expected }) => { - const { - annotationOpen, - annotationResolved, - commentOpen, - commentResolved, - taskItem, - versionItem, - } = cloneDeep(filterableActivityFeedItems); + 'should correctly filter feed items when status is equal to $status', + async ({ status, entries, expected }) => { const wrapper = getWrapper(); const instance = wrapper.instance(); + + api.getFeedAPI().fetchFileActivities = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({ entries })); + instance.setState({ feedItems: [ annotationOpen, @@ -1542,7 +1570,10 @@ describe('elements/content-sidebar/ActivitySidebar', () => { instance.setState({ feedItemsStatusFilter: status, }); - expect(instance.getFilteredFeedItems()).toMatchObject(expected); + + await instance.getFilteredFeedItems(); + + expect(instance.state.feedItems).toMatchObject(expected); }, ); }); @@ -1561,8 +1592,12 @@ describe('elements/content-sidebar/ActivitySidebar', () => { const wrapper = getWrapper({ onFilterChange: mockOnFilterChange }); const instance = wrapper.instance(); instance.setState = jest.fn(); + instance.getFilteredFeedItems = jest.fn(); instance.handleItemsFiltered(status); - expect(instance.setState).toBeCalledWith({ feedItemsStatusFilter: expected }); + expect(instance.setState).toBeCalledWith( + { feedItemsStatusFilter: expected }, + instance.getFilteredFeedItems, + ); expect(mockOnFilterChange).toBeCalledWith(expected); }, ); diff --git a/src/elements/content-sidebar/fixtures.js b/src/elements/content-sidebar/fixtures.js index 4cbcf3286a..0243a3800d 100644 --- a/src/elements/content-sidebar/fixtures.js +++ b/src/elements/content-sidebar/fixtures.js @@ -1,5 +1,6 @@ // @flow import { + FILE_ACTIVITY_TYPE_VERSION, FEED_ITEM_TYPE_ANNOTATION, FEED_ITEM_TYPE_COMMENT, FEED_ITEM_TYPE_TASK, @@ -9,174 +10,202 @@ import { export const filterableActivityFeedItems = { annotationOpen: { - type: FEED_ITEM_TYPE_ANNOTATION, - id: 'open2', - tagged_message: '', - message: 'test', - created_at: '2022-07-26T09:08:20-07:00', - created_by: { - type: 'user', - id: '6187936317', - name: 'Jhon', - login: 'jdoe@box.com', - }, - modified_at: '2022-07-26T09:08:20-07:00', - permissions: { - can_delete: true, - can_edit: true, - can_reply: true, + activity_type: FEED_ITEM_TYPE_ANNOTATION, + source: { + annotation: { + created_at: '2022-07-26T09:08:20-07:00', + created_by: { + id: '6187936317', + name: 'Jhon', + login: 'jdoe@box.com', + type: 'user', + }, + description: { + message: 'test', + }, + id: 'open2', + modified_at: '2022-07-26T09:08:20-07:00', + permissions: { + can_delete: true, + can_edit: true, + can_reply: true, + }, + status: 'open', + type: FEED_ITEM_TYPE_ANNOTATION, + }, }, - status: 'open', }, annotationResolved: { - type: FEED_ITEM_TYPE_ANNOTATION, - id: 'open2', - tagged_message: '', - message: 'test', - created_at: '2022-07-26T09:08:20-07:00', - created_by: { - type: 'user', - id: '6187936317', - name: 'Jhon', - login: 'jdoe@box.com', - }, - modified_at: '2022-07-26T09:08:20-07:00', - permissions: { - can_delete: true, - can_edit: true, - can_reply: true, + activity_type: FEED_ITEM_TYPE_ANNOTATION, + source: { + annotation: { + created_at: '2022-07-26T09:08:20-07:00', + created_by: { + type: 'user', + id: '6187936317', + name: 'Jhon', + login: 'jdoe@box.com', + }, + description: { + message: 'test', + }, + id: 'open2', + modified_at: '2022-07-26T09:08:20-07:00', + permissions: { + can_delete: true, + can_edit: true, + can_reply: true, + }, + status: 'resolved', + type: FEED_ITEM_TYPE_ANNOTATION, + }, }, - status: 'resolved', }, commentOpen: { - type: FEED_ITEM_TYPE_COMMENT, - id: 'open1', - tagged_message: '', - message: 'test', - created_at: '2022-07-26T09:08:20-07:00', - created_by: { - type: 'user', - id: '6187936317', - name: 'Jhon', - login: 'jdoe@box.com', - }, - modified_at: '2022-07-26T09:08:20-07:00', - permissions: { - can_delete: true, - can_edit: true, - can_reply: true, + activity_type: FEED_ITEM_TYPE_COMMENT, + source: { + comment: { + created_at: '2022-07-26T09:08:20-07:00', + created_by: { + type: 'user', + id: '6187936317', + name: 'Jhon', + login: 'jdoe@box.com', + }, + id: 'open1', + message: 'test', + modified_at: '2022-07-26T09:08:20-07:00', + permissions: { + can_delete: true, + can_edit: true, + can_reply: true, + }, + status: 'open', + type: FEED_ITEM_TYPE_COMMENT, + }, }, - status: 'open', }, commentResolved: { - type: FEED_ITEM_TYPE_COMMENT, - id: 'open1', - tagged_message: '', - message: 'test', - created_at: '2022-07-26T09:08:20-07:00', - created_by: { - type: 'user', - id: '6187936317', - name: 'Jhon', - login: 'jdoe@box.com', - }, - modified_at: '2022-07-26T09:08:20-07:00', - permissions: { - can_delete: true, - can_edit: true, - can_reply: true, + activity_type: FEED_ITEM_TYPE_COMMENT, + source: { + comment: { + created_at: '2022-07-26T09:08:20-07:00', + created_by: { + type: 'user', + id: '6187936317', + name: 'Jhon', + login: 'jdoe@box.com', + }, + id: 'open1', + message: 'test', + modified_at: '2022-07-26T09:08:20-07:00', + permissions: { + can_delete: true, + can_edit: true, + can_reply: true, + }, + status: 'resolved', + type: FEED_ITEM_TYPE_COMMENT, + }, }, - status: 'resolved', }, taskItem: { - created_by: { - type: 'task_collaborator', - target: { name: 'Jay-Z', id: '100' }, - id: '000', - role: 'CREATOR', - status: TASK_NEW_NOT_STARTED, - }, - created_at: '2019-01-01', - due_at: '2019-02-02', - id: '0', - name: 'task message', - type: FEED_ITEM_TYPE_TASK, - assigned_to: { - entries: [ - { - id: '1', - target: { name: 'Beyonce', id: '2', avatar_url: '', type: 'user' }, - status: TASK_NEW_NOT_STARTED, - permissions: { - can_delete: false, - can_update: false, - }, - role: 'ASSIGNEE', + activity_type: FEED_ITEM_TYPE_TASK, + source: { + task: { + assigned_to: { + entries: [ + { + id: '1', + target: { name: 'Beyonce', id: '2', avatar_url: '', type: 'user' }, + status: TASK_NEW_NOT_STARTED, + permissions: { + can_delete: false, + can_update: false, + }, + role: 'ASSIGNEE', + type: 'task_collaborator', + }, + ], + limit: 10, + next_marker: null, + }, + created_at: '2019-01-01', + created_by: { type: 'task_collaborator', + target: { name: 'Jay-Z', id: '100' }, + id: '000', + role: 'CREATOR', + status: TASK_NEW_NOT_STARTED, }, - ], - limit: 10, - next_marker: null, - }, - permissions: { - can_update: false, - can_delete: false, - can_create_task_collaborator: false, - can_create_task_link: false, - }, - task_links: { - entries: [ - { - id: '03', - type: 'task_link', - target: { - type: 'file', - id: '4', - }, - permissions: { - can_delete: false, - can_update: false, - }, - }, - ], - limit: 1, - next_marker: null, + due_at: '2019-02-02', + id: '0', + permissions: { + can_update: false, + can_delete: false, + can_create_task_collaborator: false, + can_create_task_link: false, + }, + status: TASK_NEW_NOT_STARTED, + task_links: { + entries: [ + { + id: '03', + type: 'task_link', + target: { + type: 'file', + id: '4', + }, + permissions: { + can_delete: false, + can_update: false, + }, + }, + ], + limit: 1, + next_marker: null, + }, + type: FEED_ITEM_TYPE_TASK, + }, }, - status: TASK_NEW_NOT_STARTED, }, versionItem: { - type: FEED_ITEM_TYPE_VERSION, - id: '1060370614597', - authenticated_download_url: 'https://someurl.com', - created_at: '2022-07-06T03:01:28-07:00', - extension: 'boxnote', - is_download_available: true, - modified_at: '2022-07-06T03:01:28-07:00', - modified_by: { - type: 'user', - id: '18836063940', - name: 'John Doe', - login: 'jdoe@box.com', - }, - name: 'Title', - permissions: { - can_download: true, - can_preview: true, - can_delete: true, - can_annotate: false, - can_view_annotations_all: true, - can_view_annotations_self: true, - can_create_annotations: false, - can_view_annotations: false, + activity_type: FILE_ACTIVITY_TYPE_VERSION, + source: { + versions: { + id: '1060370614597', + authenticated_download_url: 'https://someurl.com', + created_at: '2022-07-06T03:01:28-07:00', + extension: 'boxnote', + is_download_available: true, + modified_at: '2022-07-06T03:01:28-07:00', + modified_by: { + type: 'user', + id: '18836063940', + name: 'John Doe', + login: 'jdoe@box.com', + }, + name: 'Title', + permissions: { + can_download: true, + can_preview: true, + can_delete: true, + can_annotate: false, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: false, + can_view_annotations: false, + }, + restored_at: null, + restored_by: null, + retention: null, + size: 4159, + trashed_at: null, + trashed_by: null, + type: FEED_ITEM_TYPE_VERSION, + uploader_display_name: 'John Doe', + }, }, - restored_at: null, - restored_by: null, - retention: null, - size: 4159, - trashed_at: null, - trashed_by: null, - uploader_display_name: 'John Doe', - version_number: '27', }, };