fix(ui): onboarding design fixes (#1993)
* fix(ui): onboarding design fixes * more stuff * some assist details and options
This commit is contained in:
parent
21c312f98b
commit
8b52432a9a
12 changed files with 410 additions and 249 deletions
|
|
@ -43,8 +43,36 @@ function MyApp() {
|
|||
<div className="font-bold my-2">Options</div>
|
||||
<Highlight className="js">
|
||||
{`trackerAssist({
|
||||
confirmText: string;
|
||||
})`}
|
||||
onAgentConnect: StartEndCallback;
|
||||
onCallStart: StartEndCallback;
|
||||
onRemoteControlStart: StartEndCallback;
|
||||
onRecordingRequest?: (agentInfo: Record<string, any>) => any;
|
||||
onCallDeny?: () => any;
|
||||
onRemoteControlDeny?: (agentInfo: Record<string, any>) => any;
|
||||
onRecordingDeny?: (agentInfo: Record<string, any>) => any;
|
||||
session_calling_peer_key: string;
|
||||
session_control_peer_key: string;
|
||||
callConfirm: ConfirmOptions;
|
||||
controlConfirm: ConfirmOptions;
|
||||
recordingConfirm: ConfirmOptions;
|
||||
socketHost?: string;
|
||||
config: RTCConfiguration;
|
||||
serverURL: string
|
||||
callUITemplate?: string;
|
||||
})
|
||||
|
||||
type ConfirmOptions = {
|
||||
text?:string,
|
||||
style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'})
|
||||
confirmBtn?: ButtonOptions,
|
||||
declineBtn?: ButtonOptions
|
||||
}
|
||||
|
||||
type ButtonOptions = HTMLButtonElement | string | {
|
||||
innerHTML?: string, // to pass an svg string or text
|
||||
style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'})
|
||||
}
|
||||
`}
|
||||
</Highlight>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Form, Input, Button, Icon, SegmentSelection } from 'UI';
|
||||
import { save, edit, update, fetchList, remove } from 'Duck/site';
|
||||
import { pushNewSite } from 'Duck/user';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import styles from './siteForm.module.css';
|
||||
import { confirm } from 'UI';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
||||
import { withStore } from 'App/mstore';
|
||||
import { Segmented } from 'antd';
|
||||
import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react';
|
||||
import { ConnectedProps, connect } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { withStore } from 'App/mstore';
|
||||
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
import { edit, fetchList, remove, save, update } from 'Duck/site';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
import { pushNewSite } from 'Duck/user';
|
||||
import { Button, Form, Icon, Input, SegmentSelection } from 'UI';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
import styles from './siteForm.module.css';
|
||||
|
||||
type OwnProps = {
|
||||
onClose: (arg: any) => void;
|
||||
mstore: any;
|
||||
|
|
@ -81,8 +84,10 @@ const NewSiteForm = ({
|
|||
const handleRemove = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
|
||||
header: 'Project Deletion Alert',
|
||||
confirmation: `Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.`,
|
||||
confirmButton: 'Yes, delete',
|
||||
cancelButton: 'Cancel',
|
||||
})
|
||||
) {
|
||||
remove(site.id).then(() => {
|
||||
|
|
@ -94,15 +99,25 @@ const NewSiteForm = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleEdit = ({ target: { name, value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleEdit = ({
|
||||
target: { name, value },
|
||||
}: ChangeEvent<HTMLInputElement>) => {
|
||||
setExistsError(false);
|
||||
edit({ [name]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{site.exists() ? 'Edit Project' : 'New Project'}</h3>
|
||||
<Form className={styles.formWrapper} onSubmit={site.validate() && onSubmit}>
|
||||
<div
|
||||
className="bg-white h-screen overflow-y-auto"
|
||||
style={{ width: '350px' }}
|
||||
>
|
||||
<h3 className="p-5 text-2xl">
|
||||
{site.exists() ? 'Edit Project' : 'New Project'}
|
||||
</h3>
|
||||
<Form
|
||||
className={styles.formWrapper}
|
||||
onSubmit={site.validate() && onSubmit}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
|
|
@ -117,17 +132,24 @@ const NewSiteForm = ({
|
|||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Project Type</label>
|
||||
<SegmentSelection
|
||||
name={"platform"}
|
||||
value={{ name: site.platform, value: site.platform }}
|
||||
list={[
|
||||
{ name: 'Web', value: 'web' },
|
||||
{ name: 'iOS', value: 'ios' },
|
||||
]}
|
||||
onSelect={(_, { value }) => {
|
||||
edit({ platform: value });
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Segmented
|
||||
options={[
|
||||
{
|
||||
value: 'web',
|
||||
label: 'Web',
|
||||
},
|
||||
{
|
||||
value: 'ios',
|
||||
label: 'iOS',
|
||||
},
|
||||
]}
|
||||
value={site.platform}
|
||||
onChange={(value) => {
|
||||
edit({ platform: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button
|
||||
|
|
@ -140,12 +162,21 @@ const NewSiteForm = ({
|
|||
{site.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{site.exists() && (
|
||||
<Button variant="text" type="button" onClick={handleRemove} disabled={!canDelete}>
|
||||
<Button
|
||||
variant="text"
|
||||
type="button"
|
||||
onClick={handleRemove}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
<Icon name="trash" size="16" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{existsError && <div className={styles.errorMessage}>{'Project exists already.'}</div>}
|
||||
{existsError && (
|
||||
<div className={styles.errorMessage}>
|
||||
{'Project exists already.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
@ -156,7 +187,9 @@ const mapStateToProps = (state: any) => ({
|
|||
activeSiteId: state.getIn(['site', 'active', 'id']),
|
||||
site: state.getIn(['site', 'instance']),
|
||||
siteList: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']),
|
||||
loading:
|
||||
state.getIn(['site', 'save', 'loading']) ||
|
||||
state.getIn(['site', 'remove', 'loading']),
|
||||
canDelete: state.getIn(['site', 'list']).size > 1,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
function InsightItem(props: Props) {
|
||||
const { item, onClick = () => {} } = props;
|
||||
const className =
|
||||
'whitespace-nowrap flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
|
||||
'flex items-start flex-wrap py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
|
||||
|
||||
switch (item.category) {
|
||||
case IssueCategory.RAGE:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function DashboardList() {
|
|||
<div className="grid grid-cols-12 py-2 font-medium px-6">
|
||||
<div className="col-span-8">Title</div>
|
||||
<div className="col-span-2">Visibility</div>
|
||||
<div className="col-span-2 text-right">Creation Date</div>
|
||||
<div className="col-span-2 text-right">Last Modified</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import DocCard from 'Shared/DocCard/DocCard';
|
|||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
import withPageTitle from 'App/components/hocs/withPageTitle';
|
||||
import { Button as AntButton } from 'antd'
|
||||
|
||||
interface Props extends WithOnboardingProps {
|
||||
platforms: Array<{
|
||||
|
|
@ -41,13 +42,14 @@ function IdentifyUsersTab(props: Props) {
|
|||
href={`https://docs.openreplay.com/en/installation/identify-user${platform.value === "web" ? "/#with-npm" : "/#with-ios-app"}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="text-primary" icon="question-circle" className="ml-2">
|
||||
See Documentation
|
||||
</Button>
|
||||
<AntButton size={'small'} type={'text'} className="ml-2 flex items-center gap-2">
|
||||
<Icon name={'question-circle'} />
|
||||
<div className={'text-main'}>See Documentation</div>
|
||||
</AntButton>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="p-4 flex gap-2 items-center">
|
||||
<span className="font-medium">Your platform</span>
|
||||
<span className="font-medium">Project Type</span>
|
||||
<Segmented
|
||||
options={platforms}
|
||||
value={platform.value}
|
||||
|
|
@ -69,11 +71,11 @@ function IdentifyUsersTab(props: Props) {
|
|||
{platform.value === 'web' ? (
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
) : (
|
||||
<HighlightCode
|
||||
className="swift"
|
||||
text={`OpenReplay.shared.setUserID('john@doe.com');`}
|
||||
/>
|
||||
)}
|
||||
<HighlightCode
|
||||
className="swift"
|
||||
text={`OpenReplay.shared.setUserID('john@doe.com');`}
|
||||
/>
|
||||
)}
|
||||
{platform.value === 'web' ? (
|
||||
<div className="flex items-center my-2">
|
||||
<Icon name="info-circle" color="gray-darkest" />
|
||||
|
|
@ -121,11 +123,11 @@ function IdentifyUsersTab(props: Props) {
|
|||
{platform.value === 'web' ? (
|
||||
<HighlightCode className="js" text={`tracker.setMetadata('plan', 'premium');`} />
|
||||
) : (
|
||||
<HighlightCode
|
||||
className="swift"
|
||||
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
|
||||
/>
|
||||
)}
|
||||
<HighlightCode
|
||||
className="swift"
|
||||
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { WithOnboardingProps } from '../withOnboarding';
|
|||
import { OB_TABS } from 'App/routes';
|
||||
import withPageTitle from 'App/components/hocs/withPageTitle';
|
||||
import { Segmented } from 'antd';
|
||||
import { Button as AntButton } from 'antd'
|
||||
|
||||
interface Props extends WithOnboardingProps {
|
||||
platforms: Array<{
|
||||
|
|
@ -39,14 +40,15 @@ function InstallOpenReplayTab(props: Props) {
|
|||
<ProjectFormButton />
|
||||
</div>
|
||||
</div>
|
||||
<a href={platform.value === 'web' ? 'https://docs.openreplay.com/en/sdk/' : 'https://docs.openreplay.com/en/ios-sdk/'} target="_blank">
|
||||
<Button variant="text-primary" icon="question-circle" className="ml-2">
|
||||
See Documentation
|
||||
</Button>
|
||||
<a href={"https://docs.openreplay.com/en/using-or/"} target="_blank">
|
||||
<AntButton size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
|
||||
<Icon name={"question-circle"} />
|
||||
<div className={"text-main"}>See Documentation</div>
|
||||
</AntButton>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="p-4 flex gap-2 items-center">
|
||||
<span className="font-medium">Your platform</span>
|
||||
<span className="font-medium">Project Type</span>
|
||||
<Segmented
|
||||
options={platforms}
|
||||
value={platform.value}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { Button as AntButton } from 'antd';
|
||||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
|
||||
import Integrations from 'App/components/Client/Integrations/Integrations';
|
||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
import withPageTitle from 'App/components/hocs/withPageTitle';
|
||||
import { Button, Icon } from 'UI';
|
||||
|
||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
function IntegrationsTab(props: Props) {
|
||||
|
|
@ -14,15 +17,27 @@ function IntegrationsTab(props: Props) {
|
|||
<div className="ml-3">Integrations</div>
|
||||
</div>
|
||||
|
||||
<a href="https://docs.openreplay.com/en/v1.10.0/integrations/" target="_blank">
|
||||
<Button variant="text-primary" icon="question-circle" className="ml-2">
|
||||
See Documentation
|
||||
</Button>
|
||||
<a
|
||||
href="https://docs.openreplay.com/en/integrations/"
|
||||
target="_blank"
|
||||
>
|
||||
<AntButton
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
className="ml-2 flex items-center gap-2"
|
||||
>
|
||||
<Icon name={'question-circle'} />
|
||||
<div className={'text-main'}>See Documentation</div>
|
||||
</AntButton>
|
||||
</a>
|
||||
</h1>
|
||||
<Integrations hideHeader={true} />
|
||||
<div className="border-t px-4 py-3 flex justify-end">
|
||||
<Button variant="primary" className="" onClick={() => (props.skip ? props.skip() : null)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => (props.skip ? props.skip() : null)}
|
||||
>
|
||||
Complete Setup
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -30,4 +45,6 @@ function IntegrationsTab(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(withPageTitle("Integrations - OpenReplay")(IntegrationsTab));
|
||||
export default withOnboarding(
|
||||
withPageTitle('Integrations - OpenReplay')(IntegrationsTab)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Button, Icon } from 'UI';
|
|||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
import withPageTitle from 'App/components/hocs/withPageTitle';
|
||||
import { Button as AntButton } from 'antd'
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
|
|
@ -21,9 +22,10 @@ function ManageUsersTab(props: Props) {
|
|||
href="https://docs.openreplay.com/en/tutorials/adding-users/"
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="text-primary" icon="question-circle" className="ml-2">
|
||||
See Documentation
|
||||
</Button>
|
||||
<AntButton size={'small'} type={'text'} className="ml-2 flex items-center gap-2">
|
||||
<Icon name={'question-circle'} />
|
||||
<div className={'text-main'}>See Documentation</div>
|
||||
</AntButton>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="grid grid-cols-6 gap-4 p-4">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function InstallDocs({ site }) {
|
|||
const [isSpa, setIsSpa] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<div className="mb-6">
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="1" />
|
||||
Install the npm package.
|
||||
|
|
@ -48,7 +48,7 @@ function InstallDocs({ site }) {
|
|||
<Highlight className="cli">{installationCommand}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={'mb-6'}>
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="2" />
|
||||
Continue with one of the following options.
|
||||
|
|
@ -87,7 +87,9 @@ function InstallDocs({ site }) {
|
|||
<div>
|
||||
<div className="mb-2 text-sm">
|
||||
Otherwise, if your web app is <strong>Server-Side-Rendered (SSR)</strong> (i.e.
|
||||
NextJS, NuxtJS) use this snippet:
|
||||
NextJS, NuxtJS),{' '}
|
||||
<a className={'text-main'} href={'https://docs.openreplay.com/en/using-or/next/'}>consider async imports</a>
|
||||
or cjs version of the library:
|
||||
</div>
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
|
|
@ -100,6 +102,43 @@ function InstallDocs({ site }) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="3" />
|
||||
Enable Assist (Optional)
|
||||
</div>
|
||||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
Install the plugin via npm:
|
||||
</div>
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={`npm i @openreplay/tracker-assist`} />
|
||||
</div>
|
||||
<Highlight className="js">{`$ npm i @openreplay/tracker-assist`}</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={'mb-2'}>
|
||||
Then enable it with your tracker:
|
||||
</div>
|
||||
<div className={cn(stl.snippetWrapper)}>
|
||||
<div className="absolute mt-1 mr-2 right-0">
|
||||
<CopyButton content={`tracker.use(trackerAssist(options));`} />
|
||||
</div>
|
||||
<Highlight className="js">{`tracker.use(trackerAssist(options));`}</Highlight>
|
||||
</div>
|
||||
<div className={'text-sm'}>Read more about available options <a
|
||||
className={'text-main'}
|
||||
href={'https://github.com/openreplay/openreplay/blob/main/tracker/tracker-assist/README.md'}>here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Button} from 'UI';
|
||||
import { confirmable } from 'react-confirm';
|
||||
import { Modal } from 'UI'
|
||||
|
||||
import { Button } from 'antd'
|
||||
|
||||
const Confirmation = ({
|
||||
show,
|
||||
proceed,
|
||||
|
|
@ -31,7 +31,7 @@ const Confirmation = ({
|
|||
<Modal.Footer>
|
||||
<Button
|
||||
onClick={() => proceed(true)}
|
||||
variant="primary"
|
||||
type={'primary'}
|
||||
className="mr-2"
|
||||
>
|
||||
{confirmButton}
|
||||
|
|
|
|||
|
|
@ -1,184 +1,212 @@
|
|||
import { makeAutoObservable, observable, action } from "mobx"
|
||||
import User from "./types/user";
|
||||
import { userService } from "App/services";
|
||||
import { toast } from 'react-toastify';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { action, makeAutoObservable, observable } from 'mobx';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { userService } from 'App/services';
|
||||
|
||||
import User from './types/user';
|
||||
|
||||
export default class UserStore {
|
||||
list: User[] = [];
|
||||
instance: User|null = null;
|
||||
page: number = 1;
|
||||
pageSize: number = 10;
|
||||
searchQuery: string = "";
|
||||
modifiedCount: number = 0;
|
||||
list: User[] = [];
|
||||
instance: User | null = null;
|
||||
page: number = 1;
|
||||
pageSize: number = 10;
|
||||
searchQuery: string = '';
|
||||
modifiedCount: number = 0;
|
||||
|
||||
loading: boolean = false;
|
||||
saving: boolean = false;
|
||||
limits: any = {};
|
||||
initialDataFetched: boolean = false;
|
||||
loading: boolean = false;
|
||||
saving: boolean = false;
|
||||
limits: any = {};
|
||||
initialDataFetched: boolean = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
instance: observable,
|
||||
updateUser: action,
|
||||
updateKey: action,
|
||||
initUser: action,
|
||||
setLimits: action,
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
instance: observable,
|
||||
updateUser: action,
|
||||
updateKey: action,
|
||||
initUser: action,
|
||||
setLimits: action,
|
||||
});
|
||||
}
|
||||
|
||||
fetchLimits(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.getLimits()
|
||||
.then((response: any) => {
|
||||
this.setLimits(response);
|
||||
resolve(response);
|
||||
})
|
||||
}
|
||||
|
||||
fetchLimits(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.getLimits()
|
||||
.then((response: any) => {
|
||||
this.setLimits(response);
|
||||
resolve(response);
|
||||
}).catch((error: any) => {
|
||||
reject(error);
|
||||
});
|
||||
.catch((error: any) => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLimits(limits: any) {
|
||||
this.limits = limits;
|
||||
}
|
||||
setLimits(limits: any) {
|
||||
this.limits = limits;
|
||||
}
|
||||
|
||||
initUser(user?: any ): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (user) {
|
||||
this.instance = new User().fromJson(user.toJson());
|
||||
} else {
|
||||
this.instance = new User();
|
||||
}
|
||||
resolve();
|
||||
initUser(user?: any): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (user) {
|
||||
this.instance = new User().fromJson(user.toJson());
|
||||
} else {
|
||||
this.instance = new User();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
updateKey(key: keyof this, value: any) {
|
||||
this[key] = value;
|
||||
|
||||
if (key === 'searchQuery') {
|
||||
this.page = 1;
|
||||
}
|
||||
}
|
||||
|
||||
updateUser(user: User) {
|
||||
const index = this.list.findIndex((u) => u.userId === user.userId);
|
||||
if (index > -1) {
|
||||
this.list[index] = user;
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser(userId: string): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.one(userId)
|
||||
.then((response) => {
|
||||
this.instance = new User().fromJson(response.data);
|
||||
resolve(response);
|
||||
})
|
||||
}
|
||||
|
||||
updateKey(key: keyof this, value: any) {
|
||||
this[key] = value
|
||||
|
||||
if (key === 'searchQuery') {
|
||||
this.page = 1
|
||||
}
|
||||
}
|
||||
|
||||
updateUser(user: User) {
|
||||
const index = this.list.findIndex(u => u.userId === user.userId);
|
||||
if (index > -1) {
|
||||
this.list[index] = user;
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser(userId: string): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.one(userId)
|
||||
.then(response => {
|
||||
this.instance = new User().fromJson(response.data);
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetchUsers(): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.all()
|
||||
.then(response => {
|
||||
this.list = response.map(user => new User().fromJson(user));
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveUser(user: User): Promise<any> {
|
||||
this.saving = true;
|
||||
const wasCreating = !user.userId;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.save(user).then(response => {
|
||||
const newUser = new User().fromJson(response);
|
||||
if (wasCreating) {
|
||||
this.modifiedCount -= 1;
|
||||
this.list.push(newUser);
|
||||
toast.success('User created successfully');
|
||||
} else {
|
||||
this.updateUser(newUser);
|
||||
toast.success('User updated successfully');
|
||||
}
|
||||
resolve(response);
|
||||
}).catch(async (e) => {
|
||||
const err = await e.response?.json();
|
||||
this.saving = false;
|
||||
toast.error(err.errors[0] ? err.errors[0] : 'Error saving user');
|
||||
reject(e);
|
||||
}).finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(userId: string): Promise<any> {
|
||||
this.saving = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.delete(userId)
|
||||
.then(response => {
|
||||
this.modifiedCount += 1;
|
||||
this.list = this.list.filter(user => user.userId !== userId);
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.saving = false;
|
||||
toast.error('Error deleting user');
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copyInviteCode(userId: string): void {
|
||||
const content = this.list.find(u => u.userId === userId)?.invitationLink;
|
||||
if (content) {
|
||||
copy(content);
|
||||
toast.success('Invite code copied successfully');
|
||||
} else {
|
||||
toast.error('Invite code not found');
|
||||
}
|
||||
}
|
||||
|
||||
generateInviteCode(userId: string): Promise<any> {
|
||||
this.saving = true;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
userService.generateInviteCode(userId)
|
||||
.then(response => {
|
||||
const index = this.list.findIndex(u => u.userId === userId);
|
||||
if (index > -1) {
|
||||
this.list[index].updateKey('isExpiredInvite', false);
|
||||
this.list[index].updateKey('invitationLink', response.invitationLink);
|
||||
}
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.saving = false;
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
pending: 'Generating an invite code...',
|
||||
success: 'Invite code generated successfully',
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
fetchUsers(): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.all()
|
||||
.then((response) => {
|
||||
this.list = response.map((user) => new User().fromJson(user));
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveUser(user: User): Promise<any> {
|
||||
this.saving = true;
|
||||
const wasCreating = !user.userId;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.save(user)
|
||||
.then((response) => {
|
||||
const newUser = new User().fromJson(response);
|
||||
if (wasCreating) {
|
||||
this.modifiedCount -= 1;
|
||||
this.list.push(newUser);
|
||||
toast.success('User created successfully');
|
||||
} else {
|
||||
this.updateUser(newUser);
|
||||
toast.success('User updated successfully');
|
||||
}
|
||||
resolve(response);
|
||||
})
|
||||
.catch(async (e) => {
|
||||
const err = await e.response?.json();
|
||||
this.saving = false;
|
||||
const errStr = err.errors[0]
|
||||
? err.errors[0].includes('already exists')
|
||||
? `This email is already linked to an account or team on OpenReplay and can't be used again.`
|
||||
: err.errors[0]
|
||||
: 'Error saving user';
|
||||
toast.error(errStr);
|
||||
reject(e);
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(userId: string): Promise<any> {
|
||||
this.saving = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.delete(userId)
|
||||
.then((response) => {
|
||||
this.modifiedCount += 1;
|
||||
this.list = this.list.filter((user) => user.userId !== userId);
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.saving = false;
|
||||
toast.error('Error deleting user');
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copyInviteCode(userId: string): void {
|
||||
const content = this.list.find((u) => u.userId === userId)?.invitationLink;
|
||||
if (content) {
|
||||
copy(content);
|
||||
toast.success('Invite code copied successfully');
|
||||
} else {
|
||||
toast.error('Invite code not found');
|
||||
}
|
||||
}
|
||||
|
||||
generateInviteCode(userId: string): Promise<any> {
|
||||
this.saving = true;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
userService
|
||||
.generateInviteCode(userId)
|
||||
.then((response) => {
|
||||
const index = this.list.findIndex((u) => u.userId === userId);
|
||||
if (index > -1) {
|
||||
this.list[index].updateKey('isExpiredInvite', false);
|
||||
this.list[index].updateKey(
|
||||
'invitationLink',
|
||||
response.invitationLink
|
||||
);
|
||||
}
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.saving = false;
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
pending: 'Generating an invite code...',
|
||||
success: 'Invite code generated successfully',
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,14 +61,24 @@ function MyApp() {
|
|||
|
||||
#### Options
|
||||
|
||||
```js
|
||||
```ts
|
||||
trackerAssist({
|
||||
callConfirm?: string|ConfirmOptions;
|
||||
controlConfirm?: string|ConfirmOptions;
|
||||
config?: object;
|
||||
onAgentConnect?: () => (()=>void | void);
|
||||
onCallStart?: () => (()=>void | void);
|
||||
onRemoteControlStart?: () => (()=>void | void);
|
||||
onAgentConnect: StartEndCallback;
|
||||
onCallStart: StartEndCallback;
|
||||
onRemoteControlStart: StartEndCallback;
|
||||
onRecordingRequest?: (agentInfo: Record<string, any>) => any;
|
||||
onCallDeny?: () => any;
|
||||
onRemoteControlDeny?: (agentInfo: Record<string, any>) => any;
|
||||
onRecordingDeny?: (agentInfo: Record<string, any>) => any;
|
||||
session_calling_peer_key: string;
|
||||
session_control_peer_key: string;
|
||||
callConfirm: ConfirmOptions;
|
||||
controlConfirm: ConfirmOptions;
|
||||
recordingConfirm: ConfirmOptions;
|
||||
socketHost?: string;
|
||||
config: RTCConfiguration;
|
||||
serverURL: string
|
||||
callUITemplate?: string;
|
||||
})
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue