diff --git a/api/chalicelib/core/autocomplete.py b/api/chalicelib/core/autocomplete.py index 06741cbe5..00eff7d61 100644 --- a/api/chalicelib/core/autocomplete.py +++ b/api/chalicelib/core/autocomplete.py @@ -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): diff --git a/ee/api/chalicelib/core/autocomplete_exp.py b/ee/api/chalicelib/core/autocomplete_exp.py index 2e1a3f002..03ca85957 100644 --- a/ee/api/chalicelib/core/autocomplete_exp.py +++ b/ee/api/chalicelib/core/autocomplete_exp.py @@ -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): diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index a9e092486..c9a041cc9 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -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 }); } }) } diff --git a/frontend/app/api_middleware.js b/frontend/app/api_middleware.js index f98ad344b..9ef371f01 100644 --- a/frontend/app/api_middleware.js +++ b/frontend/app/api_middleware.js @@ -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) }); }); }; diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 22108deee..15d24be3d 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -39,17 +39,6 @@ class CustomFieldForm extends React.PureComponent { placeholder="Field Name" /> - - {/* TODO: errors state is not updating properly */} - {/* {errors && ( -
- {errors.map((error, i) => ( - - {error.message} - - ))} -
- )} */}
diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index 30844dec2..d5843425a 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -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) {
- +
diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index d1f53f56d..f1851f919 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -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: }, - { title: 'VueX', slug: 'vuex', icon: 'integrations/vuejs', component: }, - { title: 'Pinia', slug: 'pinia', icon: 'integrations/pinia', component: }, - { - title: 'GraphQL', - slug: 'graphql', - icon: 'integrations/graphql', - component: , - }, - { title: 'NgRx', slug: 'ngrx', icon: 'integrations/ngrx', component: }, - { title: 'MobX', slug: 'mobx', icon: 'integrations/mobx', component: }, - { - title: 'Profiler', - slug: 'profiler', - icon: 'integrations/openreplay', - component: , - }, - { - title: 'Assist', - slug: 'assist', - icon: 'integrations/openreplay', - component: , - }, - { title: 'Zustand', slug: 'zustand', icon: '', header: '🐻', component: }, + { title: 'Redux', icon: 'integrations/redux', component: }, + { title: 'VueX', icon: 'integrations/vuejs', component: }, + { title: 'Pinia', icon: 'integrations/pinia', component: }, + { title: 'GraphQL', icon: 'integrations/graphql', component: }, + { title: 'NgRx', icon: 'integrations/ngrx', component: }, + { title: 'MobX', icon: 'integrations/mobx', component: }, + { title: 'Profiler', icon: 'integrations/openreplay', component: }, + { title: 'Assist', icon: 'integrations/openreplay', component: }, + { title: 'Zustand', icon: '', header: '🐻', component: }, ], }, ]; diff --git a/frontend/app/components/Client/ProfileSettings/ChangePassword.js b/frontend/app/components/Client/ProfileSettings/ChangePassword.js index d0222abf0..2640ab612 100644 --- a/frontend/app/components/Client/ProfileSettings/ChangePassword.js +++ b/frontend/app/components/Client/ProfileSettings/ChangePassword.js @@ -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 { -
+
@@ -107,9 +111,6 @@ export default class ChangePassword extends React.PureComponent { Cancel
- ) : (
this.setState({ show: true })}> diff --git a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/EventGroupWrapper.js index 1ec053462..924be9f2c 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/EventGroupWrapper.js @@ -88,7 +88,6 @@ class EventGroupWrapper extends React.Component { )} {isNote ? ( m.id === event.userId)?.email || event.userId} note={event} filterOutNote={filterOutNote} onEdit={this.props.setEditNoteTooltip} diff --git a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/NoteEvent.tsx b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/NoteEvent.tsx index 676b1f901..a09869ba5 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/NoteEvent.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/NoteEvent.tsx @@ -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) => void; } @@ -86,7 +85,7 @@ function NoteEvent(props: Props) { whiteSpace: 'nowrap', }} > - {props.userEmail}, {props.userEmail} + {props.note.userName}
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)} diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 0c2bb89ab..eb4fbf8db 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -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) { {showNoteModal ? ( ) => 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))); diff --git a/frontend/app/components/Session_/BugReport/components/Steps.tsx b/frontend/app/components/Session_/BugReport/components/Steps.tsx index 8307eea47..2f1517d24 100644 --- a/frontend/app/components/Session_/BugReport/components/Steps.tsx +++ b/frontend/app/components/Session_/BugReport/components/Steps.tsx @@ -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 (
Steps to reproduce @@ -63,7 +69,7 @@ function Steps({ xrayProps, notes, members }: Props) { STEPS
{timePointer > 0 ? ( - + ) : null}
diff --git a/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx index 543d2c803..afa8b15c7 100644 --- a/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx +++ b/frontend/app/components/Session_/BugReport/components/StepsComponents/StepRadius.tsx @@ -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 (
@@ -18,7 +19,7 @@ function StepRadius({ pickRadius, setRadius }: Props) {
setRadius(pickRadius + 1)} + onClick={() => pickRadius < Math.floor(stepsNum/2) ? setRadius(pickRadius + 1) : null} > +1
diff --git a/frontend/app/components/Session_/BugReport/utils.ts b/frontend/app/components/Session_/BugReport/utils.ts index a97e275d7..053ece364 100644 --- a/frontend/app/components/Session_/BugReport/utils.ts +++ b/frontend/app/components/Session_/BugReport/utils.ts @@ -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] diff --git a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js index 1ec053462..924be9f2c 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js @@ -88,7 +88,6 @@ class EventGroupWrapper extends React.Component { )} {isNote ? ( m.id === event.userId)?.email || event.userId} note={event} filterOutNote={filterOutNote} onEdit={this.props.setEditNoteTooltip} diff --git a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx index 676b1f901..a09869ba5 100644 --- a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx +++ b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx @@ -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) => void; } @@ -86,7 +85,7 @@ function NoteEvent(props: Props) { whiteSpace: 'nowrap', }} > - {props.userEmail}, {props.userEmail} + {props.note.userName}
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)} diff --git a/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx b/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx index 988412b2d..550c8f99f 100644 --- a/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx @@ -58,7 +58,7 @@ function ReadNote(props: Props) {
-
{props.userEmail}
+
{props.note.userName}
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
diff --git a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx index 6a863e69b..67b1e12e7 100644 --- a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx +++ b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx @@ -15,9 +15,6 @@ function SessionListContainer({ fetchMembers: () => void; members: object[]; }) { - React.useEffect(() => { - fetchMembers(); - }, []); return (
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx index 1ece07d7f..05113f523 100644 --- a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx +++ b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx @@ -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}
By - {props.userEmail},{' '} + {props.note.userName},{' '} {formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
{!props.note.isPublic ? null : } diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx index 0a66cd0eb..3c48d2af0 100644 --- a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx +++ b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx @@ -35,10 +35,7 @@ function NotesList({ members }: { members: Array> }) {
{sliceListPerPage(list, notesStore.page - 1, notesStore.pageSize).map((note) => ( - m.id === note.userId)?.email || note.userId} - /> + ))}
diff --git a/frontend/app/components/shared/SharePopup/SharePopup.js b/frontend/app/components/shared/SharePopup/SharePopup.js index 1df16cf6d..8e09f819c 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.js +++ b/frontend/app/components/shared/SharePopup/SharePopup.js @@ -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 ( { 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: diff --git a/frontend/app/mstore/settingsStore.ts b/frontend/app/mstore/settingsStore.ts index d9dfb0dd3..7dd584fd3 100644 --- a/frontend/app/mstore/settingsStore.ts +++ b/frontend/app/mstore/settingsStore.ts @@ -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) diff --git a/frontend/app/services/NotesService.ts b/frontend/app/services/NotesService.ts index cdecb7c3f..d66316129 100644 --- a/frontend/app/services/NotesService.ts +++ b/frontend/app/services/NotesService.ts @@ -32,6 +32,7 @@ export interface Note { tag: iTag timestamp: number userId: number + userName: string } export interface NotesFilter {