Skip to content

Commit dc169ca

Browse files
bfoxx1906jlandisBoxtjiang-box
authored andcommitted
feat(timestamped-comments): enabling time stamped video comments (box#4228)
* feat(timestamped-comments): feature enabled timesetamp support * feat(timestamped-comments): updating stories and adding toggle form * feat(timestamped-comments): fixing flow errors * feat(timestamped-comments): removing unnecessary prop * feat(timestamped-comments): removing formatting * feat(timestamped-comments): removing async modifier * feat(timestamped-comments): removing unnecssary import * feat(timestamped-comments): passing down file prop to BaseComment * feat(metadata-instance-editor): remove ai agent selector split and fi… (box#4226) * feat(metadata-instance-editor): remove ai agent selector split and fix styling * fix: tests * fix: tests * feat: use new box ai agent selector with alignment * feat(metadata-view): Implement metadata sidepanel (box#4230) * feat(metadata-view): Implement metadata sidepanel * feat(metadata-view): resolve comments * feat(metadata-view): resolve comments * feat(metadata-view): resolve nits * feat(timestamped-comments): addressing PR feedback * feat(timestamped-comments): addressing PR comments * feat(timestamped-comments): addressing PR feedback * feat(timestamped-comments): addressing PR feedback * feat(timestamped-comments): fixing flow error * feat(timestamped-comments): addressing PR feedback * feat(timestamped-comments): fixing unit tests --------- Co-authored-by: jlandisBox <[email protected]> Co-authored-by: Jerry Jiang <[email protected]>
1 parent 156f010 commit dc169ca

File tree

16 files changed

+318
-105
lines changed

16 files changed

+318
-105
lines changed

i18n/en-US.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ be.contentSidebar.activityFeed.commentForm.commentCancel = Cancel
218218
be.contentSidebar.activityFeed.commentForm.commentLabel = Write a comment
219219
# Text for post button
220220
be.contentSidebar.activityFeed.commentForm.commentPost = Post
221+
# Label for toggle to add video timestamp to comment
222+
be.contentSidebar.activityFeed.commentForm.commentTimestampLabel = Comment with timestamp
221223
# Placeholder for comment input
222224
be.contentSidebar.activityFeed.commentForm.commentWrite = Write a comment
223225
# Show original button for showing original comment

src/components/form-elements/draft-js-mention-selector/DraftJSMentionSelector.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import DraftMentionItem from './DraftMentionItem';
88
import FormInput from '../form/FormInput';
99
import * as messages from '../input-messages';
1010
import type { SelectorItems } from '../../../common/types/core';
11+
import Toggle from '../../toggle/Toggle';
1112

1213
/**
1314
* Scans a Draft ContentBlock for entity ranges, so they can be annotated
@@ -46,6 +47,7 @@ type Props = {
4647
placeholder?: string,
4748
selectorRow?: React.Element<any>,
4849
startMentionMessage?: React.Node,
50+
timestampLabel?: string | null,
4951
validateOnBlur?: boolean,
5052
};
5153

@@ -148,10 +150,7 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
148150
}
149151

150152
isEditorStateEmpty(editorState: EditorState): boolean {
151-
const text = editorState
152-
.getCurrentContent()
153-
.getPlainText()
154-
.trim();
153+
const text = editorState.getCurrentContent().getPlainText().trim();
155154
const lastChangeType = editorState.getLastChangeType();
156155

157156
return text.length === 0 && lastChangeType === null;
@@ -166,10 +165,7 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
166165

167166
// manually check for content length if isRequired is true
168167
const editorState: EditorState = internalEditorState || externalEditorState;
169-
const { length } = editorState
170-
.getCurrentContent()
171-
.getPlainText()
172-
.trim();
168+
const { length } = editorState.getCurrentContent().getPlainText().trim();
173169

174170
if (isRequired && !length) {
175171
return messages.valueMissing();
@@ -261,6 +257,7 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
261257
selectorRow,
262258
startMentionMessage,
263259
onReturn,
260+
timestampLabel,
264261
} = this.props;
265262
const { contacts, internalEditorState, error } = this.state;
266263
const { handleBlur, handleChange, handleFocus } = this;
@@ -294,6 +291,10 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
294291
selectorRow={selectorRow}
295292
startMentionMessage={startMentionMessage}
296293
/>
294+
295+
{isRequired && timestampLabel && (
296+
<Toggle className="bcs-CommentTimestamp-toggle" label={timestampLabel} onChange={noop} />
297+
)}
297298
</FormInput>
298299
</div>
299300
);

src/components/form-elements/draft-js-mention-selector/__tests__/DraftJSMentionSelector.test.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ describe('bcomponents/form-elements/draft-js-mention-selector/DraftJSMentionSele
2626

2727
expect(wrapper.find('FormInput').length).toBe(1);
2828
});
29+
test('should toggle the time stamp if isRequired and timestampedCommentsEnabled is true', () => {
30+
const wrapper = shallow(
31+
<DraftJSMentionSelector {...requiredProps} isRequired={true} timestampLabel={'Toggle Timestamp'} />,
32+
);
33+
expect(wrapper.find('Toggle').length).toEqual(1);
34+
});
35+
36+
test('should not toggle the time stamp if isRequired is false', () => {
37+
const wrapper = shallow(
38+
<DraftJSMentionSelector {...requiredProps} isRequired={false} timestampLabel={'Toggle Timestamp'} />,
39+
);
40+
expect(wrapper.find('Toggle').length).toEqual(0);
41+
});
42+
43+
test('should not toggle the time stamp if timeStampLabel is undefined', () => {
44+
const wrapper = shallow(
45+
<DraftJSMentionSelector {...requiredProps} isRequired={true} timestampLabel={undefined} />,
46+
);
47+
expect(wrapper.find('Toggle').length).toEqual(0);
48+
});
2949
});
3050

3151
describe('getDerivedStateFromProps()', () => {
@@ -252,10 +272,7 @@ describe('bcomponents/form-elements/draft-js-mention-selector/DraftJSMentionSele
252272
});
253273
} else {
254274
test('should not call checkValidity when called', () => {
255-
sandbox
256-
.mock(instance)
257-
.expects('checkValidity')
258-
.never();
275+
sandbox.mock(instance).expects('checkValidity').never();
259276
});
260277
}
261278
});
@@ -323,17 +340,11 @@ describe('bcomponents/form-elements/draft-js-mention-selector/DraftJSMentionSele
323340

324341
if (isTouched) {
325342
test('should update state', () => {
326-
sandbox
327-
.mock(instance)
328-
.expects('setState')
329-
.withArgs({ error: err });
343+
sandbox.mock(instance).expects('setState').withArgs({ error: err });
330344
});
331345
} else {
332346
test('should not update state', () => {
333-
sandbox
334-
.mock(instance)
335-
.expects('setState')
336-
.never();
347+
sandbox.mock(instance).expects('setState').never();
337348
});
338349
}
339350
});

src/elements/content-sidebar/activity-feed/activity-feed/ActiveState.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type {
3131
FeedItems,
3232
FeedItemStatus,
3333
} from '../../../../common/types/feed';
34-
import type { SelectorItems, User } from '../../../../common/types/core';
34+
import type { SelectorItems, User, BoxItem } from '../../../../common/types/core';
3535
import type { GetAvatarUrlCallback, GetProfileUrlCallback } from '../../../common/flowTypes';
3636
import type { Translations } from '../../flowTypes';
3737

@@ -49,6 +49,7 @@ type Props = {
4949
approverSelectorContacts?: SelectorItems<>,
5050
currentFileVersionId: string,
5151
currentUser?: User,
52+
file: BoxItem,
5253
getApproverWithQuery?: Function,
5354
getAvatarUrl: GetAvatarUrlCallback,
5455
getMentionWithQuery?: Function,
@@ -94,6 +95,7 @@ const ActiveState = ({
9495
approverSelectorContacts,
9596
currentFileVersionId,
9697
currentUser,
98+
file,
9799
getApproverWithQuery,
98100
getAvatarUrl,
99101
getMentionWithQuery,
@@ -137,17 +139,19 @@ const ActiveState = ({
137139
const onReplyDeleteHandler = (parentId: string) => (options: { id: string, permissions: BoxCommentPermission }) => {
138140
onReplyDelete({ ...options, parentId });
139141
};
140-
const onReplyUpdateHandler = (parentId: string) => (
141-
id: string,
142-
text: string,
143-
status?: FeedItemStatus,
144-
hasMention?: boolean,
145-
permissions: BoxCommentPermission,
146-
onSuccess: ?Function,
147-
onError: ?Function,
148-
) => {
149-
onReplyUpdate(id, parentId, text, permissions, onSuccess, onError);
150-
};
142+
const onReplyUpdateHandler =
143+
(parentId: string) =>
144+
(
145+
id: string,
146+
text: string,
147+
status?: FeedItemStatus,
148+
hasMention?: boolean,
149+
permissions: BoxCommentPermission,
150+
onSuccess: ?Function,
151+
onError: ?Function,
152+
) => {
153+
onReplyUpdate(id, parentId, text, permissions, onSuccess, onError);
154+
};
151155
const onShowRepliesHandler = (id: string, type: CommentFeedItemType) => () => {
152156
onShowReplies(id, type);
153157
};
@@ -178,6 +182,7 @@ const ActiveState = ({
178182
...item,
179183
...replyProps,
180184
currentUser,
185+
file,
181186
getAvatarUrl,
182187
getMentionWithQuery,
183188
getUserProfileUrl,
@@ -213,6 +218,7 @@ const ActiveState = ({
213218
// $FlowFixMe
214219
<BaseComment
215220
{...commentAndAnnotationCommonProps}
221+
file={file}
216222
onDelete={onCommentDelete}
217223
onCommentEdit={onCommentEdit}
218224
onReplyCreate={reply => onReplyCreate(item.id, FEED_ITEM_TYPE_COMMENT, reply)}

src/elements/content-sidebar/activity-feed/activity-feed/ActivityFeed.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ class ActivityFeed extends React.Component<ActivityFeedProps, State> {
395395
approverSelectorContacts={approverSelectorContacts}
396396
currentFileVersionId={currentFileVersionId}
397397
currentUser={currentUser}
398+
file={file}
398399
getApproverWithQuery={getApproverWithQuery}
399400
getAvatarUrl={getAvatarUrl}
400401
getMentionWithQuery={getMentionWithQuery}
@@ -462,6 +463,7 @@ class ActivityFeed extends React.Component<ActivityFeedProps, State> {
462463
'bcs-is-disabled': isDisabled,
463464
})}
464465
createComment={hasCommentPermission ? this.onCommentCreate : noop}
466+
file={file}
465467
getMentionWithQuery={getMentionWithQuery}
466468
isOpen={isInputOpen}
467469
// $FlowFixMe

src/elements/content-sidebar/activity-feed/activity-feed/__tests__/ActivityFeed.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ describe('elements/content-sidebar/ActivityFeed/activity-feed/ActivityFeed', ()
439439
});
440440

441441
const instance = wrapper.instance();
442-
const commentForm = wrapper.find('CommentForm').first();
442+
const commentForm = wrapper.find('ForwardRef(withFeatureConsumer(CommentForm))').first();
443443

444444
instance.commentFormFocusHandler();
445445
expect(wrapper.state('isInputOpen')).toBe(true);

src/elements/content-sidebar/activity-feed/annotations/AnnotationActivity.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ACTIVITY_TARGETS } from '../../../common/interactionTargets';
2121
import { COMMENT_STATUS_RESOLVED, PLACEHOLDER_USER } from '../../../../constants';
2222
import type { Annotation, AnnotationPermission, FeedItemStatus } from '../../../../common/types/feed';
2323
import type { GetAvatarUrlCallback, GetProfileUrlCallback } from '../../../common/flowTypes';
24-
import type { SelectorItems, User } from '../../../../common/types/core';
24+
import type { SelectorItems, User, BoxItem } from '../../../../common/types/core';
2525

2626
import IconAnnotation from '../../../../icons/two-toned/IconAnnotation';
2727

@@ -31,6 +31,7 @@ import type { OnAnnotationEdit, OnAnnotationStatusChange } from '../comment/type
3131

3232
type Props = {
3333
currentUser?: User,
34+
file?: BoxItem,
3435
getAvatarUrl: GetAvatarUrlCallback,
3536
getMentionWithQuery?: (searchStr: string) => void,
3637
getUserProfileUrl?: GetProfileUrlCallback,
@@ -47,6 +48,7 @@ type Props = {
4748
const AnnotationActivity = ({
4849
currentUser,
4950
item,
51+
file,
5052
getAvatarUrl,
5153
getMentionWithQuery,
5254
getUserProfileUrl,
@@ -172,6 +174,7 @@ const AnnotationActivity = ({
172174
<CommentForm
173175
className="bcs-AnnotationActivity-editor"
174176
entityId={id}
177+
file={file}
175178
getAvatarUrl={getAvatarUrl}
176179
getMentionWithQuery={getMentionWithQuery}
177180
isEditing={isEditing}

src/elements/content-sidebar/activity-feed/annotations/__tests__/AnnotationActivity.test.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,13 @@ describe('elements/content-sidebar/ActivityFeed/annotations/AnnotationActivity',
109109

110110
const wrapper = getWrapper({ ...mockActivity, ...activity });
111111

112-
wrapper
113-
.find(AnnotationActivityMenu)
114-
.dive()
115-
.simulate('click');
116-
wrapper
117-
.find(AnnotationActivityMenu)
118-
.dive()
119-
.find('MenuItem')
120-
.simulate('click');
121-
expect(wrapper.exists('CommentForm')).toBe(true);
112+
wrapper.find(AnnotationActivityMenu).dive().simulate('click');
113+
wrapper.find(AnnotationActivityMenu).dive().find('MenuItem').simulate('click');
114+
expect(wrapper.exists('ForwardRef(withFeatureConsumer(CommentForm))')).toBe(true);
122115

123116
// Firing the onCancel prop will remove the CommentForm
124-
wrapper
125-
.find('CommentForm')
126-
.dive()
127-
.props()
128-
.onCancel();
129-
expect(wrapper.exists('CommentForm')).toBe(false);
117+
wrapper.find('ForwardRef(withFeatureConsumer(CommentForm))').props().onCancel();
118+
expect(wrapper.exists('ForwardRef(withFeatureConsumer(CommentForm))')).toBe(false);
130119
});
131120

132121
test('should correctly render annotation activity of another file version', () => {

src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,22 @@ import DraftJSMentionSelector, {
1717
} from '../../../../components/form-elements/draft-js-mention-selector';
1818
import Form from '../../../../components/form-elements/form/Form';
1919
import Media from '../../../../components/media';
20+
import { withFeatureConsumer, getFeatureConfig } from '../../../common/feature-checking';
21+
// $FlowFixMe
22+
import { FILE_EXTENSIONS } from '../../../common/item/constants';
23+
import type { FeatureConfig } from '../../../common/feature-checking/flowTypes';
2024
import messages from './messages';
2125
import type { GetAvatarUrlCallback } from '../../../common/flowTypes';
22-
import type { SelectorItems, User } from '../../../../common/types/core';
23-
26+
import type { SelectorItems, User, BoxItem } from '../../../../common/types/core';
2427
import './CommentForm.scss';
2528

26-
type Props = {
29+
export type CommentFormProps = {
2730
className: string,
2831
contactsLoaded?: boolean,
2932
createComment?: Function,
3033
entityId?: string,
34+
features?: FeatureConfig,
35+
file?: BoxItem,
3136
getAvatarUrl?: GetAvatarUrlCallback,
3237
getMentionWithQuery?: Function,
3338
intl: IntlShape,
@@ -55,17 +60,18 @@ type State = {
5560
commentEditorState: any,
5661
};
5762

58-
class CommentForm extends React.Component<Props, State> {
63+
class CommentForm extends React.Component<CommentFormProps, State> {
5964
static defaultProps = {
6065
isOpen: false,
6166
shouldFocusOnOpen: false,
67+
timestampedCommentsEnabled: false,
6268
};
6369

6470
state = {
6571
commentEditorState: getEditorState(this.props.shouldFocusOnOpen, this.props.tagged_message),
6672
};
6773

68-
componentDidUpdate({ isOpen: prevIsOpen }: Props): void {
74+
componentDidUpdate({ isOpen: prevIsOpen }: CommentFormProps): void {
6975
const { isOpen } = this.props;
7076

7177
if (isOpen !== prevIsOpen && !isOpen) {
@@ -130,7 +136,18 @@ class CommentForm extends React.Component<Props, State> {
130136
getAvatarUrl,
131137
showTip = true,
132138
placeholder = formatMessage(messages.commentWrite),
139+
features = {},
140+
file,
133141
} = this.props;
142+
143+
// Get feature configuration from context
144+
const timestampCommentsConfig = getFeatureConfig(features, 'activityFeed.timestampedComments');
145+
146+
// Use feature config to determine if time stamped comments are enabled
147+
const istimestampedCommentsEnabled = timestampCommentsConfig?.enabled === true;
148+
const isVideo = FILE_EXTENSIONS.video.includes(file?.extension);
149+
const allowVideoTimeStamps = isVideo && istimestampedCommentsEnabled;
150+
const timestampLabel = allowVideoTimeStamps ? formatMessage(messages.commentTimestampLabel) : undefined;
134151
const { commentEditorState } = this.state;
135152
const inputContainerClassNames = classNames('bcs-CommentForm', className, {
136153
'bcs-is-open': isOpen,
@@ -156,6 +173,7 @@ class CommentForm extends React.Component<Props, State> {
156173
isRequired={isOpen}
157174
name="commentText"
158175
label={formatMessage(messages.commentLabel)}
176+
timestampLabel={timestampLabel}
159177
description={formatMessage(messages.atMentionTipDescription)}
160178
onChange={this.onMentionSelectorChangeHandler}
161179
onFocus={onFocus}
@@ -179,4 +197,4 @@ class CommentForm extends React.Component<Props, State> {
179197

180198
// For testing only
181199
export { CommentForm as CommentFormUnwrapped };
182-
export default injectIntl(CommentForm);
200+
export default withFeatureConsumer(injectIntl(CommentForm));

src/elements/content-sidebar/activity-feed/comment-form/CommentForm.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
}
3636
}
3737

38+
.bcs-CommentTimestamp-toggle {
39+
margin-top: $bdl-grid-unit * 2.5;
40+
}
41+
3842
.bcs-CommentForm-tip {
3943
margin-top: 10px;
4044
color: $bdl-gray-65;

0 commit comments

Comments
 (0)