Bring back RatingButton
Summary: Removed previously during redesign by mistake Reviewed By: antonk52 Differential Revision: D51347837 fbshipit-source-id: d634ad4c8983271a3936f458cabb63f006a4bb0a
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ace3626938
commit
e225d9e1c3
@@ -72,6 +72,7 @@ import {TroubleshootingGuide} from './appinspect/fb-stubs/TroubleshootingGuide';
|
|||||||
import {FlipperDevTools} from '../chrome/FlipperDevTools';
|
import {FlipperDevTools} from '../chrome/FlipperDevTools';
|
||||||
import {TroubleshootingHub} from '../chrome/TroubleshootingHub';
|
import {TroubleshootingHub} from '../chrome/TroubleshootingHub';
|
||||||
import {Notification} from './notification/Notification';
|
import {Notification} from './notification/Notification';
|
||||||
|
import {SandyRatingButton} from './RatingButton';
|
||||||
|
|
||||||
export const Navbar = withTrackingScope(function Navbar() {
|
export const Navbar = withTrackingScope(function Navbar() {
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +105,7 @@ export const Navbar = withTrackingScope(function Navbar() {
|
|||||||
|
|
||||||
<NotificationButton />
|
<NotificationButton />
|
||||||
<TroubleshootMenu />
|
<TroubleshootMenu />
|
||||||
|
<SandyRatingButton />
|
||||||
<ExtrasMenu />
|
<ExtrasMenu />
|
||||||
<RightSidebarToggleButton />
|
<RightSidebarToggleButton />
|
||||||
{getRenderHostInstance().serverConfig.environmentInfo
|
{getRenderHostInstance().serverConfig.environmentInfo
|
||||||
|
|||||||
349
desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx
Normal file
349
desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
Component,
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {styled, Input, Link, FlexColumn, FlexRow} from '../ui';
|
||||||
|
import * as UserFeedback from '../fb-stubs/UserFeedback';
|
||||||
|
import {FeedbackPrompt} from '../fb-stubs/UserFeedback';
|
||||||
|
import {StarOutlined} from '@ant-design/icons';
|
||||||
|
import {Button, Checkbox, Popover, Rate} from 'antd';
|
||||||
|
import {currentUser} from '../fb-stubs/user';
|
||||||
|
import {theme, useValue} from 'flipper-plugin';
|
||||||
|
import {reportPlatformFailures} from 'flipper-common';
|
||||||
|
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||||
|
import {NavbarButton} from './Navbar';
|
||||||
|
|
||||||
|
type NextAction = 'select-rating' | 'leave-comment' | 'finished';
|
||||||
|
|
||||||
|
class PredefinedComment extends Component<{
|
||||||
|
comment: string;
|
||||||
|
selected: boolean;
|
||||||
|
onClick: (_: unknown) => unknown;
|
||||||
|
}> {
|
||||||
|
static Container = styled.div<{selected: boolean}>((props) => {
|
||||||
|
return {
|
||||||
|
border: '1px solid #f2f3f5',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 24,
|
||||||
|
backgroundColor: props.selected ? '#ecf3ff' : '#f2f3f5',
|
||||||
|
marginBottom: 4,
|
||||||
|
marginRight: 4,
|
||||||
|
padding: '4px 8px',
|
||||||
|
color: props.selected ? 'rgb(56, 88, 152)' : undefined,
|
||||||
|
borderColor: props.selected ? '#3578e5' : undefined,
|
||||||
|
':hover': {
|
||||||
|
borderColor: '#3578e5',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PredefinedComment.Container
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
selected={this.props.selected}>
|
||||||
|
{this.props.comment}
|
||||||
|
</PredefinedComment.Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row = styled(FlexRow)({
|
||||||
|
marginTop: 5,
|
||||||
|
marginBottom: 5,
|
||||||
|
justifyContent: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#9a9a9a',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DismissRow = styled(Row)({
|
||||||
|
marginBottom: 0,
|
||||||
|
marginTop: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const DismissButton = styled.span({
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Spacer = styled(FlexColumn)({
|
||||||
|
flexGrow: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
function dismissRow(dismiss: () => void) {
|
||||||
|
return (
|
||||||
|
<DismissRow key="dismiss">
|
||||||
|
<Spacer />
|
||||||
|
<DismissButton onClick={dismiss}>Dismiss</DismissButton>
|
||||||
|
<Spacer />
|
||||||
|
</DismissRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackComponentState = {
|
||||||
|
rating: number | null;
|
||||||
|
hoveredRating: number;
|
||||||
|
allowUserInfoSharing: boolean;
|
||||||
|
nextAction: NextAction;
|
||||||
|
predefinedComments: {[key: string]: boolean};
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FeedbackComponent extends Component<
|
||||||
|
{
|
||||||
|
submitRating: (rating: number) => void;
|
||||||
|
submitComment: (
|
||||||
|
rating: number,
|
||||||
|
comment: string,
|
||||||
|
selectedPredefinedComments: Array<string>,
|
||||||
|
allowUserInfoSharing: boolean,
|
||||||
|
) => void;
|
||||||
|
close: () => void;
|
||||||
|
dismiss: () => void;
|
||||||
|
promptData: FeedbackPrompt;
|
||||||
|
},
|
||||||
|
FeedbackComponentState
|
||||||
|
> {
|
||||||
|
state: FeedbackComponentState = {
|
||||||
|
rating: null,
|
||||||
|
hoveredRating: 0,
|
||||||
|
allowUserInfoSharing: true,
|
||||||
|
nextAction: 'select-rating' as NextAction,
|
||||||
|
predefinedComments: this.props.promptData.predefinedComments.reduce(
|
||||||
|
(acc, cv) => ({...acc, [cv]: false}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
comment: '',
|
||||||
|
};
|
||||||
|
onSubmitRating(newRating: number) {
|
||||||
|
const nextAction = newRating <= 2 ? 'leave-comment' : 'finished';
|
||||||
|
this.setState({rating: newRating, nextAction: nextAction});
|
||||||
|
this.props.submitRating(newRating);
|
||||||
|
if (nextAction === 'finished') {
|
||||||
|
setTimeout(this.props.close, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommentSubmitted(comment: string) {
|
||||||
|
this.setState({nextAction: 'finished'});
|
||||||
|
const selectedPredefinedComments: Array<string> = Object.entries(
|
||||||
|
this.state.predefinedComments,
|
||||||
|
)
|
||||||
|
.map((x) => ({comment: x[0], enabled: x[1]}))
|
||||||
|
.filter((x) => x.enabled)
|
||||||
|
.map((x) => x.comment);
|
||||||
|
const currentRating = this.state.rating;
|
||||||
|
if (currentRating) {
|
||||||
|
this.props.submitComment(
|
||||||
|
currentRating,
|
||||||
|
comment,
|
||||||
|
selectedPredefinedComments,
|
||||||
|
this.state.allowUserInfoSharing,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Illegal state: Submitting comment with no rating set.');
|
||||||
|
}
|
||||||
|
setTimeout(this.props.close, 1000);
|
||||||
|
}
|
||||||
|
onAllowUserSharingChanged(allowed: boolean) {
|
||||||
|
this.setState({allowUserInfoSharing: allowed});
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let body: Array<ReactElement>;
|
||||||
|
switch (this.state.nextAction) {
|
||||||
|
case 'select-rating':
|
||||||
|
body = [
|
||||||
|
<Row key="bodyText">{this.props.promptData.bodyText}</Row>,
|
||||||
|
<Row key="stars" style={{margin: 'auto'}}>
|
||||||
|
<Rate onChange={(newRating) => this.onSubmitRating(newRating)} />
|
||||||
|
</Row>,
|
||||||
|
dismissRow(this.props.dismiss),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'leave-comment':
|
||||||
|
const predefinedComments = Object.entries(
|
||||||
|
this.state.predefinedComments,
|
||||||
|
).map((c: [string, unknown], idx: number) => (
|
||||||
|
<PredefinedComment
|
||||||
|
key={idx}
|
||||||
|
comment={c[0]}
|
||||||
|
selected={Boolean(c[1])}
|
||||||
|
onClick={() =>
|
||||||
|
this.setState({
|
||||||
|
predefinedComments: {
|
||||||
|
...this.state.predefinedComments,
|
||||||
|
[c[0]]: !c[1],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
body = [
|
||||||
|
<Row key="predefinedComments">{predefinedComments}</Row>,
|
||||||
|
<Row key="inputRow">
|
||||||
|
<Input
|
||||||
|
style={{height: 30, width: '100%'}}
|
||||||
|
placeholder={this.props.promptData.commentPlaceholder}
|
||||||
|
value={this.state.comment}
|
||||||
|
onChange={(e) => this.setState({comment: e.target.value})}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
e.key == 'Enter' && this.onCommentSubmitted(this.state.comment)
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Row>,
|
||||||
|
<Row key="contactCheckbox">
|
||||||
|
<Checkbox
|
||||||
|
checked={this.state.allowUserInfoSharing}
|
||||||
|
onChange={(e) => this.onAllowUserSharingChanged(e.target.checked)}
|
||||||
|
/>
|
||||||
|
{'Tool owner can contact me '}
|
||||||
|
</Row>,
|
||||||
|
<Row key="submit">
|
||||||
|
<Button onClick={() => this.onCommentSubmitted(this.state.comment)}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Row>,
|
||||||
|
dismissRow(this.props.dismiss),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'finished':
|
||||||
|
body = [
|
||||||
|
<Row key="thanks">
|
||||||
|
Thanks for the feedback! You can now help
|
||||||
|
<Link href="https://www.internalfb.com/intern/papercuts/?application=flipper">
|
||||||
|
prioritize bugs and features for Flipper in Papercuts
|
||||||
|
</Link>
|
||||||
|
</Row>,
|
||||||
|
dismissRow(this.props.dismiss),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
console.error('Illegal state: nextAction: ' + this.state.nextAction);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FlexColumn
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
paddingLeft: 20,
|
||||||
|
paddingRight: 20,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
}}>
|
||||||
|
<Row key="heading" style={{color: theme.primaryColor, fontSize: 20}}>
|
||||||
|
{this.state.nextAction === 'finished'
|
||||||
|
? this.props.promptData.postSubmitHeading
|
||||||
|
: this.props.promptData.preSubmitHeading}
|
||||||
|
</Row>
|
||||||
|
{body}
|
||||||
|
</FlexColumn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SandyRatingButton() {
|
||||||
|
const [promptData, setPromptData] =
|
||||||
|
useState<UserFeedback.FeedbackPrompt | null>(null);
|
||||||
|
const [isShown, setIsShown] = useState(false);
|
||||||
|
const [hasTriggered, setHasTriggered] = useState(false);
|
||||||
|
const sessionId = getRenderHostInstance().serverConfig.sessionId;
|
||||||
|
const loggedIn = useValue(currentUser());
|
||||||
|
|
||||||
|
const triggerPopover = useCallback(() => {
|
||||||
|
if (!hasTriggered) {
|
||||||
|
setIsShown(true);
|
||||||
|
setHasTriggered(true);
|
||||||
|
}
|
||||||
|
}, [hasTriggered]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
getRenderHostInstance().GK('flipper_enable_star_ratiings') &&
|
||||||
|
!hasTriggered &&
|
||||||
|
loggedIn
|
||||||
|
) {
|
||||||
|
reportPlatformFailures(
|
||||||
|
UserFeedback.getPrompt().then((prompt) => {
|
||||||
|
setPromptData(prompt);
|
||||||
|
setTimeout(triggerPopover, 30000);
|
||||||
|
}),
|
||||||
|
'RatingButton:getPrompt',
|
||||||
|
).catch((e) => {
|
||||||
|
console.warn('Failed to load ratings prompt:', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [triggerPopover, hasTriggered, loggedIn]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
const willBeShown = !isShown;
|
||||||
|
setIsShown(willBeShown);
|
||||||
|
setHasTriggered(true);
|
||||||
|
if (!willBeShown) {
|
||||||
|
UserFeedback.dismiss(sessionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitRating = (rating: number) => {
|
||||||
|
UserFeedback.submitRating(rating, sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitComment = (
|
||||||
|
rating: number,
|
||||||
|
comment: string,
|
||||||
|
selectedPredefinedComments: Array<string>,
|
||||||
|
allowUserInfoSharing: boolean,
|
||||||
|
) => {
|
||||||
|
UserFeedback.submitComment(
|
||||||
|
rating,
|
||||||
|
comment,
|
||||||
|
selectedPredefinedComments,
|
||||||
|
allowUserInfoSharing,
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!promptData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!promptData.shouldPopup || (hasTriggered && !isShown)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
visible={isShown}
|
||||||
|
content={
|
||||||
|
<FeedbackComponent
|
||||||
|
submitRating={submitRating}
|
||||||
|
submitComment={submitComment}
|
||||||
|
close={() => {
|
||||||
|
setIsShown(false);
|
||||||
|
}}
|
||||||
|
dismiss={onClick}
|
||||||
|
promptData={promptData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
trigger="click">
|
||||||
|
<NavbarButton
|
||||||
|
icon={StarOutlined}
|
||||||
|
label="Rate Flipper"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user