feat(ui): change share modal in player (#1958)

* feat(ui): change share modal in player

* fix(ui): rm console
This commit is contained in:
Delirium 2024-03-15 10:31:47 +01:00 committed by GitHub
parent 0f9eca733a
commit 11a1cf709f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 303 additions and 220 deletions

View file

@ -44,8 +44,8 @@ function SubHeader(props: any) {
const location = const location =
currentLocation && currentLocation.length > 70 currentLocation && currentLocation.length > 70
? `${currentLocation.slice(0, 25)}...${currentLocation.slice(-40)}` ? `${currentLocation.slice(0, 25)}...${currentLocation.slice(-40)}`
: currentLocation; : currentLocation;
const showReportModal = () => { const showReportModal = () => {
const { tabStates, currentTab } = store.get(); const { tabStates, currentTab } = store.get();
@ -114,6 +114,7 @@ function SubHeader(props: any) {
}, },
{ {
key: 4, key: 4,
autoclose: true,
component: ( component: (
<SharePopup <SharePopup
entity="sessions" entity="sessions"
@ -189,11 +190,11 @@ function SubHeader(props: any) {
defaultChecked={!uxtestingStore.hideDevtools} defaultChecked={!uxtestingStore.hideDevtools}
/> />
) : ( ) : (
<div> <div>
{/* @ts-ignore */} {/* @ts-ignore */}
<QueueControls /> <QueueControls />
</div> </div>
)} )}
</div> </div>
</div> </div>
{location && ( {location && (

View file

@ -6,6 +6,7 @@ import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
interface MenuItem { interface MenuItem {
key: number; key: number;
autoclose?: boolean;
component?: React.ReactElement; component?: React.ReactElement;
} }
@ -53,6 +54,7 @@ export default class ItemMenu extends React.PureComponent<Props> {
item.component ? ( item.component ? (
<div <div
key={item.key} key={item.key}
onClick={item.autoclose ? this.closeMenu : undefined}
role="menuitem" role="menuitem"
className="hover:bg-gray-light-shade cursor-pointer flex items-center w-full" className="hover:bg-gray-light-shade cursor-pointer flex items-center w-full"
> >

View file

@ -9,7 +9,8 @@ import { DateTime } from 'luxon';
function SessionCopyLink({ startedAt }: any) { function SessionCopyLink({ startedAt }: any) {
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
const { store } = React.useContext(PlayerContext); const { store } = React.useContext(PlayerContext);
const time = store.get().time;
const time = store?.get().time;
const copyHandler = () => { const copyHandler = () => {
setCopied(true); setCopied(true);

View file

@ -1,211 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { toast } from 'react-toastify';
import { Form, Button, Popover, Loader } from 'UI';
import styles from './sharePopup.module.css';
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
import SessionCopyLink from './SessionCopyLink';
import Select from 'Shared/Select';
import cn from 'classnames';
import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack';
import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams';
@connect(
(state) => ({
sessionId: state.getIn(['sessions', 'current']).sessionId,
channels: state.getIn(['slack', 'list']),
slackLoaded: state.getIn(['slack', 'loaded']),
msTeamsChannels: state.getIn(['teams', 'list']),
msTeamsLoaded: state.getIn(['teams', 'loaded']),
tenantId: state.getIn(['user', 'account', 'tenantId']),
}),
{ fetchSlack, fetchTeams, sendSlackMsg, sendMsTeamsMsg }
)
export default class SharePopup extends React.PureComponent {
state = {
comment: '',
isOpen: false,
channelId: this.props.channels.getIn([0, 'webhookId']),
teamsChannel: this.props.msTeamsChannels.getIn([0, 'webhookId']),
loadingSlack: false,
loadingTeams: false,
};
componentDidUpdate() {
if (this.state.isOpen) {
if (this.props.channels.size === 0 && !this.props.slackLoaded) {
this.props.fetchSlack();
}
if (this.props.msTeamsChannels.size === 0 && !this.props.msTeamsLoaded) {
this.props.fetchTeams();
}
}
}
editMessage = (e) => this.setState({ comment: e.target.value });
shareToSlack = () => {
this.setState({ loadingSlack: true }, () => {
this.props
.sendSlackMsg({
integrationId: this.state.channelId,
entity: 'sessions',
entityId: this.props.sessionId,
data: { comment: this.state.comment },
})
.then(() => this.handleSuccess('Slack'));
});
};
shareToMSTeams = () => {
this.setState({ loadingTeams: true }, () => {
this.props
.sendMsTeamsMsg({
integrationId: this.state.teamsChannel,
entity: 'sessions',
entityId: this.props.sessionId,
data: { comment: this.state.comment },
})
.then(() => this.handleSuccess('MS Teams'));
});
};
handleOpen = () => {
setTimeout(function () {
document.getElementById('message').focus();
}, 100);
};
handleClose = () => {
this.setState({ comment: '' });
};
handleSuccess = (endpoint) => {
const obj = endpoint === 'Slack' ? { loadingSlack: false } : { loadingTeams: false };
this.setState(obj);
toast.success(`Sent to ${endpoint}.`);
};
changeSlackChannel = ({ value }) => this.setState({ channelId: value.value });
changeTeamsChannel = ({ value }) => this.setState({ teamsChannel: value.value });
onClickHandler = () => {
this.setState({ isOpen: true });
};
render() {
const { trigger, channels, msTeamsChannels, showCopyLink = false } = this.props;
const { comment, channelId, teamsChannel, loadingSlack, loadingTeams } = this.state;
const slackOptions = channels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
const msTeamsOptions = msTeamsChannels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
return (
<Popover
onOpen={() => this.setState({ isOpen: true })}
onClose={() => this.setState({ isOpen: false, comment: '' })}
render={() => (
<div className={styles.wrapper}>
{this.state.loadingTeams || this.state.loadingSlack ? (
<Loader loading />
) : (
<>
<div className="text-xl mr-4 font-semibold mb-4">
Share
</div>
{slackOptions.length > 0 || msTeamsOptions.length > 0 ? (
<div>
<div className={styles.body}>
<textarea
name="message"
id="message"
cols="30"
rows="4"
resize="none"
onChange={this.editMessage}
value={comment}
placeholder="Add Message (Optional)"
className="p-4 text-figmaColors-text-primary text-base"
/>
{slackOptions.length > 0 && (
<Form.Field className="mb-15-imp">
<label>Share to slack</label>
<div className="grid grid-cols-6 gap-4">
<Select
options={slackOptions}
defaultValue={channelId}
onChange={this.changeSlackChannel}
className="col-span-4"
/>
{this.state.channelId && (
<Button
onClick={this.shareToSlack}
icon="integrations/slack-bw"
variant="outline"
className="col-span-2"
>
{loadingSlack ? 'Sending...' : 'Send'}
</Button>
)}
</div>
</Form.Field>
)}
{msTeamsOptions.length > 0 && (
<Form.Field className="mb-15-imp">
<label>Share to MS Teams</label>
<div className="grid grid-cols-6 gap-4">
<Select
options={msTeamsOptions}
defaultValue={teamsChannel}
onChange={this.changeTeamsChannel}
className="col-span-4"
/>
{this.state.teamsChannel && (
<Button
onClick={this.shareToMSTeams}
icon="integrations/teams-white"
variant="outline"
className="col-span-2"
>
{loadingTeams ? 'Sending...' : 'Send'}
</Button>
)}
</div>
</Form.Field>
)}
</div>
<div className={styles.footer}>
<SessionCopyLink />
</div>
</div>
) : (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<>
<div className="border-t -mx-2" />
<div>
<SessionCopyLink />
</div>
</>
)}
</>
)}
</>
)}
</div>
)}
>
{trigger}
</Popover>
);
}
}

View file

@ -0,0 +1,289 @@
import { useModal } from 'Components/Modal';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { toast } from 'react-toastify';
import { Icon, Loader } from 'UI';
import styles from './sharePopup.module.css';
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
import SessionCopyLink from './SessionCopyLink';
import Select from 'Shared/Select';
import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack';
import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams';
import { Button, Segmented } from 'antd';
interface Msg {
integrationId: string;
entity: 'sessions';
entityId: string;
data: { comment: string };
}
const SharePopup = ({
trigger,
showCopyLink = false,
}: {
trigger: string;
showCopyLink?: boolean;
}) => {
const { showModal, hideModal } = useModal();
const openModal = () => {
showModal(
<ShareModal
hideModal={hideModal}
showCopyLink={showCopyLink}
/>,
{ right: true, width: 300 }
);
};
return (
<div className={'w-full h-full'} onClick={openModal}>
{trigger}
</div>
);
};
interface Props {
sessionId: string;
channels: { webhookId: string; name: string }[];
slackLoaded: boolean;
msTeamsChannels: { webhookId: string; name: string }[];
msTeamsLoaded: boolean;
tenantId: string;
fetchSlack: () => void;
fetchTeams: () => void;
sendSlackMsg: (msg: Msg) => any;
sendMsTeamsMsg: (msg: Msg) => any;
showCopyLink?: boolean;
hideModal: () => void;
}
function ShareModalComp({
sessionId,
sendSlackMsg,
sendMsTeamsMsg,
showCopyLink,
channels,
slackLoaded,
msTeamsChannels,
msTeamsLoaded,
fetchSlack,
fetchTeams,
hideModal,
}: Props) {
const [shareTo, setShareTo] = useState('slack');
const [comment, setComment] = useState('');
// @ts-ignore
const [channelId, setChannelId] = useState(channels.getIn([0, 'webhookId']));
// @ts-ignore
const [teamsChannel, setTeamsChannel] = useState(msTeamsChannels.getIn([0, 'webhookId']));
const [loadingSlack, setLoadingSlack] = useState(false);
const [loadingTeams, setLoadingTeams] = useState(false);
const isLoading = loadingSlack || loadingTeams;
useEffect(() => {
// @ts-ignore
if (channels.size === 0 && !slackLoaded) {
fetchSlack();
}
// @ts-ignore
if (msTeamsChannels.size === 0 && !msTeamsLoaded) {
fetchTeams();
}
}, [channels, slackLoaded, msTeamsChannels, msTeamsLoaded, fetchSlack, fetchTeams]);
const editMessage = (e: React.ChangeEvent<HTMLTextAreaElement>) => setComment(e.target.value);
const shareToSlack = () => {
setLoadingSlack(true);
sendSlackMsg({
integrationId: channelId,
entity: 'sessions',
entityId: sessionId,
data: { comment },
}).then(() => handleSuccess('Slack'));
};
const shareToMSTeams = () => {
setLoadingTeams(true);
sendMsTeamsMsg({
integrationId: teamsChannel,
entity: 'sessions',
entityId: sessionId,
data: { comment },
}).then(() => handleSuccess('MS Teams'));
};
const handleSuccess = (endpoint: string) => {
if (endpoint === 'Slack') {
setLoadingSlack(false);
} else {
setLoadingTeams(false);
}
// @ts-ignore
toast.success(`Sent to ${endpoint}.`);
};
const changeSlackChannel = (value: any) => setChannelId(value.value);
const changeTeamsChannel = (value: any) => setTeamsChannel(value.value);
const slackOptions = channels
.map(({ webhookId, name }) => ({
value: webhookId,
label: name,
}))
// @ts-ignore
.toJS();
const msTeamsOptions = msTeamsChannels
.map(({ webhookId, name }) => ({
value: webhookId,
label: name,
}))
// @ts-ignore
.toJS();
const sendMsg = () => {
if (shareTo === 'slack') {
shareToSlack();
} else {
shareToMSTeams();
}
hideModal();
}
const hasBoth = slackOptions.length > 0 && msTeamsOptions.length > 0;
const hasNothing = slackOptions.length === 0 && msTeamsOptions.length === 0;
return (
<div className={styles.wrapper}>
{isLoading ? (
<Loader loading />
) : (
<>
<div className="text-xl mr-4 font-semibold mb-4 flex items-center gap-2">
<Icon name={'share-alt'} size={16} />
<div>Share Session</div>
</div>
{!hasNothing ? (
<div>
<div className={'flex flex-col gap-4'}>
<div>
<div className={'font-semibold flex items-center'}>
Share via
</div>
{hasBoth ? (
<Segmented
options={[
{
label: <div className={'flex items-center gap-2'}>
<Icon name="integrations/slack-bw" size={16} />
<div>Slack</div>
</div>,
value: 'slack',
},
{
label: <div className={'flex items-center gap-2'}>
<Icon name="integrations/teams-white" size={16} />
<div>MS Teams</div>
</div>,
value: 'teams',
},
]}
onChange={(value) => setShareTo(value as 'slack' | 'teams')}
/>
) : (
<div>
<Icon
name={
slackOptions.length > 0
? 'integrations/slack-bw'
: 'integrations/teams-white'
}
/>
<div>{slackOptions.length > 0 ? 'Slack' : 'MS Teams'}</div>
</div>
)}
</div>
<div>
<div className={'font-semibold'}>Select a channel or individual</div>
{shareTo === 'slack' ? (
<Select
options={slackOptions}
defaultValue={channelId}
onChange={changeSlackChannel}
className="col-span-4"
/>
) : (
<Select
options={msTeamsOptions}
defaultValue={teamsChannel}
onChange={changeTeamsChannel}
className="col-span-4"
/>
)}
</div>
<div>
<div className={'font-semibold'}>Message</div>
<textarea
name="message"
id="message"
cols={30}
rows={4}
onChange={editMessage}
value={comment}
placeholder="Add Message (Optional)"
className="p-4 text-figmaColors-text-primary text-base bg-white border rounded border-gray-light"
/>
</div>
</div>
<div className={'mt-4'}>
<SessionCopyLink />
<div className={'flex items-center gap-2 pt-8 mt-4 border-t'}>
<Button type={'primary'} onClick={sendMsg}>Send</Button>
<Button type={'primary'} ghost onClick={hideModal}>
Cancel
</Button>
</div>
</div>
</div>
) : (
<>
<div className={styles.body}>
<IntegrateSlackButton />
</div>
{showCopyLink && (
<>
<div className="border-t -mx-2" />
<div>
<SessionCopyLink />
</div>
</>
)}
</>
)}
</>
)}
</div>
);
}
const mapStateToProps = (state: Record<string, any>) => ({
sessionId: state.getIn(['sessions', 'current']).sessionId,
channels: state.getIn(['slack', 'list']),
slackLoaded: state.getIn(['slack', 'loaded']),
msTeamsChannels: state.getIn(['teams', 'list']),
msTeamsLoaded: state.getIn(['teams', 'loaded']),
tenantId: state.getIn(['user', 'account', 'tenantId']),
});
const ShareModal = connect(mapStateToProps, {
fetchSlack,
fetchTeams,
sendSlackMsg,
sendMsTeamsMsg,
})(ShareModalComp);
export default SharePopup;

View file

@ -8,7 +8,8 @@
.wrapper { .wrapper {
background-color: white; background-color: white;
width: 390px; width: 100%;
height: 100vh;
padding: 10px 8px; padding: 10px 8px;
color: $gray-darkest !important; color: $gray-darkest !important;
} }