fix(ui): issue form\

This commit is contained in:
Shekar Siri 2025-02-04 11:32:53 +01:00
parent 1b3a3dfc21
commit 82621012de
7 changed files with 328 additions and 311 deletions

View file

@ -1,178 +0,0 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { useStore } from 'App/mstore';
import { Button, CircularLoader, Form, Input, Loader } from 'UI';
import Select from 'Shared/Select';
const SelectedValue = ({ icon, text }) => {
return (
<div className="flex items-center">
{icon}
<span>{text}</span>
</div>
);
};
function IssueForm(props) {
const { closeHandler } = props;
const { issueReportingStore } = useStore();
const creating = issueReportingStore.createLoading;
const projects = issueReportingStore.projects;
const projectsLoading = issueReportingStore.projectsLoading;
const users = issueReportingStore.users;
const instance = issueReportingStore.instance;
const metaLoading = issueReportingStore.metaLoading;
const issueTypes = issueReportingStore.issueTypes;
const addActivity = issueReportingStore.saveIssue;
const init = issueReportingStore.init;
const edit = issueReportingStore.editInstance;
const fetchMeta = issueReportingStore.fetchMeta;
React.useEffect(() => {
init({
projectId: projects[0] ? projects[0].id : '',
issueType: issueTypes[0] ? issueTypes[0].id : '',
});
}, []);
React.useEffect(() => {
if (instance?.projectId) {
fetchMeta(instance?.projectId).then(() => {
edit({
issueType: '',
assignee: '',
projectId: instance?.projectId,
});
});
}
}, [instance?.projectId]);
const onSubmit = () => {
const { sessionId } = props;
addActivity(sessionId, instance).then(() => {
const { errors } = props;
if (!errors || errors.length === 0) {
init({ projectId: instance?.projectId });
void issueReportingStore.fetchList(sessionId);
closeHandler();
}
});
};
const write = (e) => {
const {
target: { name, value },
} = e;
edit({ [name]: value });
};
const writeOption = ({ name, value }) => edit({ [name]: value.value });
const projectOptions = projects.map(({ name, id }) => ({
label: name,
value: id,
}));
const userOptions = users.map(({ name, id }) => ({ label: name, value: id }));
const issueTypeOptions = issueTypes.map(({ name, id, iconUrl, color }) => {
return { label: name, value: id, iconUrl, color };
});
const selectedIssueType = issueTypes.filter(
(issue) => issue.id == instance?.issueType
)[0];
return (
<Loader loading={projectsLoading} size={40}>
<Form onSubmit={onSubmit} className="text-left">
<Form.Field className="mb-15-imp">
<label htmlFor="issueType" className="flex items-center">
<span className="mr-2">Project</span>
<CircularLoader loading={metaLoading} />
</label>
<Select
name="projectId"
options={projectOptions}
defaultValue={instance?.projectId}
fluid
onChange={writeOption}
placeholder="Project"
/>
</Form.Field>
<Form.Field className="mb-15-imp">
<label htmlFor="issueType">Issue Type</label>
<Select
selection
name="issueType"
labeled
options={issueTypeOptions}
defaultValue={instance?.issueType}
fluid
onChange={writeOption}
placeholder="Select issue type"
text={
selectedIssueType ? (
<SelectedValue
icon={selectedIssueType.iconUrl}
text={selectedIssueType.name}
/>
) : (
''
)
}
/>
</Form.Field>
<Form.Field className="mb-15-imp">
<label htmlFor="assignee">Assignee</label>
<Select
selection
name="assignee"
options={userOptions}
fluid
onChange={writeOption}
placeholder="Select a user"
/>
</Form.Field>
<Form.Field className="mb-15-imp">
<label htmlFor="title">Summary</label>
<Input
name="title"
value={instance?.title}
placeholder="Issue Title / Summary"
onChange={write}
/>
</Form.Field>
<Form.Field className="mb-15-imp">
<label htmlFor="description">Description</label>
<textarea
name="description"
rows="2"
value={instance?.description}
placeholder="E.g. Found this issue at 3:29secs"
onChange={write}
className="text-area"
/>
</Form.Field>
<Button
loading={creating}
variant="primary"
disabled={!instance?.isValid}
className="float-left mr-2"
type="submit"
>
{'Create'}
</Button>
<Button type="button" onClick={closeHandler}>
{'Cancel'}
</Button>
</Form>
</Loader>
);
}
export default observer(IssueForm);

View file

@ -0,0 +1,177 @@
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { useStore } from 'App/mstore';
import { CircularLoader, Loader } from 'UI';
import { Form, Input, Button, Select } from 'antd';
import { toast } from 'react-toastify';
interface Props {
closeHandler: () => void;
sessionId: string;
errors: string[];
}
const IssueForm: React.FC<Props> = observer(({ closeHandler, sessionId, errors }) => {
const {
issueReportingStore: {
createLoading: creating,
projects,
projectsLoading,
users,
instance,
metaLoading,
issueTypes,
saveIssue,
init,
editInstance: edit,
fetchMeta,
fetchList
}
} = useStore();
useEffect(() => {
init({
projectId: projects[0]?.id || '',
issueType: issueTypes[0]?.id || ''
});
}, []);
useEffect(() => {
if (instance?.projectId) {
fetchMeta(instance.projectId).then(() => {
edit({ issueType: '', assignee: '', projectId: instance.projectId });
});
}
}, [instance?.projectId]);
const onFinish = async () => {
await saveIssue(sessionId, instance).then(() => {
closeHandler();
}).catch(() => {
toast('Failed to create issue', { type: 'error' });
});
// if (!errors || errors.length === 0) {
// init({ projectId: instance?.projectId });
// fetchList(sessionId);
// closeHandler();
// }
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
edit({ [name]: value });
};
const handleSelect = (field: string) => (value: string) => {
edit({ [field]: value });
};
const projectOptions = projects.map(
({ name, id }: { name: string; id: string }) => ({ label: name, value: id })
);
const userOptions = users.map(
({ name, id }: { name: string; id: string }) => ({ label: name, value: id })
);
const issueTypeOptions = issueTypes.map((opt: any) => ({
label: opt.name,
value: opt.id,
iconUrl: opt.iconUrl
}));
return (
<Loader loading={projectsLoading} size={40}>
<Form onFinish={onFinish} layout="vertical" className="text-left">
<Form.Item
label={
<>
<span className="mr-2">Project</span>
<CircularLoader loading={metaLoading} />
</>
}
className="mb-15-imp"
>
<Select
value={instance?.projectId}
onChange={handleSelect('projectId')}
placeholder="Project"
>
{projectOptions.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Issue Type" className="mb-15-imp">
<Select
value={instance?.issueType}
onChange={handleSelect('issueType')}
placeholder="Select issue type"
optionLabelProp="label"
>
{issueTypeOptions.map((option) => (
<Select.Option
key={option.value}
value={option.value}
label={
<div className="flex items-center">
{option.iconUrl}
<span>{option.label}</span>
</div>
}
>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Assignee" className="mb-15-imp">
<Select
value={instance?.assignee}
onChange={handleSelect('assignee')}
placeholder="Select a user"
>
{userOptions.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Summary" className="mb-15-imp">
<Input
name="title"
value={instance?.title}
placeholder="Issue Title / Summary"
onChange={handleChange}
/>
</Form.Item>
<Form.Item label="Description" className="mb-15-imp">
<Input.TextArea
name="description"
rows={2}
value={instance?.description}
placeholder="E.g. Found this issue at 3:29secs"
onChange={handleChange}
className="text-area"
/>
</Form.Item>
<Button
loading={creating}
disabled={!instance?.isValid}
className="float-left mr-2"
type="primary"
htmlType="submit"
>
Create
</Button>
<Button onClick={closeHandler}>Cancel</Button>
</Form>
</Loader>
);
});
export default IssueForm;

View file

@ -1,20 +1,25 @@
import React from 'react';
import stl from './issuesModal.module.css';
import IssueForm from './IssueForm';
import cn from 'classnames'
import cn from 'classnames';
interface Props {
sessionId: string;
closeHandler: () => void;
}
const IssuesModal = ({
sessionId,
closeHandler,
}) => {
sessionId,
closeHandler
}: Props) => {
return (
<div className={cn(stl.wrapper, 'h-screen')}>
<h3 className="text-xl font-semibold">
<span>Create Issue</span>
</h3>
<IssueForm sessionId={ sessionId } closeHandler={ closeHandler } />
<IssueForm sessionId={sessionId} closeHandler={closeHandler} errors={[]} />
</div>
);
}
};
export default IssuesModal;

View file

@ -11,6 +11,7 @@ const Key = ({ label }: { label: string }) => (
{label}
</div>
);
function Cell({ shortcut, text }: any) {
return (
<div className="flex items-center gap-2 justify-start rounded">
@ -39,28 +40,26 @@ export const PlaybackSpeedShortcut = () => <Key label={'↑ / ↓'} />;
export function ShortcutGrid() {
return (
<div className={'p-4 overflow-y-auto h-screen'}>
<div className={'mb-4 font-semibold text-xl'}>Keyboard Shortcuts</div>
<div className=" grid grid-cols-1 grid-flow-row-dense auto-cols-max gap-4 justify-items-start">
<Cell shortcut="⇧ + U" text="Copy Session URL with time" />
<Cell shortcut="⇧ + C" text="Launch Console" />
<Cell shortcut="⇧ + N" text="Launch Network" />
<Cell shortcut="⇧ + P" text="Launch Performance" />
<Cell shortcut="⇧ + R" text="Launch State" />
<Cell shortcut="⇧ + E" text="Launch Events" />
<Cell shortcut="⇧ + F" text="Play Session in Fullscreen" />
<Cell shortcut="Space" text="Play/Pause Session" />
<Cell shortcut="⇧ + X" text="Launch X-Ray" />
<Cell shortcut="⇧ + A" text="Launch User Actions" />
<Cell shortcut="⇧ + I" text="Launch More User Info" />
<Cell shortcut="⇧ + M" text="Launch Options Menu" />
<Cell shortcut="⇧ + >" text="Play Next Session" />
<Cell shortcut="⇧ + <" text="Play Previous Session" />
<Cell shortcut="→" text="Skip Forward" />
<Cell shortcut="←" text="Skip Backward" />
<Cell shortcut="↑" text="Playback Speed Up" />
<Cell shortcut="↓" text="Playback Speed Down" />
</div>
<div className=" grid grid-cols-1 grid-flow-row-dense auto-cols-max gap-4 justify-items-start">
<Cell shortcut="⇧ + U" text="Copy Session URL with time" />
<Cell shortcut="⇧ + C" text="Launch Console" />
<Cell shortcut="⇧ + N" text="Launch Network" />
<Cell shortcut="⇧ + P" text="Launch Performance" />
<Cell shortcut="⇧ + R" text="Launch State" />
<Cell shortcut="⇧ + E" text="Launch Events" />
<Cell shortcut="⇧ + F" text="Play Session in Fullscreen" />
<Cell shortcut="Space" text="Play/Pause Session" />
<Cell shortcut="⇧ + X" text="Launch X-Ray" />
<Cell shortcut="⇧ + A" text="Launch User Actions" />
<Cell shortcut="⇧ + I" text="Launch More User Info" />
<Cell shortcut="⇧ + M" text="Launch Options Menu" />
<Cell shortcut="⇧ + >" text="Play Next Session" />
<Cell shortcut="⇧ + <" text="Play Previous Session" />
<Cell shortcut="→" text="Skip Forward" />
<Cell shortcut="←" text="Skip Backward" />
<Cell shortcut="↑" text="Playback Speed Up" />
<Cell shortcut="↓" text="Playback Speed Down" />
</div>
);
}

View file

@ -1,12 +1,11 @@
import { ShareAltOutlined } from '@ant-design/icons';
import { Button as AntButton, Switch, Tooltip, Dropdown } from 'antd';
import cn from 'classnames';
import { useModal } from "Components/Modal";
import IssuesModal from "Components/Session_/Issues/IssuesModal";
import IssuesModal from 'Components/Session_/Issues/IssuesModal';
import { Link2, Keyboard } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React, { useMemo } from 'react';
import { MoreOutlined } from '@ant-design/icons'
import { MoreOutlined } from '@ant-design/icons';
import { Icon } from 'UI';
import { PlayerContext } from 'App/components/Session/playerContext';
import { IFRAME } from 'App/constants/storageKeys';
@ -15,12 +14,14 @@ import { checkParam, truncateStringToFit } from 'App/utils';
import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs';
import { ShortcutGrid } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
import WarnBadge from 'Components/Session_/WarnBadge';
import { toast } from "react-toastify";
import { toast } from 'react-toastify';
import HighlightButton from './Highlight/HighlightButton';
import SharePopup from '../shared/SharePopup/SharePopup';
import QueueControls from './QueueControls';
import { Bookmark as BookmarkIcn, BookmarkCheck, Vault } from "lucide-react";
import { Bookmark as BookmarkIcn, BookmarkCheck, Vault } from 'lucide-react';
import { useModal } from 'Components/ModalContext';
import IssueForm from 'Components/Session_/Issues/IssueForm';
const disableDevtools = 'or_devtools_uxt_toggle';
@ -31,11 +32,11 @@ function SubHeader(props) {
sessionStore,
projectsStore,
userStore,
issueReportingStore,
issueReportingStore
} = useStore();
const favorite = sessionStore.current.favorite;
const isEnterprise = userStore.isEnterprise;
const currentSession = sessionStore.current
const currentSession = sessionStore.current;
const projectId = projectsStore.siteId;
const integrations = integrationsStore.issues.list;
const { store } = React.useContext(PlayerContext);
@ -43,7 +44,7 @@ function SubHeader(props) {
const hasIframe = localStorage.getItem(IFRAME) === 'true';
const [hideTools, setHideTools] = React.useState(false);
const [isFavorite, setIsFavorite] = React.useState(favorite);
const { showModal, hideModal } = useModal();
const { openModal, closeModal } = useModal();
React.useEffect(() => {
const hideDevtools = checkParam('hideTools');
@ -62,7 +63,7 @@ function SubHeader(props) {
const issuesIntegrationList = integrationsStore.issues.list;
const handleOpenIssueModal = () => {
issueReportingStore.init();
issueReportingStore.init({});
if (!issueReportingStore.projectsFetched) {
issueReportingStore.fetchProjects().then((projects) => {
if (projects && projects[0]) {
@ -70,13 +71,12 @@ function SubHeader(props) {
}
});
}
showModal(
<IssuesModal
provider={reportingProvider}
sessionId={currentSession.sessionId}
closeHandler={hideModal}
/>
)
openModal(
<IssueForm sessionId={currentSession.sessionId} closeHandler={closeModal} errors={[]} />,
{
title: 'Create Issue'
}
);
};
const reportingProvider = issuesIntegrationList[0]?.provider || '';
@ -90,10 +90,10 @@ function SubHeader(props) {
localStorage.setItem(disableDevtools, enabled ? '0' : '1');
uxtestingStore.setHideDevtools(!enabled);
};
const showKbHelp = () => {
showModal(<ShortcutGrid />, { right: true, width: 320 });
}
openModal(<ShortcutGrid />, { width: 320, title: 'Keyboard Shortcuts' });
};
const vaultIcon = isEnterprise ? (
<Vault size={16} strokeWidth={1} />
@ -115,7 +115,7 @@ function SubHeader(props) {
toast.success(isFavorite ? REMOVED_MESSAGE : ADDED_MESSAGE);
setIsFavorite(!isFavorite);
});
}
};
return (
<>
@ -126,13 +126,13 @@ function SubHeader(props) {
? props.live
? '#F6FFED'
: '#EBF4F5'
: undefined,
: undefined
}}
>
<WarnBadge
siteId={projectId}
siteId={projectId!}
currentLocation={currentLocation}
version={currentSession?.trackerVersion ?? undefined}
version={currentSession?.trackerVersion ?? ""}
/>
<SessionTabs />
@ -164,7 +164,7 @@ function SubHeader(props) {
<Dropdown
menu={{
items: [
{
key: '2',
label: <div className={'flex items-center gap-2'}>
@ -180,7 +180,7 @@ function SubHeader(props) {
<span>Issues</span>
</div>,
disabled: !enabledIntegration,
onClick: handleOpenIssueModal,
onClick: handleOpenIssueModal
},
{
key: '1',

View file

@ -1,60 +1,66 @@
import { makeAutoObservable } from 'mobx';
import ReportedIssue from "../types/session/assignment";
import { issueReportsService } from "App/services";
import ReportedIssue from '../types/session/assignment';
import { issueReportsService } from 'App/services';
import { makePersistable } from '.store/mobx-persist-store-virtual-858ce4d906/package';
export default class issueReportingStore {
instance: ReportedIssue
issueTypes: any[] = []
list: any[] = []
issueTypeIcons: {}
users: any[] = []
projects: any[] = []
issuesFetched = false
projectsFetched = false
projectsLoading = false
metaLoading = false
createLoading = false
export default class IssueReportingStore {
instance!: ReportedIssue;
issueTypes: any[] = [];
list: any[] = [];
issueTypeIcons: Record<string, string> = {};
users: any[] = [];
projects: any[] = [];
issuesFetched = false;
projectsFetched = false;
projectsLoading = false;
metaLoading = false;
createLoading = false;
constructor() {
makeAutoObservable(this);
// void makePersistable(this, {
// name: 'IssueReportingStore',
// properties: ['issueTypes', 'list', 'projects', 'users', 'projectsFetched', 'issuesFetched', 'issueTypeIcons'],
// expireIn: 60000 * 10,
// removeOnExpiration: true,
// storage: window.localStorage
// });
}
init = (instance: any) => {
this.instance = new ReportedIssue(instance);
init = (instanceData: any) => {
this.instance = new ReportedIssue(instanceData);
if (this.issueTypes.length > 0) {
this.instance.issueType = this.issueTypes[0].id;
}
}
};
editInstance = (data: any) => {
const inst = this.instance
this.instance = new ReportedIssue({ ...inst, ...data })
}
editInstance = (data: Partial<ReportedIssue>) => {
Object.assign(this.instance, data);
};
setList = (list: any[]) => {
this.list = list;
this.issuesFetched = true
}
this.issuesFetched = true;
};
setProjects = (projects: any[]) => {
this.projectsFetched = true;
this.projects = projects;
}
};
setMeta = (data: any) => {
const issueTypes = data.issueTypes || [];
const itIcons = {}
const itIcons: Record<string, string> = {};
issueTypes.forEach((it: any) => {
itIcons[it.id] = it.iconUrl
})
itIcons[it.id] = it.iconUrl;
});
this.issueTypes = issueTypes;
this.issueTypeIcons = itIcons;
this.users = data.users || [];
}
};
fetchProjects = async () => {
if (this.projectsLoading) return;
if (this.projectsLoading || this.projects.length > 0) return;
this.projectsLoading = true;
try {
const { data } = await issueReportsService.fetchProjects();
@ -62,11 +68,11 @@ export default class issueReportingStore {
this.projectsFetched = true;
return data;
} catch (e) {
console.error(e)
console.error(e);
} finally {
this.projectsLoading = false;
}
}
};
fetchMeta = async (projectId: number) => {
if (this.metaLoading) return;
@ -75,32 +81,36 @@ export default class issueReportingStore {
const { data } = await issueReportsService.fetchMeta(projectId);
this.setMeta(data);
} catch (e) {
console.error(e)
console.error(e);
} finally {
this.metaLoading = false;
}
}
};
saveIssue = async (sessionId: string, params: any) => {
saveIssue = async (sessionId: string, instance: ReportedIssue) => {
if (this.createLoading) return;
this.createLoading = true;
try {
const data = { ...params, assignee: params.assignee, issueType: params.issueType }
const { data: issue } = await issueReportsService.saveIssue(sessionId, data);
this.init(issue)
const payload = { ...instance, assignee: instance.assignee + '', issueType: instance.issueType + '' };
const resp = await issueReportsService.saveIssue(sessionId, payload);
// const resp = await issueReportsService.saveIssue(sessionId, instance.toCreate());
// const { data: issue } = await issueReportsService.saveIssue(sessionId, payload);
this.init(resp.data.issue);
} catch (e) {
console.error(e)
throw e;
// console.error(e);
} finally {
this.createLoading = false;
}
}
};
fetchList = async (sessionId: string) => {
try {
const { data } = await issueReportsService.fetchIssueIntegrations(sessionId);
this.setList(data);
} catch (e) {
console.error(e)
console.error(e);
}
}
}
};
}

View file

@ -1,15 +1,16 @@
import { makeAutoObservable } from 'mobx';
import Activity, { IActivity } from './activity';
import { DateTime } from 'luxon';
import { notEmptyString } from 'App/validate';
import { notEmptyString } from 'App/validate';
interface IReportedIssue {
export interface IReportedIssue {
id: string;
title: string;
timestamp: number;
timestamp: number | null;
sessionId: string;
projectId: string;
siteId: string;
activities: [];
activities: any[];
closed: boolean;
assignee: string;
commentsCount: number;
@ -17,58 +18,61 @@ interface IReportedIssue {
description: string;
iconUrl: string;
createdAt?: string;
comments: IActivity[]
users: { id: string }[]
comments: IActivity[];
users: { id: string }[];
}
export default class ReportedIssue {
id: IReportedIssue["id"];
title: IReportedIssue["title"] = '';
timestamp: IReportedIssue["timestamp"];
sessionId: IReportedIssue["sessionId"];
projectId: IReportedIssue["projectId"] = '';
siteId: IReportedIssue["siteId"];
activities: IReportedIssue["activities"];
closed: IReportedIssue["closed"];
assignee: IReportedIssue["assignee"] = '';
issueType: IReportedIssue["issueType"] = '';
description: IReportedIssue["description"] = '';
iconUrl: IReportedIssue["iconUrl"] = '';
id: IReportedIssue['id'] = '';
title: IReportedIssue['title'] = '';
timestamp: DateTime | null = null;
sessionId: IReportedIssue['sessionId'] = '';
projectId: IReportedIssue['projectId'] = '';
siteId: IReportedIssue['siteId'] = '';
activities: any[] = [];
closed: IReportedIssue['closed'] = false;
assignee: IReportedIssue['assignee'] = '';
issueType: IReportedIssue['issueType'] = '';
description: IReportedIssue['description'] = '';
iconUrl: IReportedIssue['iconUrl'] = '';
constructor(assignment?: IReportedIssue) {
makeAutoObservable(this);
if (assignment) {
Object.assign(this, {
...assignment,
timestamp: assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : undefined,
activities: assignment.comments ? assignment.comments.map(activity => {
Object.assign(this, assignment);
this.timestamp = assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : null;
this.activities = assignment.comments
? assignment.comments.map((activity) => {
if (assignment.users) {
// @ts-ignore ???
activity.user = assignment.users.filter(user => user.id === activity.author)[0];
// @ts-ignore
activity.user = assignment.users.find(user => user.id === activity.author);
}
return new Activity(activity)
}) : []
})
return new Activity(activity);
})
: [];
}
}
toJS() {
return this
}
validate() {
return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description)
return (
!!this.projectId &&
!!this.issueType &&
notEmptyString(this.title) &&
notEmptyString(this.description)
);
}
get isValid() {
return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description)
return this.validate();
}
toCreate() {
return {
title: this.title,
description: this.description,
assignee: this.assignee,
issueType: this.issueType
}
assignee: this.assignee + '',
issueType: this.issueType + '',
projectId: this.projectId,
};
}
}
}