Skip to content

Commit d69cdfe

Browse files
feat(activity-feed-v2): replace stub with real adapter (#4533)
* feat(activity-feed-v2): replace stub with real @box/activity-feed adapter Wire up ActivityFeedV2 with real compound components from @box/activity-feed behind the threadedRepliesV2 feature flag. Add data transformers, FeedItemRow renderer, CSS isolation, and tighten prop types. * feat(activity-feed-v2): add drawing and highlight annotation badge support * fix(activity-feed-v2): preserve original task data, fix aria labels * fix(activity-feed-v2): debounce mentions, guard permissions, drop empty edit text - Debounce getMentionAsync in ActivitySidebar so mention lookups don't fire on every keystroke; reuse DEFAULT_COLLAB_DEBOUNCE and supersede in-flight promises with empty results. - Default Comment/Annotation permissions to {} in transformFeedItem so the non-optional TransformedFeedItem.permissions field can't be undefined at runtime. - Stop passing text: '' to onAnnotationEdit (would blank the annotation message); make text optional in the handler signature. * fix(activity-feed-v2): guard getMentionAsync against stale responses Use ElementsXhrError for the mention reject type (flow fix) and add a generation counter so in-flight responses from superseded requests cant resolve or reject the current promise with stale data. * fix(activity-feed-v2): drop broken edit wiring, remap version click args Remove the onEdit handler for comments and annotations: threaded-annotations V2 has no inline edit UI yet, and the existing wiring routed into the submit-path (updateThreadedComment / updateAnnotation) which throws when text and status are both undefined. Drop the onAnnotationEdit prop across FeedItemRow, ActivityFeedV2, and ActivitySidebar's V2 render block. Rewire onVersionClick to consume the V2 callback's { id, versionNumber } args and remap to the V1 { id, version_number } shape that downstream onVersionHistoryClick consumers expect. * fix(activity-feed-v2): address review feedback - Bump peer dep minimums to match @box/activity-feed@1.17.0 requirements (@box/blueprint-web ^14.18.0, @box/blueprint-web-assets ^4.115.4, @box/collaboration-popover ^1.61.5, @box/readable-time ^1.40.5, @box/user-selector ^1.75.5). Run yarn-deduplicate to collapse @tanstack/react-virtual to a single 3.13.24. - Cancel pending debounce and bump mention generation on ActivitySidebar unmount, and move errorCallback inside the generation check so superseded mention requests do not fire onError on a destroyed component. - Drop the broken sync getAvatarUrl wrapper in mentionContext: the vendor expects sync (id) => string but our BUIE prop is async, so the wrapper always returned empty. Leave the optional prop undefined until a caching layer exists. - Rename toPermissions() param perms to permissions, and expand the UAA acronym in file headers (public repo). * test(sidebar-file-properties): update snapshot for blueprint-web bump @box/blueprint-web 14.18.0 emits new CSS module class hashes; markup is otherwise unchanged. * test(activity-feed-v2): use getByRole and clear mocks between tests Switch ActivityFeed list-item mocks from plain divs with data-testid to semantic article elements with aria-label, so tests can use getByRole instead of getByTestId per project testing conventions. Add afterEach(jest.clearAllMocks) in both test files so mock call counts don't leak between tests. * fix(activity-feed-v2): gate row mutations on isDisabled, settle pending mention on unmount - Honor isDisabled in FeedItemRow comment/annotation branches so delete, resolve, unresolve, and reply callbacks no-op in read-only mode, matching how the editor and task button already behave - Resolve any pending getMentionAsync promise with [] in componentWillUnmount so awaiters do not hang after the sidebar closes - Narrow captured-prop test types via Partial<ThreadedAnnotationsPropsV2> / Partial<TaskItemProps> / Partial<VersionItemProps> so the tests drop per-callback (as ...) => void casts * fix(activity-feed-v2): address review feedback on a11y, avatar cache, empty posts - Key fetchAvatarUrls cache by contact.value (string id) instead of contact.id (Number(id) || 0 funneled non-numeric ids to 0) - Replace misused noActivityCommentPrompt and loading messages on the mention user selector with mentionUserSelectorRoleDescription and mentionUserSelectorLoading so screen readers announce the control's actual role and loading state - Trim-and-early-return empty or whitespace-only content in handleCommentPost and buildReplyPost to avoid the feed API's missing-item-text error path; tighten the try block so consumer callback throws propagate - Scope the ul > li padding rule to the top-level feed list by wrapping ActivityFeed.List in a display:contents div; prevents bleed into nested reply lists --------- Co-authored-by: Jackie Jou <jackiejou@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 17f8107 commit d69cdfe

16 files changed

Lines changed: 2737 additions & 46 deletions

i18n/en-US.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,10 @@ be.contentSidebar.boxSignWatermarkBlockedTooltip = This action is unavailable, b
382382
be.contentSidebar.editTask.approval.title = Modify Approval Task
383383
# modal title for when editing an existing general task
384384
be.contentSidebar.editTask.general.title = Modify General Task
385+
# Accessibility label announced while the mention user selector is loading results
386+
be.contentSidebar.mentionUserSelectorLoading = Loading users
387+
# Accessibility role description for the mention user selector input
388+
be.contentSidebar.mentionUserSelectorRoleDescription = Mention a user
385389
# Label for copy action.
386390
be.copy = Copy
387391
# Label for create action.

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,14 @@
124124
"@babel/preset-typescript": "^7.24.7",
125125
"@babel/template": "^7.24.7",
126126
"@babel/types": "^7.24.7",
127-
"@box/blueprint-web": "^14.0.2",
128-
"@box/blueprint-web-assets": "^4.111.17",
127+
"@box/activity-feed": "^1.17.0",
128+
"@box/blueprint-web": "^14.18.0",
129+
"@box/blueprint-web-assets": "^4.115.4",
129130
"@box/box-ai-agent-selector": "^1.39.21",
130131
"@box/box-ai-content-answers": "^1.43.22",
131132
"@box/box-item-type-selector": "^1.39.21",
132133
"@box/cldr-data": "^34.2.0",
134+
"@box/collaboration-popover": "^1.61.5",
133135
"@box/combobox-with-api": "^1.42.22",
134136
"@box/copy-input": "^1.40.21",
135137
"@box/content-field": "^1.40.23",
@@ -140,9 +142,11 @@
140142
"@box/metadata-filter": "^1.80.23",
141143
"@box/metadata-view": "^1.53.26",
142144
"@box/react-virtualized": "^9.22.3-rc-box.10",
145+
"@box/readable-time": "^1.40.5",
146+
"@box/threaded-annotations": "^1.83.6",
143147
"@box/types": "^2.1.8",
144148
"@box/unified-share-modal": "^2.12.4",
145-
"@box/user-selector": "^1.74.22",
149+
"@box/user-selector": "^1.75.5",
146150
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
147151
"@chromatic-com/storybook": "^4.0.1",
148152
"@commitlint/cli": "^19.8.0",
@@ -289,12 +293,14 @@
289293
"webpack-dev-server": "^5.2.3"
290294
},
291295
"peerDependencies": {
292-
"@box/blueprint-web": "^14.0.2",
293-
"@box/blueprint-web-assets": "^4.111.17",
296+
"@box/activity-feed": "^1.17.0",
297+
"@box/blueprint-web": "^14.18.0",
298+
"@box/blueprint-web-assets": "^4.115.4",
294299
"@box/box-ai-agent-selector": "^1.39.21",
295300
"@box/box-ai-content-answers": "^1.43.22",
296301
"@box/box-item-type-selector": "^1.39.21",
297302
"@box/cldr-data": ">=34.2.0",
303+
"@box/collaboration-popover": "^1.61.5",
298304
"@box/combobox-with-api": "^1.42.22",
299305
"@box/copy-input": "^1.40.21",
300306
"@box/content-field": "^1.40.23",
@@ -303,9 +309,11 @@
303309
"@box/metadata-filter": "^1.80.23",
304310
"@box/metadata-view": "^1.53.26",
305311
"@box/react-virtualized": "^9.22.3-rc-box.10",
312+
"@box/readable-time": "^1.40.5",
313+
"@box/threaded-annotations": "^1.83.6",
306314
"@box/types": "^2.1.8",
307315
"@box/unified-share-modal": "^2.12.4",
308-
"@box/user-selector": "^1.74.22",
316+
"@box/user-selector": "^1.75.5",
309317
"@hapi/address": "^2.1.4",
310318
"@tanstack/react-virtual": "^3.13.12",
311319
"axios": "^0.31.1",

scripts/jest/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ module.exports = {
2828
testMatch: ['**/__tests__/**/*.test.+(js|jsx|ts|tsx)'],
2929
testPathIgnorePatterns: ['stories.test.js$', 'stories.test.tsx$', 'stories.test.d.ts'],
3030
transformIgnorePatterns: [
31-
'node_modules/(?!(@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets|@box/metadata-editor|@box/box-ai-content-answers|@box/box-ai-agent-selector|@box/item-icon|@box/combobox-with-api|@box/tree|@box/metadata-filter|@box/metadata-view|@box/content-field|@box/types|@box/box-item-type-selector|@box/unified-share-modal|@box/user-selector|@box/copy-input)/)',
31+
'node_modules/(?!(@box/activity-feed|@box/collaboration-popover|@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets|@box/metadata-editor|@box/box-ai-content-answers|@box/box-ai-agent-selector|@box/item-icon|@box/combobox-with-api|@box/tree|@box/metadata-filter|@box/metadata-view|@box/content-field|@box/types|@box/box-item-type-selector|@box/unified-share-modal|@box/user-selector|@box/copy-input|@box/readable-time|@box/threaded-annotations)/)',
3232
],
3333
};

src/elements/content-sidebar/ActivitySidebar.js

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,22 @@ class ActivitySidebar extends React.PureComponent<Props, State> {
205205
}
206206
}
207207

208+
componentWillUnmount() {
209+
// Cancel pending debounces and bump the generation so any in-flight
210+
// response from getCollaboratorsWithQuery is ignored after unmount.
211+
[this.getMention, this.debouncedFetchMentionCollaborators].forEach(fn => {
212+
if (typeof fn?.cancel === 'function') fn.cancel();
213+
});
214+
this.mentionGeneration += 1;
215+
// Resolve any pending getMentionAsync promise with an empty result so
216+
// awaiters (e.g. ActivityFeedV2's fetchUsers) don't hang forever.
217+
if (this.pendingMentionResolve) {
218+
this.pendingMentionResolve([]);
219+
}
220+
this.pendingMentionResolve = null;
221+
this.pendingMentionReject = null;
222+
}
223+
208224
handleAnnotationDelete = ({ id, permissions }: { id: string, permissions: AnnotationPermission }) => {
209225
const { api, emitAnnotationRemoveEvent, file } = this.props;
210226

@@ -477,7 +493,8 @@ class ActivitySidebar extends React.PureComponent<Props, State> {
477493
onSuccess: ?Function,
478494
onError: ?Function,
479495
): void => {
480-
const { api, file, hasReplies, onCommentUpdate } = this.props;
496+
const { api, features, file, hasReplies, onCommentUpdate } = this.props;
497+
const isThreadedRepliesV2Enabled = isFeatureEnabled(features, 'activityFeed.threadedRepliesV2.enabled');
481498

482499
const errorCallback = (e, code) => {
483500
if (onError) {
@@ -494,7 +511,7 @@ class ActivitySidebar extends React.PureComponent<Props, State> {
494511
onCommentUpdate();
495512
};
496513

497-
if (hasReplies) {
514+
if (hasReplies || isThreadedRepliesV2Enabled) {
498515
api.getFeedAPI(false).updateThreadedComment(
499516
file,
500517
id,
@@ -991,6 +1008,54 @@ class ActivitySidebar extends React.PureComponent<Props, State> {
9911008
);
9921009
}, DEFAULT_COLLAB_DEBOUNCE);
9931010

1011+
pendingMentionResolve: ?(entries: SelectorItems<>) => void = null;
1012+
1013+
pendingMentionReject: ?(error: ElementsXhrError) => void = null;
1014+
1015+
mentionGeneration: number = 0;
1016+
1017+
// Debounced inner fetch. The generation arg guards against in-flight
1018+
// responses from superseded requests resolving/rejecting the current
1019+
// promise with stale data.
1020+
debouncedFetchMentionCollaborators = debounce((searchStr: string, generation: number) => {
1021+
const { api, file } = this.props;
1022+
api.getFileCollaboratorsAPI(false).getCollaboratorsWithQuery(
1023+
file.id,
1024+
(collaborators: { entries: SelectorItems<> }) => {
1025+
if (generation === this.mentionGeneration && this.pendingMentionResolve) {
1026+
this.pendingMentionResolve(collaborators.entries);
1027+
this.pendingMentionResolve = null;
1028+
this.pendingMentionReject = null;
1029+
}
1030+
},
1031+
(error, code, contextInfo) => {
1032+
if (generation !== this.mentionGeneration) {
1033+
return;
1034+
}
1035+
this.errorCallback(error, code, contextInfo);
1036+
if (this.pendingMentionReject) {
1037+
this.pendingMentionReject(error);
1038+
this.pendingMentionResolve = null;
1039+
this.pendingMentionReject = null;
1040+
}
1041+
},
1042+
searchStr,
1043+
);
1044+
}, DEFAULT_COLLAB_DEBOUNCE);
1045+
1046+
getMentionAsync = (searchStr: string): Promise<Array<Object>> => {
1047+
if (this.pendingMentionResolve) {
1048+
this.pendingMentionResolve([]);
1049+
}
1050+
this.mentionGeneration += 1;
1051+
const generation = this.mentionGeneration;
1052+
return new Promise((resolve, reject) => {
1053+
this.pendingMentionResolve = resolve;
1054+
this.pendingMentionReject = reject;
1055+
this.debouncedFetchMentionCollaborators(searchStr, generation);
1056+
});
1057+
};
1058+
9941059
/**
9951060
* Returns feed item based on the item id
9961061
*
@@ -1316,15 +1381,38 @@ class ActivitySidebar extends React.PureComponent<Props, State> {
13161381
const shouldUseUAA = isFeatureEnabled(features, 'activityFeed.uaaIntegration.enabled');
13171382

13181383
if (isThreadedRepliesV2Enabled) {
1384+
const label = `${elementId}${elementId === '' ? '' : '_'}${SIDEBAR_VIEW_ACTIVITY}`;
13191385
return (
1320-
<SidebarContent
1321-
className="bcs-activity"
1322-
elementId={elementId}
1323-
sidebarView={SIDEBAR_VIEW_ACTIVITY}
1324-
title={this.renderTitle()}
1386+
<div
1387+
aria-labelledby={label}
1388+
className="bcs-content"
1389+
data-testid="bcs-content"
1390+
id={`${label}-content`}
1391+
role="tabpanel"
13251392
>
1326-
<ActivityFeedV2 />
1327-
</SidebarContent>
1393+
<ActivityFeedV2
1394+
activeFeedEntryId={activeFeedEntryId}
1395+
approverSelectorContacts={approverSelectorContacts}
1396+
createTask={this.createTask}
1397+
currentUser={currentUser}
1398+
feedItems={this.getFilteredFeedItems()}
1399+
getApproverWithQuery={this.getApprover}
1400+
getAvatarUrl={this.getAvatarUrl}
1401+
getMentionAsync={this.getMentionAsync}
1402+
hasTasks={this.props.hasTasks}
1403+
isDisabled={isDisabled}
1404+
onAnnotationDelete={this.handleAnnotationDelete}
1405+
onAnnotationSelect={this.handleAnnotationSelect}
1406+
onAnnotationStatusChange={this.handleAnnotationStatusChange}
1407+
onCommentCreate={this.createComment}
1408+
onCommentDelete={this.deleteComment}
1409+
onCommentUpdate={this.updateComment}
1410+
onReplyCreate={this.createReply}
1411+
onTaskDelete={this.deleteTask}
1412+
onTaskView={onTaskView}
1413+
onVersionHistoryClick={onVersionHistoryClick}
1414+
/>
1415+
</div>
13281416
);
13291417
}
13301418

src/elements/content-sidebar/__tests__/ActivitySidebar.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FEED_ITEM_TYPE_COMMENT } from '../../../constants';
88

99
jest.mock('lodash/debounce', () => jest.fn(i => i));
1010
jest.mock('lodash/uniqueId', () => () => 'uniqueId');
11+
jest.mock('../activity-feed-v2', () => () => <div data-testid="activity-feed-adapter-v2" />);
1112

1213
const userError = 'Bad box user!';
1314

src/elements/content-sidebar/__tests__/__snapshots__/SidebarFileProperties.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ exports[`elements/content-sidebar/SidebarFileProperties render() should render r
130130
<dd>
131131
<Text>
132132
<p
133-
className="bp_text_module_textReset--1fd57 bp_text_module_breakWord--1fd57 bp_text_module_bodyDefault--1fd57 bp_text_module_textOnLightDefault--1fd57"
133+
className="bp_text_module_textReset--1bfe8 bp_text_module_breakWord--1bfe8 bp_text_module_bodyDefault--1bfe8 bp_text_module_textOnLightDefault--1bfe8"
134134
data-modern="false"
135135
data-typography="lato"
136136
>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// CSS isolation layer for ActivityFeedV2.
2+
// BUIE global styles (.bcs scope from base.scss) apply broad rules to native
3+
// elements (inputs, contenteditable, buttons, links, textareas). These leak
4+
// into Blueprint and @box/threaded-annotations components, breaking their
5+
// styling. This file resets those rules within the v2 adapter boundary.
6+
7+
.bcs-NewActivityFeed {
8+
display: flex;
9+
flex-direction: column;
10+
height: 100%;
11+
overflow: hidden auto;
12+
13+
// `display: contents` keeps the wrapper transparent to the vendor's flex layout.
14+
&-list {
15+
display: contents;
16+
}
17+
18+
&-list > ul > li {
19+
padding-right: var(--bp-space-040);
20+
padding-left: var(--bp-space-040);
21+
}
22+
23+
// Forms: BUIE sets width, padding, border, box-shadow, border-radius on
24+
// contenteditable and inputs via _forms.scss inside .bcs scope.
25+
div[contenteditable='true'],
26+
div[contenteditable='true']:hover,
27+
div[contenteditable='true']:focus {
28+
width: auto;
29+
padding: 0;
30+
color: inherit;
31+
font-size: inherit;
32+
border: none;
33+
border-radius: 0;
34+
outline: none;
35+
box-shadow: none;
36+
transition: none;
37+
}
38+
39+
// Links: BUIE forces color and text-decoration on all <a> elements
40+
// via _links.scss.
41+
a {
42+
color: inherit;
43+
text-decoration: inherit;
44+
}
45+
46+
a:focus {
47+
text-decoration: inherit;
48+
}
49+
50+
// BUIE sets pointer-events: none on <svg> inside <a> and <button>,
51+
// which breaks Blueprint icon buttons.
52+
a svg,
53+
button svg {
54+
pointer-events: auto;
55+
}
56+
}

0 commit comments

Comments
 (0)