ui: move assignments to issueReportingStore.ts

This commit is contained in:
nick-delirium 2024-09-13 13:54:19 +02:00
parent 1a2e143888
commit 5d3872c371
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
27 changed files with 324 additions and 1074 deletions

View file

@ -1,21 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'UI';
import { resetActiveIsue } from 'Duck/issues';
const ActiveIssueClose = ({ resetActiveIsue }) => {
return (
<div className="absolute right-0 top-0 mr-4 mt-4">
<Button
variant="text"
onClick={ resetActiveIsue }
>
Close
</Button>
</div>
);
};
export default connect(null, {
resetActiveIsue
})(ActiveIssueClose);

View file

@ -1,13 +0,0 @@
import React from 'react';
class ActivityList extends React.Component {
render() {
return (
<div>
Hello
</div>
);
}
}
export default ActivityList;

View file

@ -1,12 +0,0 @@
import React from 'react';
import cn from 'classnames';
const AuthorAvatar = ({ className, imgUrl, width = 32, height = 32 }) => {
return (
<div className={ cn(className, "img-crcle")}>
<img src={ imgUrl } alt="" width={ width } height={ height } />
</div>
);
};
export default AuthorAvatar;

View file

@ -1,83 +0,0 @@
import React from 'react';
import { CodeBlock } from 'UI';
import stl from './contentRender.module.css';
const elType = {
PARAGRAPH: 'paragraph',
TEXT: 'text',
QUOTE: 'blockquote',
CODE_BLOCK: 'codeBlock',
MENTION: 'mention',
RULE: 'rule',
HARD_BREAK: 'hardBreak',
};
const renderElement = (el, provider) => {
if (provider === 'github') return el;
switch (el.type) {
case elType.PARAGRAPH:
return (
<p className={stl.para}>
<ContentRender message={el} />
</p>
);
case elType.QUOTE:
return (
<blockquote className={stl.quote}>
<ContentRender message={el} />
</blockquote>
);
case elType.CODE_BLOCK:
return (
<CodeBlock
code={codeRender(el.content)[0]}
language={el.attrs.language || ''}
/>
);
// return <CodeMirror
// className={ stl.codeMirror }
// value={ codeRender(el.content)[0] }
// options={{
// mode: el.attrs.language || '',
// theme: 'material',
// lineNumbers: true,
// readOnly: true,
// showCursorWhenSelecting: false,
// scroll: true
// }}
// />
case elType.MENTION:
return <span className={stl.mention}>{`@${el.attrs.text}`}</span>;
case elType.RULE:
return <hr className={stl.rule} />;
case elType.HARD_BREAK:
return <br />;
case elType.RULE:
return <hr className={stl.rule} />;
case elType.TEXT:
return el.text;
}
return <ContentRender key={el.text} message={el} />;
};
const codeRender = (content) => content.map((el) => el.text);
const ContentRender = (props) => {
const { message, provider } = props;
return (
<React.Fragment>
{provider === 'github'
? message
: message &&
message.content &&
message.content.map((el) => (
<React.Fragment>{renderElement(el, provider)}</React.Fragment>
))}
</React.Fragment>
);
};
export default ContentRender;

View file

@ -1,26 +0,0 @@
import React from 'react';
import { checkRecentTime } from 'App/date';
import AuthorAvatar from './AuthoAvatar';
import ContentRender from './ContentRender';
const IssueComment = ({ activity, provider }) => {
return (
<div className="mb-4 flex">
<AuthorAvatar
className="flex-shrink-0 mr-4"
imgUrl={ activity.user && activity.user.avatarUrls['24x24'] }
width={24}
height={24}
/>
<div className="flex flex-col flex-1 mb-2">
<div className="flex">
<span className="font-medium mr-3">{ activity.user && activity.user.name }</span>
<span className="text-sm ">{ activity.createdAt && checkRecentTime(activity.createdAt) }</span>
</div>
<div>{ <ContentRender message={ activity.message } provider={provider} /> }</div>
</div>
</div>
);
};
export default IssueComment;

View file

@ -1,54 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Input, Button, Form } from 'UI';
import { addMessage } from 'Duck/assignments';
class IssueCommentForm extends React.PureComponent {
state = { comment: '' }
write = (e) => {
e.stopPropagation();
const { target: { name, value } } = e
this.setState({ comment: value });
}
addComment = () => {
const { comment } = this.state;
const { sessionId, issueId, addMessage, loading } = this.props;
if (loading) return;
addMessage(sessionId, issueId, { message: comment, type: 'message' }).then(() => {
this.setState({comment: ''});
})
}
render() {
const { comment } = this.state;
const { loading } = this.props;
return (
<div className="p-6 bg-white">
<h5 className="mb-1">Add Comment</h5>
<Form onSubmit={ this.addComment }>
<div className="flex mt-2 items-center">
<Input
onChange={ this.write }
value={ comment }
fluid
placeholder="Type here"
className="flex-1 mr-3"
/>
<Button
disabled={ comment.length === 0 }
variant="primary"
loading={ loading }>{'Comment'}</Button>
</div>
</Form>
</div>
);
}
};
export default connect(state => ({
loading: state.getIn(['assignments', 'addMessage', 'loading'])
}), { addMessage })(IssueCommentForm)

View file

@ -1,13 +0,0 @@
import React from 'react';
import ContentRender from './ContentRender';
const IssueDescription = ({ className, description, provider }) => {
return (
<div className={ className }>
<h5 className="mb-4">Description</h5>
<ContentRender message={ description } provider={provider} />
</div>
);
};
export default IssueDescription;

View file

@ -1,57 +0,0 @@
import { connect } from 'react-redux';
import React from 'react';
import cn from 'classnames';
import { Loader } from 'UI';
import IssueHeader from './IssueHeader';
import IssueCommentForm from './IssueCommentForm';
import IssueComment from './IssueComment';
import stl from './issueDetails.module.css';
import IssueDescription from './IssueDescription';
class IssueDetails extends React.PureComponent {
state = { searchQuery: ''}
write = (e, { name, value }) => this.setState({ [ name ]: value });
render() {
const { sessionId, issue, loading, users, issueTypeIcons, issuesIntegration } = this.props;
const activities = issue.activities;
const provider = issuesIntegration.provider;
const assignee = users.filter(({id}) => issue.assignee === id).first();
return (
<div className="flex flex-col">
<Loader loading={ loading }>
<div>
<IssueHeader
issue={ issue }
typeIcon={ issueTypeIcons[issue.issueType] }
assignee={ assignee }
onSearchComment={ this.write }
/>
<div className={ cn( stl.activitiesList, 'p-6') }>
<IssueDescription
className="mb-10"
description={ issue.description }
provider={provider}
/>
{ activities.size > 0 && <h5 className="mb-4">Comments</h5>}
{ activities.map(activity => (
<IssueComment activity={ activity } key={activity.key} provider={provider} />
))}
</div>
<IssueCommentForm sessionId={ sessionId } issueId={ issue.id } />
</div>
</Loader>
</div>
);
}
}
export default connect(state => ({
users: state.getIn(['assignments', 'users']),
loading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']),
issuesIntegration: state.getIn([ 'issues', 'list'])[0] || {},
}))(IssueDetails);

View file

@ -1,182 +1,178 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { connect } from 'react-redux';
import { Form, Input, Button, CircularLoader, Loader } from 'UI';
import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assignments';
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">
{/* <img className="mr-2" src={ icon } width="13" height="13" /> */}
{icon}
<span>{text}</span>
</div>
);
};
class IssueForm extends React.PureComponent {
componentDidMount() {
const { projects, issueTypes } = this.props;
this.props.init({
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;
console.log(users, instance, projects, issueTypes)
React.useEffect(() => {
init({
projectId: projects[0] ? projects[0].id : '',
issueType: issueTypes[0] ? issueTypes[0].id : '',
});
}
}, []);
componentWillReceiveProps(newProps) {
const { instance } = this.props;
if (newProps.instance.projectId && newProps.instance.projectId != instance.projectId) {
this.props.fetchMeta(newProps.instance.projectId).then(() => {
this.props.edit({ issueType: '', assignee: '', projectId: newProps.instance.projectId });
React.useEffect(() => {
if (instance?.projectId) {
fetchMeta(instance?.projectId).then(() => {
edit({
issueType: '',
assignee: '',
projectId: instance?.projectId,
});
});
}
}
}, [instance?.projectId]);
onSubmit = () => {
const { sessionId, addActivity } = this.props;
const { instance } = this.props;
addActivity(sessionId, instance.toJS()).then(() => {
const { errors } = this.props;
const onSubmit = () => {
const { sessionId } = props;
addActivity(sessionId, instance).then(() => {
const { errors } = props;
if (!errors || errors.length === 0) {
this.props.init({ projectId: instance.projectId });
this.props.fetchAssignments(sessionId);
this.props.closeHandler();
init({ projectId: instance?.projectId });
closeHandler();
}
});
};
write = (e) => {
const write = (e) => {
const {
target: { name, value },
} = e;
this.props.edit({ [name]: value });
edit({ [name]: value });
};
writeOption = ({ name, value }) => this.props.edit({ [name]: value.value });
render() {
const {
creating,
projects,
users,
issueTypes,
instance,
closeHandler,
metaLoading,
projectsLoading,
} = this.props;
const projectOptions = projects.map(({ name, id }) => ({ label: name, value: id })).toArray();
const userOptions = users.map(({ name, id }) => ({ label: name, value: id })).toArray();
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 issueTypeOptions = issueTypes.map(({ name, id, iconUrl, color }) => {
return { label: name, value: id, iconUrl, color };
});
const selectedIssueType = issueTypes.filter((issue) => issue.id == instance.issueType)[0];
const selectedIssueType = issueTypes.filter(
(issue) => issue.id == instance?.issueType
)[0];
return (
<Loader loading={projectsLoading} size={40}>
<Form onSubmit={this.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={this.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={this.writeOption}
placeholder="Select issue type"
text={
selectedIssueType ? (
<SelectedValue icon={selectedIssueType.iconUrl} text={selectedIssueType.name} />
) : (
''
)
}
/>
</Form.Field>
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={this.writeOption}
placeholder="Select a user"
/>
</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={this.write}
/>
</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
{/* <span className="text-sm text-gray-500">(Optional)</span> */}
</label>
<textarea
name="description"
rows="2"
value={instance.description}
placeholder="E.g. Found this issue at 3:29secs"
onChange={this.write}
className="text-area"
/>
</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>
);
}
<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 connect(
(state) => ({
creating: state.getIn(['assignments', 'addActivity', 'loading']),
projects: state.getIn(['assignments', 'projects']),
projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']),
users: state.getIn(['assignments', 'users']),
instance: state.getIn(['assignments', 'instance']),
metaLoading: state.getIn(['assignments', 'fetchMeta', 'loading']),
issueTypes: state.getIn(['assignments', 'issueTypes']),
errors: state.getIn(['assignments', 'addActivity', 'errors']),
}),
{ addActivity, init, edit, fetchAssignments, fetchMeta }
)(IssueForm);
export default observer(IssueForm);

View file

@ -1,33 +0,0 @@
import React from 'react';
import { Icon } from 'UI';
const GotoSessionLink = props => (
<a className="flex items-center absolute right-0 mr-3 cursor-pointer">
{'Go to session'}
<Icon name="next1" size="16" />
</a>
)
const IssueHeader = ({issue, typeIcon, assignee}) => {
return (
<div className="relative p-6 bg-white">
{/* <GotoSessionLink /> */}
{/* <ActiveIssueClose /> */}
<div className="flex leading-none mb-2 items-center">
{ typeIcon }
{/* <img className="mr-2" src={typeIcon} alt="" width={16} height={16} /> */}
<span className="mr-2 font-medium">{ issue.id }</span>
{/* <div className="text-gray-700 text-sm">{ '@ 00:13 Secs'}</div> */}
{ assignee &&
<div>
<span className="text-gray-600 mr-2">{'Assigned to'}</span>
<span className="font-medium" key={ assignee.id }>{ assignee.name }</span>
</div>
}
</div>
<h2 className="text-xl font-medium mb-2 truncate">{issue.title}</h2>
</div>
);
};
export default IssueHeader;

View file

@ -1,34 +0,0 @@
import React from 'react';
import cn from 'classnames';
import { Tooltip } from 'UI';
import stl from './issueListItem.module.css';
const IssueListItem = ({ issue, onClick, icon, user, active }) => {
return (
<div
onClick={() => onClick(issue)}
className={cn(
stl.wrapper,
active ? 'active-bg' : '',
'flex flex-col justify-between cursor-pointer text-base text-gray-800'
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
{icon}
<span>{issue.id}</span>
</div>
<div className="flex items-center">
{user && (
<Tooltip title={'Assignee ' + user.name}>
<img src={user.avatarUrls['24x24']} width="24" height="24" />
</Tooltip>
)}
</div>
</div>
<div className={stl.title}>{issue.title}</div>
</div>
);
};
export default IssueListItem;

View file

@ -1,81 +1,56 @@
import { Button, Tooltip } from 'antd';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { useStore } from 'App/mstore';
import { Icon, Popover } from 'UI';
import { connect } from 'react-redux';
import { Popover, Icon } from 'UI';
import IssuesModal from './IssuesModal';
import { fetchProjects, fetchMeta } from 'Duck/assignments';
import { Tooltip, Button } from 'antd';
@connect(
(state) => ({
issues: state.getIn(['assignments', 'list']),
metaLoading: state.getIn(['assignments', 'fetchMeta', 'loading']),
projects: state.getIn(['assignments', 'projects']),
projectsFetched: state.getIn(['assignments', 'projectsFetched']),
activeIssue: state.getIn(['assignments', 'activeIssue']),
fetchIssueLoading: state.getIn(['assignments', 'fetchAssignment', 'loading']),
fetchIssuesLoading: state.getIn(['assignments', 'fetchAssignments', 'loading']),
projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']),
issuesIntegration: state.getIn(['issues', 'list']) || {},
issuesFetched: state.getIn(['issues', 'issuesFetched']),
}),
{ fetchMeta, fetchProjects }
)
class Issues extends React.Component {
state = { showModal: false };
function Issues(props) {
const { issueReportingStore } = useStore();
constructor(props) {
super(props);
this.state = { showModal: false };
}
closeModal = () => {
this.setState({ showModal: false });
};
showIssuesList = (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({ showModal: true });
};
handleOpen = () => {
this.setState({ showModal: true });
if (!this.props.projectsFetched) {
// cache projects fetch
this.props.fetchProjects().then(
function () {
const { projects } = this.props;
if (projects && projects.first()) {
this.props.fetchMeta(projects.first().id);
}
}.bind(this)
);
const handleOpen = () => {
issueReportingStore.init();
if (!issueReportingStore.projectsFetched) {
issueReportingStore.fetchProjects().then((projects) => {
if (projects && projects[0]) {
void issueReportingStore.fetchMeta(projects[0].id);
}
});
}
};
render() {
const { sessionId, issuesIntegration } = this.props;
const provider = issuesIntegration.first()?.provider || '';
const { sessionId, issuesIntegration } = props;
const provider = issuesIntegration[0]?.provider || '';
return (
<Popover
onOpen={this.handleOpen}
render={({ close }) => (
<div>
<IssuesModal provider={provider} sessionId={sessionId} closeHandler={close} />
</div>
)}
>
console.log(provider, sessionId, issuesIntegration)
return (
<Popover
onOpen={handleOpen}
render={({ close }) => (
<div>
<Tooltip title={'Create Issue'} placement='bottom'>
<Button size={'small'} className={'flex items-center justify-center'}>
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} />
</Button>
</Tooltip>
<IssuesModal
provider={provider}
sessionId={sessionId}
closeHandler={close}
/>
</div>
</Popover>
);
}
)}
>
<div>
<Tooltip title={'Create Issue'} placement="bottom">
<Button size={'small'} className={'flex items-center justify-center'}>
<Icon
name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`}
/>
</Button>
</Tooltip>
</div>
</Popover>
);
}
export default Issues;
export default connect((state) => ({
issuesIntegration: state.getIn(['issues', 'list']) || {},
issuesFetched: state.getIn(['issues', 'issuesFetched']),
}))(observer(Issues));

View file

@ -7,12 +7,10 @@ import store from 'App/store';
const IssuesModal = ({
sessionId,
closeHandler,
provider
}) => {
return (
<div className={ stl.wrapper }>
<h3 className="text-xl font-semibold">
{/* <Icon name={headerIcon} size="18" color="color-gray-darkest" /> */}
<span>Create Issue</span>
</h3>
<Provider store={store}>

View file

@ -1,32 +0,0 @@
.codeMirror > div {
border: 1px solid #eee;
height: auto;
}
.para {
padding: 3px 0;
}
.mention {
font-weight: 500;
margin-right: 5px;
}
.quote {
padding: 5px;
padding-left: 10px;
border-left: solid 2px $gray-light;
color: $gray-dark;
margin: 5px 0;
margin-left: 10px;
}
.code {
background-color: lightgray;
padding: 2px 5px;
}
.rule {
margin: 6px 0;
}

View file

@ -1,2 +1 @@
export { defualt as Issues } from './Issues';
export { defualt as IssueModal } from './IssuesModal';
export { defualt as Issues } from './Issues';

View file

@ -1,8 +0,0 @@
.activitiesList {
max-height: calc(100vh - 186px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
}
}

View file

@ -1,21 +0,0 @@
.title {
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wrapper {
transition: all 0.4s;
padding: 8px 14px;
/* margin: 0 -14px; */
height: 70px;
background-color: white;
border-bottom: solid thin $gray-light;
&:hover {
background-color: $active-blue;
transition: all 0.2s;
}
}

View file

@ -1,300 +0,0 @@
import { storiesOf } from '@storybook/react';
import { List } from 'immutable';
import IssuesModal from './IssuesModal';
import IssueHeader from './IssueHeader';
import IssueComment from './IssueComment';
import IssueCommentForm from './IssueCommentForm';
import IssueDetails from './IssueDetails';
import IssueForm from './IssueForm';
import IssueListItem from './IssueListItem';
import IssueDescription from './IssueDescription';
const description = {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "test para"
}
]
},
{
"type": "blockquote",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "test quote"
}
]
}
]
},
{
"type": "rule"
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "another para"
}
]
},
{
"type": "paragraph",
"content": []
},
{
"type": "paragraph",
"content": []
},
{
"type": "codeBlock",
"attrs": {},
"content": [
{
"type": "text",
"text": "var d = \"test code\"\nvar e = \"test new line\""
}
]
},
{
"type": "paragraph",
"content": []
}
]
}
const issueTypeIcons = {
'1': 'https://openreplay.atlassian.net/secure/viewavatar?size=medium&avatarId=10310&avatarType=issuetype',
}
const issueTypes = [
{
id: 1,
iconUrl: 'https://openreplay.atlassian.net/secure/viewavatar?size=medium&avatarId=10310&avatarType=issuetype',
name: 'Improvement'
},
{
id: 2,
iconUrl: 'https://openreplay.atlassian.net/secure/viewavatar?size=medium&avatarId=10310&avatarType=issuetype',
name: 'Bug'
}
]
const user = {
id: 1,
name: 'test',
avatarUrls: {
"16x16": "https://secure.gravatar.com/avatar/900294aa68b33490b16615b57e9709fc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMO-3.png&size=16&s=16",
"24x24": "https://secure.gravatar.com/avatar/900294aa68b33490b16615b57e9709fc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMO-3.png&size=24&s=24"
}
}
const activities = [
{
id: 1,
message: {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "First Para"
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Second para"
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Third para",
"marks": [
{
"type": "strong"
}
]
}
]
},
{
"type": "blockquote",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Quote"
}
]
}
]
},
{
"type": "paragraph",
"content": []
},
{
"type": "codeBlock",
"attrs": {},
"content": [
{
"type": "text",
"text": "alert('test')\nvar dd = \"Another line\""
}
]
},
{
"type": "rule"
},
{
"type": "paragraph",
"content": [
{
"type": "mention",
"attrs": {
"id": "5d8398868a50e80c2feed3f6",
"text": "Someone"
}
},
{
"type": "text",
"text": " "
}
]
},
{
"type": "paragraph",
"content": []
},
{
"type": "codeBlock",
"attrs": {
"language": "javascript"
},
"content": [
{
"type": "text",
"text": "var d = \"test\""
}
]
},
{
"type": "paragraph",
"content": []
}
]
},
author: 1,
user: user
},
{
id: 1,
message: {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "happy debugging"
}
]
}
]
},
author: 1,
user: user
}
]
const issues = [
{
title: 'Crash Report - runtime error: index out of range',
description: description,
commentsCount: 4,
activities: List(activities),
assignee: user.id,
issueType: 1,
id: 'APP-222'
},
{
title: 'this is the second one',
description: description,
commentsCount: 10,
activities: activities,
assignee: user.id,
issueType: 1,
id: 'APP-333'
},
{
title: 'this is the third one',
description: description,
commentsCount: 0,
activities: activities,
assignee: user.id,
issueType: 1,
id: 'APP-444'
}
];
const onIssueClick = (issue) => {
console.log(issue);
}
storiesOf('Issues', module)
.add('IssuesModal', () => (
<IssuesModal issues={ issues } />
))
.add('IssueHeader', () => (
<IssueHeader issue={ issues[0] } typeIcon={ issueTypeIcons[1] }/>
))
.add('IssueComment', () => (
<div className="p-4">
<IssueComment activity={ activities[0] } />
</div>
))
.add('IssueDescription', () => (
<div className="p-4 bg-gray-100">
<IssueDescription description={ description } />
</div>
))
.add('IssueCommentForm', () => (
<IssueCommentForm />
))
.add('IssueDetails', () => (
<IssueDetails
sessionId={ 1}
issue={ issues[0] }
users={ List([user]) }
issueTypeIcons={ issueTypeIcons }
/>
))
.add('IssueListItem', () => (
<IssueListItem issue={ issues[0] } icon={ issueTypeIcons[1] } user={ user } />
))
.add('IssueForm', () => (
<div className="p-4">
<IssueForm issueTypes={ List(issueTypes) } />
</div>
))

View file

@ -1,116 +0,0 @@
import { List, Map, Set } from 'immutable';
import Assignment from 'Types/session/assignment';
import Activity from 'Types/session/activity';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { createListUpdater } from './funcTools/tools';
import { editType, initType } from './funcTools/crud/types';
import { createInit, createEdit } from './funcTools/crud';
const idKey = 'id';
const name = 'assignment';
const listUpdater = createListUpdater(idKey);
const FETCH_PROJECTS = new RequestTypes('asignment/FETCH_PROJECTS');
const FETCH_META = new RequestTypes('asignment/FETCH_META');
const FETCH_ASSIGNMENTS = new RequestTypes('asignment/FETCH_ASSIGNMENTS');
const FETCH_ASSIGNMENT = new RequestTypes('asignment/FETCH_ASSIGNMENT');
const ADD_ACTIVITY = new RequestTypes('asignment/ADD_ACTIVITY');
const ADD_MESSAGE = new RequestTypes('asignment/ADD_MESSAGE');
const EDIT = editType(name);
const INIT = initType(name);
const initialState = Map({
list: List(),
instance: new Assignment(),
activeIssue: new Assignment(),
issueTypes: List(),
issueTypeIcons: Set(),
users: List(),
projects: List(),
projectsFetched: false
});
const reducer = (state = initialState, action = {}) => {
const users = state.get('users');
let issueTypes = []
switch (action.type) {
case INIT:
action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : '';
return state.set('instance', new Assignment(action.instance));
case EDIT:
const inst = state.get('instance')
return state.set('instance', new Assignment({ ...inst, ...action.instance }));
case FETCH_PROJECTS.SUCCESS:
return state.set('projects', List(action.data)).set('projectsFetched', true);
case FETCH_ASSIGNMENTS.SUCCESS:
return state.set('list', List(action.data).map(as => new Assignment(as)));
case FETCH_ASSIGNMENT.SUCCESS:
return state.set('activeIssue', new Assignment({ ...action.data, users}));
case FETCH_META.SUCCESS:
issueTypes = action.data.issueTypes
const issueTypeIcons = {}
issueTypes.forEach(iss => {
issueTypeIcons[iss.id] = iss.iconUrl
})
return state.set('issueTypes', issueTypes)
.set('users', List(action.data.users))
.set('issueTypeIcons', issueTypeIcons)
case ADD_ACTIVITY.SUCCESS:
const instance = new Assignment(action.data);
return listUpdater(state, instance);
case ADD_MESSAGE.SUCCESS:
const user = users.filter(user => user.id === action.data.author).first();
const activity = new Activity({ type: 'message', user, ...action.data,});
return state.update([ 'activeIssue' ], issue => issue.activities.push(activity));
default:
return state;
}
};
export default withRequestState({
fetchProjects: FETCH_PROJECTS,
fetchMeta: FETCH_META,
fetchAssignments: FETCH_ASSIGNMENTS,
addActivity: ADD_ACTIVITY,
fetchAssignment: FETCH_ASSIGNMENT,
addMessage: ADD_MESSAGE
}, reducer);
export const init = createInit(name);
export const edit = createEdit(name);
export function fetchProjects() {
return {
types: FETCH_PROJECTS.toArray(),
call: client => client.get(`/integrations/issues/list_projects`)
};
}
export function fetchMeta(projectId) {
return {
types: FETCH_META.toArray(),
call: client => client.get(`/integrations/issues/${projectId}`)
}
}
export function fetchAssignments(sessionId) {
return {
types: FETCH_ASSIGNMENTS.toArray(),
call: client => client.get(`/sessions/${ sessionId }/assign`)
}
}
export function addActivity(sessionId, params) {
const data = { ...params, assignee: params.assignee, issueType: params.issueType }
return {
types: ADD_ACTIVITY.toArray(),
call: client => client.post(`/sessions/${ sessionId }/assign/projects/${params.projectId}`, data),
}
}
export function addMessage(sessionId, assignmentId, params) {
return {
types: ADD_MESSAGE.toArray(),
call: client => client.post(`/sessions/${ sessionId }/assign/${ assignmentId }/comment`, params),
}
}

View file

@ -3,7 +3,6 @@ import { combineReducers } from 'redux-immutable';
import user from './user';
import sessions from './sessions';
import assignments from './assignments';
import filters from './filters';
import funnelFilters from './funnelFilters';
import sources from './sources';
@ -19,7 +18,6 @@ import liveSearch from './liveSearch';
const rootReducer = combineReducers({
user,
sessions,
assignments,
filters,
funnelFilters,
site,

View file

@ -1,4 +1,4 @@
import Assignment from 'Types/session/assignment';
import ReportedIssue from 'Types/session/assignment';
import Activity from 'Types/session/activity';
import { List, Map, Set } from 'immutable';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
@ -22,8 +22,8 @@ const RESET_ACTIVE_ISSUE = 'assignment/RESET_ACTIVE_ISSUE';
const initialState = Map({
list: List(),
instance: new Assignment(),
activeIssue: new Assignment(),
instance: new ReportedIssue(),
activeIssue: new ReportedIssue(),
issueTypes: List(),
issueTypeIcons: Set(),
users: List(),
@ -38,9 +38,9 @@ const reducer = (state = initialState, action = {}) => {
case FETCH_PROJECTS.SUCCESS:
return state.set('projects', List(action.data));
case FETCH_ASSIGNMENTS.SUCCESS:
return state.set('list', action.data.map(as => new Assignment(as)));
return state.set('list', action.data.map(as => new ReportedIssue(as)));
case ADD_ACTIVITY.SUCCESS:
const instance = new Assignment(action.data);
const instance = new ReportedIssue(action.data);
return listUpdater(state, instance);
case FETCH_META.SUCCESS:
issueTypes = action.data.issueTypes;
@ -52,16 +52,16 @@ const reducer = (state = initialState, action = {}) => {
.set('users', List(action.data.users))
.set('issueTypeIcons', issueTypeIcons)
case FETCH_ISSUE.SUCCESS:
return state.set('activeIssue', new Assignment({ ...action.data, users}));
return state.set('activeIssue', new ReportedIssue({ ...action.data, users}));
case RESET_ACTIVE_ISSUE:
return state.set('activeIssue', new Assignment());
return state.set('activeIssue', new ReportedIssue());
case ADD_MESSAGE.SUCCESS:
const user = users.filter(user => user.id === action.data.author).first();
const activity = new Activity({ type: 'message', user, ...action.data,});
return state.updateIn([ 'activeIssue', 'activities' ], list => list.push(activity));
case INIT:
action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : '';
return state.set('instance', new Assignment(action.instance));
return state.set('instance', new ReportedIssue(action.instance));
case EDIT:
return state.mergeIn([ 'instance' ], action.instance);
default:
@ -80,13 +80,6 @@ export default withRequestState({
export const init = createInit(name);
export const edit = createEdit(name);
export function fetchAssignments(sessionId) {
return {
types: FETCH_ASSIGNMENTS.toArray(),
call: client => client.get(`/sessions/${ sessionId }/assign`)
}
}
export function resetActiveIsue() {
return {
type: RESET_ACTIVE_ISSUE

View file

@ -25,6 +25,7 @@ import SpotStore from "./spotStore";
import LoginStore from "./loginStore";
import FilterStore from "./filterStore";
import UiPlayerStore from './uiPlayerStore';
import IssueReportingStore from './issueReportingStore';
export class RootStore {
dashboardStore: DashboardStore;
@ -51,6 +52,7 @@ export class RootStore {
loginStore: LoginStore;
filterStore: FilterStore;
uiPlayerStore: UiPlayerStore;
issueReportingStore: IssueReportingStore;
constructor() {
this.dashboardStore = new DashboardStore();
@ -77,6 +79,7 @@ export class RootStore {
this.loginStore = new LoginStore();
this.filterStore = new FilterStore();
this.uiPlayerStore = new UiPlayerStore();
this.issueReportingStore = new IssueReportingStore();
}
initClient() {

View file

@ -0,0 +1,90 @@
import { makeAutoObservable } from 'mobx';
import ReportedIssue from "../types/session/assignment";
import { issueReportsService } from "App/services";
export default class issueReportingStore {
instance: ReportedIssue
issueTypes: any[] = []
issueTypeIcons: {}
users: any[] = []
projects: any[] = []
projectsFetched = false
projectsLoading = false
metaLoading = false
createLoading = false
constructor() {
makeAutoObservable(this);
}
init = (instance: any) => {
this.instance = new ReportedIssue(instance);
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 })
}
setProjects = (projects: any[]) => {
this.projectsFetched = true;
this.projects = projects;
}
setMeta = (data: any) => {
const issueTypes = data.issueTypes || [];
const itIcons = {}
issueTypes.forEach((it: any) => {
itIcons[it.id] = it.iconUrl
})
this.issueTypes = issueTypes;
this.issueTypeIcons = itIcons;
this.users = data.users || [];
}
fetchProjects = async () => {
if (this.projectsLoading) return;
this.projectsLoading = true;
try {
const { data } = await issueReportsService.fetchProjects();
this.setProjects(data);
this.projectsFetched = true;
return data;
} catch (e) {
console.error(e)
} finally {
this.projectsLoading = false;
}
}
fetchMeta = async (projectId: number) => {
if (this.metaLoading) return;
this.metaLoading = true;
try {
const { data } = await issueReportsService.fetchMeta(projectId);
this.setMeta(data);
} catch (e) {
console.error(e)
} finally {
this.metaLoading = false;
}
}
saveIssue = async (sessionId: string, params: any) => {
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)
} catch (e) {
console.error(e)
} finally {
this.createLoading = false;
}
}
}

View file

@ -0,0 +1,21 @@
import BaseService from 'App/services/BaseService';
export default class IssueReportsService extends BaseService {
fetchProjects = async () => {
const r = await this.client.get(`/integrations/issues/list_projects`)
return await r.json();
}
fetchMeta = async (projectId: number) => {
const r = await this.client.get(`/integrations/issues/${projectId}`)
return await r.json();
}
saveIssue = async (sessionId: string, data: any) => {
const r = await this.client.post(`/sessions/${ sessionId }/assign/projects/${data.projectId}`, data)
return await r.json();
}
}

View file

@ -20,6 +20,7 @@ import WebhookService from './WebhookService';
import SpotService from './spotService';
import LoginService from "./loginService";
import FilterService from "./FilterService";
import IssueReportsService from "./IssueReportsService";
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
@ -42,6 +43,7 @@ export const aiService = new AiService();
export const spotService = new SpotService();
export const loginService = new LoginService();
export const filterService = new FilterService();
export const issueReportsService = new IssueReportsService();
export const services = [
dashboardService,
@ -65,4 +67,5 @@ export const services = [
spotService,
loginService,
filterService,
issueReportsService,
];

View file

@ -2,11 +2,10 @@ import Activity, { IActivity } from './activity';
import { DateTime } from 'luxon';
import { notEmptyString } from 'App/validate';
interface IAssignment {
interface IReportedIssue {
id: string;
title: string;
timestamp: number;
creatorId: string;
sessionId: string;
projectId: string;
siteId: string;
@ -22,23 +21,21 @@ interface IAssignment {
users: { id: string }[]
}
export default class Assignment {
id: IAssignment["id"];
title: IAssignment["title"] = '';
timestamp: IAssignment["timestamp"];
creatorId: IAssignment["creatorId"];
sessionId: IAssignment["sessionId"];
projectId: IAssignment["projectId"] = '';
siteId: IAssignment["siteId"];
activities: IAssignment["activities"];
closed: IAssignment["closed"];
assignee: IAssignment["assignee"] = '';
commentsCount: IAssignment["commentsCount"];
issueType: IAssignment["issueType"] = '';
description: IAssignment["description"] = '';
iconUrl: IAssignment["iconUrl"] = '';
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"] = '';
constructor(assignment?: IAssignment) {
constructor(assignment?: IReportedIssue) {
if (assignment) {
Object.assign(this, {
...assignment,