Merge branch 'dev' into clickmap-fix

This commit is contained in:
Alex Kaminskii 2023-01-30 15:18:30 +01:00
commit 368159aed3
24 changed files with 83 additions and 110 deletions

View file

@ -25,24 +25,24 @@ def __get_autocomplete_table(value, project_id):
if e == schemas.FilterType.user_country:
c_list = countries.get_country_code_autocomplete(value)
if len(c_list) > 0:
sub_queries.append(f"""(SELECT DISTINCT ON(value) type, value
sub_queries.append(f"""(SELECT DISTINCT ON(value) '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value IN %(c_list)s)""")
continue
sub_queries.append(f"""(SELECT type, value
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value ILIKE %(svalue)s
ORDER BY value
LIMIT 5)""")
if len(value) > 2:
sub_queries.append(f"""(SELECT type, value
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value ILIKE %(value)s
ORDER BY value
LIMIT 5)""")
@ -62,8 +62,11 @@ def __get_autocomplete_table(value, project_id):
print(value)
print("--------------------")
raise err
results = helper.list_to_camel_case(cur.fetchall())
return results
results = cur.fetchall()
for r in results:
r["type"] = r.pop("_type")
results = helper.list_to_camel_case(results)
return results
def __generic_query(typename, value_length=None):

View file

@ -25,24 +25,24 @@ def __get_autocomplete_table(value, project_id):
if e == schemas.FilterType.user_country:
c_list = countries.get_country_code_autocomplete(value)
if len(c_list) > 0:
sub_queries.append(f"""(SELECT DISTINCT ON(value) type, value
sub_queries.append(f"""(SELECT DISTINCT ON(value) '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value IN %(c_list)s)""")
continue
sub_queries.append(f"""(SELECT type, value
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value ILIKE %(svalue)s
ORDER BY value
LIMIT 5)""")
if len(value) > 2:
sub_queries.append(f"""(SELECT type, value
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
FROM {TABLE}
WHERE project_id = %(project_id)s
AND type= '{e}'
AND type= '{e.value.upper()}'
AND value ILIKE %(value)s
ORDER BY value
LIMIT 5)""")
@ -64,7 +64,10 @@ def __get_autocomplete_table(value, project_id):
print(value)
print("--------------------")
raise err
return results
for r in results:
r["type"] = r.pop("_type")
results = helper.list_to_camel_case(results)
return results
def __generic_query(typename, value_length=None):

View file

@ -95,13 +95,11 @@ export default class APIClient {
edp = `${ edp }/${ this.siteId }`
}
return fetch(edp + path, this.init)
.then(response => {
.then((response) => {
if (response.ok) {
return response
} else {
throw new Error(
`! ${this.init.method} error on ${path}; ${response.status}`
)
return Promise.reject({ message: `! ${this.init.method} error on ${path}; ${response.status}`, response });
}
})
}

View file

@ -33,9 +33,10 @@ export default () => (next) => (action) => {
next({ type: UPDATE_JWT, data: jwt });
}
})
.catch((e) => {
.catch(async (e) => {
const data = await e.response.json();
logger.error('Error during API request. ', e);
return next({ type: FAILURE, errors: parseError(e) });
return next({ type: FAILURE, errors: parseError(data.errors) });
});
};

View file

@ -39,17 +39,6 @@ class CustomFieldForm extends React.PureComponent {
placeholder="Field Name"
/>
</Form.Field>
{/* TODO: errors state is not updating properly */}
{/* {errors && (
<div className="mb-3">
{errors.map((error, i) => (
<Message visible={errors} size="mini" error key={i} className={styles.error}>
{error.message}
</Message>
))}
</div>
)} */}
<div className="flex justify-between">
<div className="flex items-center">

View file

@ -11,6 +11,7 @@ import ListItem from './ListItem';
import { confirm } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { useModal } from 'App/components/Modal';
import { toast } from 'react-toastify';
function CustomFields(props) {
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
@ -25,10 +26,11 @@ function CustomFields(props) {
}, []);
const save = (field) => {
props.save(currentSite.id, field).then(() => {
const { errors } = props;
if (!errors || errors.size === 0) {
props.save(currentSite.id, field).then((response) => {
if (!response || !response.errors || response.errors.size === 0) {
hideModal();
} else {
toast.error(response.errors[0]);
}
});
};
@ -73,7 +75,7 @@ function CustomFields(props) {
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>

View file

@ -64,7 +64,7 @@ function Integrations(props: Props) {
}, []);
const onClick = (integration: any, width: number) => {
if (integration.slug) {
if (integration.slug && integration.slug !== 'slack' && integration.slug !== 'msteams') {
props.fetch(integration.slug, props.siteId);
}
@ -241,30 +241,15 @@ const integrations = [
description:
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
integrations: [
{ title: 'Redux', slug: 'redux', icon: 'integrations/redux', component: <ReduxDoc /> },
{ title: 'VueX', slug: 'vuex', icon: 'integrations/vuejs', component: <VueDoc /> },
{ title: 'Pinia', slug: 'pinia', icon: 'integrations/pinia', component: <PiniaDoc /> },
{
title: 'GraphQL',
slug: 'graphql',
icon: 'integrations/graphql',
component: <GraphQLDoc />,
},
{ title: 'NgRx', slug: 'ngrx', icon: 'integrations/ngrx', component: <NgRxDoc /> },
{ title: 'MobX', slug: 'mobx', icon: 'integrations/mobx', component: <MobxDoc /> },
{
title: 'Profiler',
slug: 'profiler',
icon: 'integrations/openreplay',
component: <ProfilerDoc />,
},
{
title: 'Assist',
slug: 'assist',
icon: 'integrations/openreplay',
component: <AssistDoc />,
},
{ title: 'Zustand', slug: 'zustand', icon: '', header: '🐻', component: <ZustandDoc /> },
{ title: 'Redux', icon: 'integrations/redux', component: <ReduxDoc /> },
{ title: 'VueX', icon: 'integrations/vuejs', component: <VueDoc /> },
{ title: 'Pinia', icon: 'integrations/pinia', component: <PiniaDoc /> },
{ title: 'GraphQL', icon: 'integrations/graphql', component: <GraphQLDoc /> },
{ title: 'NgRx', icon: 'integrations/ngrx', component: <NgRxDoc /> },
{ title: 'MobX', icon: 'integrations/mobx', component: <MobxDoc /> },
{ title: 'Profiler', icon: 'integrations/openreplay', component: <ProfilerDoc /> },
{ title: 'Assist', icon: 'integrations/openreplay', component: <AssistDoc /> },
{ title: 'Zustand', icon: '', header: '🐻', component: <ZustandDoc /> },
],
},
];

View file

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { Button, Message, Form, Input } from 'UI';
import styles from './profileSettings.module.css';
import { updatePassword } from 'Duck/user';
import { toast } from 'react-toastify';
const ERROR_DOESNT_MATCH = "Passwords doesn't match";
const MIN_LENGTH = 8;
@ -48,12 +49,15 @@ export default class ChangePassword extends React.PureComponent {
oldPassword,
newPassword,
})
.then(() => {
if (this.props.passwordErrors.size === 0) {
this.setState({
...defaultState,
success: true,
});
.then((e) => {
const success = !e || !e.errors || e.errors.length === 0;
this.setState({
...defaultState,
success: success,
show: !success
});
if (success) {
toast.success(`Successfully changed password`);
}
});
};
@ -98,7 +102,7 @@ export default class ChangePassword extends React.PureComponent {
<Message error hidden={!doesntMatch}>
{ERROR_DOESNT_MATCH}
</Message>
<div className="flex items-center">
<div className="flex items-center pt-3">
<Button type="submit" variant="outline" disabled={this.isSubmitDisabled()} loading={loading}>
Change Password
</Button>
@ -107,9 +111,6 @@ export default class ChangePassword extends React.PureComponent {
Cancel
</Button>
</div>
<Message success hidden={!success}>
{'Successfully changed the password!'}
</Message>
</Form>
) : (
<div onClick={() => this.setState({ show: true })}>

View file

@ -88,7 +88,6 @@ class EventGroupWrapper extends React.Component {
)}
{isNote ? (
<NoteEvent
userEmail={this.props.members.find((m) => m.id === event.userId)?.email || event.userId}
note={event}
filterOutNote={filterOutNote}
onEdit={this.props.setEditNoteTooltip}

View file

@ -14,7 +14,6 @@ import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
interface Props {
note: Note;
noEdit: boolean;
userEmail: string;
filterOutNote: (id: number) => void;
onEdit: (noteTooltipObj: Record<string, any>) => void;
}
@ -86,7 +85,7 @@ function NoteEvent(props: Props) {
whiteSpace: 'nowrap',
}}
>
{props.userEmail}, {props.userEmail}
{props.note.userName}
</div>
<div className="text-disabled-text text-sm">
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}

View file

@ -9,7 +9,6 @@ import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore';
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import { fetchList as fetchMembers } from 'Duck/member';
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
import { observer } from 'mobx-react-lite';
@ -47,8 +46,6 @@ function WebPlayer(props: any) {
);
setContextValue({ player: WebPlayerInst, store: PlayerStore });
props.fetchMembers();
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
const note = props.query.get('note');
if (note) {
@ -58,7 +55,6 @@ function WebPlayer(props: any) {
}
})
const jumpToTime = props.query.get('jumpto');
const freeze = props.query.get('freeze')
if (jumpToTime) {
@ -112,10 +108,6 @@ function WebPlayer(props: any) {
<Modal open={showNoteModal} onClose={onNoteClose}>
{showNoteModal ? (
<ReadNote
userEmail={
props.members.find((m: Record<string, any>) => m.id === noteItem?.userId)?.email
|| ''
}
note={noteItem}
onClose={onNoteClose}
notFound={!noteItem}
@ -140,6 +132,5 @@ export default connect(
toggleFullscreen,
closeBottomBlock,
fetchList,
fetchMembers,
}
)(withLocationHandlers()(observer(WebPlayer)));

View file

@ -46,6 +46,12 @@ function Steps({ xrayProps, notes, members }: Props) {
bugReportStore.resetSteps();
};
React.useEffect(() => {
if (bugReportStore.sessionEventSteps.length < RADIUS && bugReportStore.sessionEventSteps.length > 0) {
setRadius(bugReportStore.sessionEventSteps.length);
}
}, [bugReportStore.sessionEventSteps])
return (
<div>
<SectionTitle>Steps to reproduce</SectionTitle>
@ -63,7 +69,7 @@ function Steps({ xrayProps, notes, members }: Props) {
STEPS
<div id="pdf-ignore">
{timePointer > 0 ? (
<StepRadius pickRadius={stepPickRadius} setRadius={setRadius} />
<StepRadius pickRadius={stepPickRadius} setRadius={setRadius} stepsNum={bugReportStore.sessionEventSteps.length}/>
) : null}
</div>
</div>

View file

@ -4,9 +4,10 @@ import { Tooltip } from 'UI'
interface Props {
pickRadius: number;
setRadius: (v: number) => void;
stepsNum: number;
}
function StepRadius({ pickRadius, setRadius }: Props) {
function StepRadius({ pickRadius, setRadius, stepsNum }: Props) {
return (
<div className="w-full flex items-center gap-4">
<div className="border-b border-dotted border-gray-medium cursor-help">
@ -18,7 +19,7 @@ function StepRadius({ pickRadius, setRadius }: Props) {
<div className="flex items-center gap-1">
<div
className="rounded px-2 bg-light-blue-bg cursor-pointer hover:bg-teal-light"
onClick={() => setRadius(pickRadius + 1)}
onClick={() => pickRadius < Math.floor(stepsNum/2) ? setRadius(pickRadius + 1) : null}
>
+1
</div>

View file

@ -62,7 +62,7 @@ export function getClosestEventStep(time: number, arr: Step[]) {
export const selectEventSteps = (steps: Step[], targetTime: number, radius: number) => {
const { targetStep, index } = getClosestEventStep(targetTime, steps)
const stepsBeforeEvent = steps.slice(index - radius, index)
const stepsBeforeEvent = steps.slice(Math.max(index - radius, 0), index)
const stepsAfterEvent = steps.slice(index + 1, index + 1 + radius)
return [...stepsBeforeEvent, targetStep, ...stepsAfterEvent]

View file

@ -88,7 +88,6 @@ class EventGroupWrapper extends React.Component {
)}
{isNote ? (
<NoteEvent
userEmail={this.props.members.find((m) => m.id === event.userId)?.email || event.userId}
note={event}
filterOutNote={filterOutNote}
onEdit={this.props.setEditNoteTooltip}

View file

@ -14,7 +14,6 @@ import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
interface Props {
note: Note;
noEdit: boolean;
userEmail: string;
filterOutNote: (id: number) => void;
onEdit: (noteTooltipObj: Record<string, any>) => void;
}
@ -86,7 +85,7 @@ function NoteEvent(props: Props) {
whiteSpace: 'nowrap',
}}
>
{props.userEmail}, {props.userEmail}
{props.note.userName}
</div>
<div className="text-disabled-text text-sm">
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}

View file

@ -58,7 +58,7 @@ function ReadNote(props: Props) {
<Icon name="quotes" color="main" size={16} />
</div>
<div className="ml-2">
<div className="text-base">{props.userEmail}</div>
<div className="text-base">{props.note.userName}</div>
<div className="text-disabled-text text-sm">
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
</div>

View file

@ -15,9 +15,6 @@ function SessionListContainer({
fetchMembers: () => void;
members: object[];
}) {
React.useEffect(() => {
fetchMembers();
}, []);
return (
<div className="widget-wrapper">
<SessionHeader />

View file

@ -13,7 +13,6 @@ import TeamBadge from './TeamBadge';
interface Props {
note: Note;
userEmail: string;
}
function NoteItem(props: Props) {
@ -69,7 +68,7 @@ function NoteItem(props: Props) {
) : null}
<div className="text-disabled-text flex items-center text-sm">
<span className="color-gray-darkest mr-1">By </span>
{props.userEmail},{' '}
{props.note.userName},{' '}
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
<div className="mx-2" />
{!props.note.isPublic ? null : <TeamBadge />}

View file

@ -35,10 +35,7 @@ function NotesList({ members }: { members: Array<Record<string, any>> }) {
<div className="border-b rounded bg-white">
{sliceListPerPage(list, notesStore.page - 1, notesStore.pageSize).map((note) => (
<React.Fragment key={note.noteId}>
<NoteItem
note={note}
userEmail={members.find((m) => m.id === note.userId)?.email || note.userId}
/>
<NoteItem note={note} />
</React.Fragment>
))}
</div>

View file

@ -91,15 +91,13 @@ export default class SharePopup extends React.PureComponent {
const { trigger, channels, msTeamsChannels, showCopyLink = false } = this.props;
const { comment, channelId, teamsChannel, loading } = this.state;
// const slackOptions = channels
// .map(({ webhookId, name }) => ({ value: webhookId, label: name }))
// .toJS();
const slackOptions = channels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
// const msTeamsOptions = msTeamsChannels
// .map(({ webhookId, name }) => ({ value: webhookId, label: name }))
// .toJS();
const slackOptions = [], msTeamsOptions = [];
const msTeamsOptions = msTeamsChannels
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
.toJS();
return (
<Popover

View file

@ -59,7 +59,7 @@ const reducer = (state = initialState, action = {}) => {
case RESET_PASSWORD.SUCCESS:
case UPDATE_PASSWORD.SUCCESS:
case LOGIN.SUCCESS:
state.set('account', Account({...action.data.user })).set('loginRequest', { loading: false, errors: [] })
state.set('account', Account({...action.data.user })).set('loginRequest', { loading: false, errors: [] }).set('passwordErrors', List())
case SIGNUP.SUCCESS:
state.set('account', Account(action.data.user)).set('onboarding', true);
case REQUEST_RESET_PASSWORD.SUCCESS:

View file

@ -71,11 +71,16 @@ export default class SettingsStore {
return webhookService.saveWebhook(inst)
.then(data => {
this.webhookInst = new Webhook(data)
this.webhooks = [...this.webhooks, this.webhookInst]
if (inst.webhookId === undefined) this.setWebhooks([...this.webhooks, this.webhookInst])
else this.setWebhooks([...this.webhooks.filter(hook => hook.webhookId !== data.webhookId), this.webhookInst])
this.hooksLoading = false
})
}
setWebhooks = (webhooks: Webhook[]) => {
this.webhooks = webhooks
}
removeWebhook = (hookId: string) => {
this.hooksLoading = true
return webhookService.removeWebhook(hookId)

View file

@ -32,6 +32,7 @@ export interface Note {
tag: iTag
timestamp: number
userId: number
userName: string
}
export interface NotesFilter {