Skip to content

Commit e495547

Browse files
committed
Add UserReview
1 parent 8d15b12 commit e495547

17 files changed

Lines changed: 235 additions & 118 deletions
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
6+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
7+
<script crossorigin="anonymous" src="/test-harness.js"></script>
8+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
9+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
10+
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
11+
</head>
12+
<body>
13+
<main id="webchat" style="position: relative"></main>
14+
<script type="module">
15+
run(async function () {
16+
const {
17+
React: { createElement },
18+
ReactDOM: { render },
19+
WebChat: { FluentThemeProvider, ReactWebChat, testIds }
20+
} = window; // Imports in UMD fashion.
21+
22+
const { directLine, store } = testHelpers.createDirectLineEmulator();
23+
24+
const styleOptions = {
25+
feedbackActionsPlacement: 'activity-actions'
26+
};
27+
28+
const { searchParams } = new URL(location);
29+
const webChatElement = createElement(ReactWebChat, { directLine, store, styleOptions });
30+
31+
render(
32+
searchParams.get('theme') === 'fluent'
33+
? createElement(FluentThemeProvider, {}, webChatElement)
34+
: webChatElement,
35+
document.getElementById('webchat')
36+
);
37+
38+
await pageConditions.uiConnected();
39+
40+
await directLine.emulateIncomingActivity({
41+
entities: [
42+
{
43+
'@context': 'https://schema.org',
44+
'@id': '',
45+
'@type': 'Message',
46+
type: 'https://schema.org/Message',
47+
keywords: ['AllowCopy'],
48+
potentialAction: [
49+
{
50+
'@type': 'LikeAction',
51+
actionStatus: 'PotentialActionStatus',
52+
result: {
53+
'@type': 'UserReview',
54+
description:
55+
'Enim elit veniam enim nostrud aliqua reprehenderit anim incididunt laboris consectetur in culpa.'
56+
},
57+
target: {
58+
'@type': 'EntryPoint',
59+
urlTemplate: 'ms-directline://postback?interaction=like'
60+
}
61+
},
62+
{
63+
'@type': 'DislikeAction',
64+
actionStatus: 'PotentialActionStatus',
65+
result: {
66+
'@type': 'UserReview',
67+
description: 'Mollit amet duis consectetur sit.',
68+
'reviewBody-input': {
69+
'@type': 'PropertyValueSpecification',
70+
valueMinLength: 3,
71+
valueName: 'reason'
72+
}
73+
},
74+
target: {
75+
'@type': 'EntryPoint',
76+
urlTemplate: 'ms-directline://postback?interaction=dislike{&reason}'
77+
}
78+
}
79+
]
80+
}
81+
],
82+
text: `Irure nisi sit incididunt commodo enim ut.`,
83+
type: 'message'
84+
});
85+
86+
await pageConditions.numActivitiesShown(1);
87+
88+
// THEN: It should match the snapshot.
89+
await host.snapshot('local');
90+
91+
// WHEN: The like button is clicked.
92+
await host.click(pageElements.allByTestId(testIds.feedbackButton)[0]);
93+
94+
// THEN: It should match the snapshot.
95+
await host.snapshot('local');
96+
97+
// WHEN: The dislike button is clicked.
98+
await host.click(pageElements.allByTestId(testIds.feedbackButton)[1]);
99+
100+
// THEN: It should match the snapshot.
101+
await host.snapshot('local');
102+
});
103+
</script>
104+
</body>
105+
</html>
12.6 KB
Loading
27.7 KB
Loading
25.5 KB
Loading

packages/component/src/ActivityFeedback/ActivityFeedback.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
import classNames from 'classnames';
2-
import React, { memo, useCallback, type FormEventHandler, type KeyboardEventHandler } from 'react';
2+
import React, { memo, useCallback, useMemo, type FormEventHandler, type KeyboardEventHandler } from 'react';
33
import { Extract, wrapWith } from 'react-wrap-with';
44
import { useRefFrom } from 'use-ref-from';
55

66
import useStyleSet from '../hooks/useStyleSet';
7-
import FeedbackLoopWithMessage from './private/FeedbackLoopWithMessage';
8-
import FeedbackLoopWithoutMessage from './private/FeedbackLoopWithoutMessage';
7+
import FeedbackForm from './private/FeedbackForm';
8+
import FeedbackVoteButtonBar from './private/FeedbackVoteButtonBar';
9+
import isActionRequireReview from './private/isActionRequireReview';
910
import ActivityFeedbackComposer from './providers/ActivityFeedbackComposer';
1011
import useActivityFeedbackHooks from './providers/useActivityFeedbackHooks';
1112

1213
function InternalActivityFeedback() {
13-
const {
14-
useActions,
15-
useFeedbackText,
16-
useFocusFeedbackButton,
17-
useSelectedActions,
18-
useShouldShowFeedbackForm,
19-
useSubmit
20-
} = useActivityFeedbackHooks();
14+
const { useActions, useFeedbackText, useFocusFeedbackButton, useHasSubmitted, useSelectedAction, useSubmit } =
15+
useActivityFeedbackHooks();
2116

22-
const [actions] = useActions();
23-
const [{ feedbackForm }] = useStyleSet();
2417
const [_, setFeedbackText] = useFeedbackText();
25-
const [selectedAction, setSelectedAction] = useSelectedActions();
26-
const [shouldShowFeedbackForm] = useShouldShowFeedbackForm();
18+
const [{ feedbackForm }] = useStyleSet();
19+
const [actions] = useActions();
20+
const [hasSubmitted] = useHasSubmitted();
21+
const [selectedAction, setSelectedAction] = useSelectedAction();
2722
const focusFeedbackButton = useFocusFeedbackButton();
2823
const submit = useSubmit();
2924

25+
const firstActionRequireReview = useMemo(() => actions.find(isActionRequireReview), [actions]);
3026
const selectedActionRef = useRefFrom(selectedAction);
31-
const shouldShowFeedbackFormRef = useRefFrom(shouldShowFeedbackForm);
3227

3328
const handleReset = useCallback<FormEventHandler<HTMLFormElement>>(() => {
3429
focusFeedbackButton(selectedActionRef.current);
@@ -50,12 +45,18 @@ function InternalActivityFeedback() {
5045
event => {
5146
// ESCAPE key should clear the feedback form and unselect like/dislike as they are radio button.
5247
// In non-form mode, the like/dislike are actions, so they should not be unselected.
53-
if (event.key === 'Escape' && selectedActionRef.current && shouldShowFeedbackFormRef.current) {
48+
if (event.key === 'Escape' && isActionRequireReview(selectedActionRef.current)) {
5449
event.stopPropagation();
5550
event.currentTarget.reset();
5651
}
5752
},
58-
[selectedActionRef, shouldShowFeedbackFormRef]
53+
[selectedActionRef]
54+
);
55+
56+
// Hide feedback form if feedback has already been submitted or it does not require UserReview.
57+
const isExpanded = useMemo(
58+
() => !hasSubmitted && selectedAction?.result?.['@type'] === 'UserReview',
59+
[hasSubmitted, selectedAction]
5960
);
6061

6162
return (
@@ -66,7 +67,12 @@ function InternalActivityFeedback() {
6667
onReset={handleReset}
6768
onSubmit={handleSubmit}
6869
>
69-
{shouldShowFeedbackForm ? <FeedbackLoopWithMessage /> : <FeedbackLoopWithoutMessage />}
70+
<FeedbackVoteButtonBar
71+
// If one of the action requires review, use radio button for all.
72+
buttonAs={firstActionRequireReview ? 'radio' : 'button'}
73+
/>
74+
{/* We put the form outside of the container to let it wrap to next line instead of keeping it the same line as the like/dislike buttons. */}
75+
{isExpanded && <FeedbackForm />}
7076
</form>
7177
)
7278
);

packages/component/src/ActivityFeedback/private/FeedbackForm.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@ import Markdownable from '../../Attachment/Text/private/Markdownable';
55
import testIds from '../../testIds';
66
import useActivityFeedbackHooks from '../providers/useActivityFeedbackHooks';
77
import FeedbackTextArea from './FeedbackTextArea';
8-
import getDisclaimer from './getDisclaimer';
8+
import getDisclaimerFromReviewAction from './getDisclaimerFromReviewAction';
99

1010
const { useLocalizer } = hooks;
1111

1212
function FeedbackForm() {
13-
const { useActivity, useFeedbackText } = useActivityFeedbackHooks();
13+
const { useFeedbackText, useSelectedAction } = useActivityFeedbackHooks();
1414

15-
const [activity] = useActivity();
15+
const [selectedAction] = useSelectedAction();
1616
const [hasFocus, setHasFocus] = useState(false);
1717
const [userFeedback, setUserFeedback] = useFeedbackText();
1818
const feedbackTextAreaRef = useRef<HTMLTextAreaElement>(null);
1919
const localize = useLocalizer();
2020

21-
const disclaimer = getDisclaimer(activity);
21+
const disclaimer = getDisclaimerFromReviewAction(selectedAction);
2222

2323
const handleMessageChange: FormEventHandler<HTMLTextAreaElement> = useCallback(
2424
({ currentTarget: { value } }) => setUserFeedback(value),

packages/component/src/ActivityFeedback/private/FeedbackLoopWithMessage.tsx

Lines changed: 0 additions & 25 deletions
This file was deleted.

packages/component/src/ActivityFeedback/private/FeedbackLoopWithoutMessage.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/component/src/ActivityFeedback/private/FeedbackVoteButton.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { custom, literal, object, optional, pipe, readonly, safeParse, union, ty
88
import { useListenToActivityFeedbackFocus } from '../providers/private/FocusPropagation';
99
import useActivityFeedbackHooks from '../providers/useActivityFeedbackHooks';
1010
import ThumbButton from './ThumbButton';
11+
import canActionResubmit from './canActionResubmit';
12+
import isActionRequireReview from './isActionRequireReview';
1113

1214
const { useLocalizer, useStyleOptions } = hooks;
1315

@@ -36,16 +38,13 @@ const feedbackVoteButtonPropsSchema = pipe(
3638
type FeedbackVoteButtonProps = InferInput<typeof feedbackVoteButtonPropsSchema>;
3739

3840
function FeedbackVoteButton(props: FeedbackVoteButtonProps) {
39-
const { useHasSubmitted, useShouldAllowResubmit, useShouldShowFeedbackForm, useSelectedActions } =
40-
useActivityFeedbackHooks();
41+
const { useHasSubmitted, useSelectedAction: useSelectedActions } = useActivityFeedbackHooks();
4142

4243
const { action, as } = validateProps(feedbackVoteButtonPropsSchema, props);
4344

4445
const [{ feedbackActionsPlacement }] = useStyleOptions();
4546
const [hasSubmitted] = useHasSubmitted();
4647
const [selectedAction, setSelectedAction] = useSelectedActions();
47-
const [shouldAllowResubmit] = useShouldAllowResubmit();
48-
const [shouldShowFeedbackForm] = useShouldShowFeedbackForm();
4948
const actionRef = useRefFrom(action);
5049
const buttonRef = useRef<HTMLInputElement>(null);
5150
const direction = useMemo(() => {
@@ -67,7 +66,7 @@ function FeedbackVoteButton(props: FeedbackVoteButtonProps) {
6766
() => setSelectedAction(actionRef.current === selectedActionRef.current ? undefined : actionRef.current),
6867
[actionRef, selectedActionRef, setSelectedAction]
6968
);
70-
const disabled = hasSubmitted && !shouldAllowResubmit;
69+
const disabled = hasSubmitted && !canActionResubmit(action);
7170

7271
useListenToActivityFeedbackFocus(
7372
useCallback(target => target === actionRef.current && buttonRef.current?.focus(), [actionRef])
@@ -81,7 +80,7 @@ function FeedbackVoteButton(props: FeedbackVoteButtonProps) {
8180
onClick={handleClick}
8281
pressed={selectedAction === action}
8382
ref={buttonRef}
84-
size={shouldShowFeedbackForm || feedbackActionsPlacement === 'activity-actions' ? 'large' : 'small'}
83+
size={isActionRequireReview(action) || feedbackActionsPlacement === 'activity-actions' ? 'large' : 'small'}
8584
submitted={hasSubmitted}
8685
title={disabled ? localize('VOTE_COMPLETE_ALT') : undefined}
8786
/>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type OrgSchemaAction } from 'botframework-webchat-core';
2+
import isActionRequireReview from './isActionRequireReview';
3+
4+
export default function canActionResubmit(action: OrgSchemaAction | undefined): boolean {
5+
return !isActionRequireReview(action);
6+
}

0 commit comments

Comments
 (0)