From e225d9e1c3753c74d39555925ec312844dfcf76b Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Fri, 17 Nov 2023 03:34:58 -0800 Subject: [PATCH] Bring back RatingButton Summary: Removed previously during redesign by mistake Reviewed By: antonk52 Differential Revision: D51347837 fbshipit-source-id: d634ad4c8983271a3936f458cabb63f006a4bb0a --- .../src/sandy-chrome/Navbar.tsx | 2 + .../src/sandy-chrome/RatingButton.tsx | 349 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx diff --git a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx index 014f9a941..94daf13f2 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx @@ -72,6 +72,7 @@ import {TroubleshootingGuide} from './appinspect/fb-stubs/TroubleshootingGuide'; import {FlipperDevTools} from '../chrome/FlipperDevTools'; import {TroubleshootingHub} from '../chrome/TroubleshootingHub'; import {Notification} from './notification/Notification'; +import {SandyRatingButton} from './RatingButton'; export const Navbar = withTrackingScope(function Navbar() { return ( @@ -104,6 +105,7 @@ export const Navbar = withTrackingScope(function Navbar() { + {getRenderHostInstance().serverConfig.environmentInfo diff --git a/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx b/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx new file mode 100644 index 000000000..b15a8a777 --- /dev/null +++ b/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx @@ -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 ( + + {this.props.comment} + + ); + } +} + +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 ( + + + Dismiss + + + ); +} + +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, + 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 = 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; + switch (this.state.nextAction) { + case 'select-rating': + body = [ + {this.props.promptData.bodyText}, + + this.onSubmitRating(newRating)} /> + , + dismissRow(this.props.dismiss), + ]; + break; + case 'leave-comment': + const predefinedComments = Object.entries( + this.state.predefinedComments, + ).map((c: [string, unknown], idx: number) => ( + + this.setState({ + predefinedComments: { + ...this.state.predefinedComments, + [c[0]]: !c[1], + }, + }) + } + /> + )); + body = [ + {predefinedComments}, + + this.setState({comment: e.target.value})} + onKeyDown={(e) => + e.key == 'Enter' && this.onCommentSubmitted(this.state.comment) + } + autoFocus + /> + , + + this.onAllowUserSharingChanged(e.target.checked)} + /> + {'Tool owner can contact me '} + , + + + , + dismissRow(this.props.dismiss), + ]; + break; + case 'finished': + body = [ + + Thanks for the feedback! You can now help + + prioritize bugs and features for Flipper in Papercuts + + , + dismissRow(this.props.dismiss), + ]; + break; + default: { + console.error('Illegal state: nextAction: ' + this.state.nextAction); + return null; + } + } + return ( + + + {this.state.nextAction === 'finished' + ? this.props.promptData.postSubmitHeading + : this.props.promptData.preSubmitHeading} + + {body} + + ); + } +} + +export function SandyRatingButton() { + const [promptData, setPromptData] = + useState(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, + allowUserInfoSharing: boolean, + ) => { + UserFeedback.submitComment( + rating, + comment, + selectedPredefinedComments, + allowUserInfoSharing, + sessionId, + ); + }; + + if (!promptData) { + return null; + } + if (!promptData.shouldPopup || (hasTriggered && !isShown)) { + return null; + } + return ( + { + setIsShown(false); + }} + dismiss={onClick} + promptData={promptData} + /> + } + placement="right" + trigger="click"> + + + ); +}