homeflowjs 1.0.36 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/user.actions.js +2 -1
- package/app/user-history.js +16 -19
- package/package.json +2 -2
- package/properties/back-to-search-button/back-to-search-button.component.jsx +4 -8
- package/properties/property-results-pagination/property-results-pagination.component.jsx +20 -0
- package/search/property-search/property-search.js +1 -2
- package/user/default-profile/account/sign-in-form.component.test.jsx +40 -29
- package/user/user-register-form/user-register-form.component.jsx +56 -29
package/actions/user.actions.js
CHANGED
@@ -54,7 +54,7 @@ export const setUserCredentials = (payload) => ({
|
|
54
54
|
payload,
|
55
55
|
});
|
56
56
|
|
57
|
-
export const createUser = (payload) => (dispatch, getState) => {
|
57
|
+
export const createUser = (payload, recaptchaData = {}) => (dispatch, getState) => {
|
58
58
|
const { user: { localUser }, app: { authenticityToken } } = getState();
|
59
59
|
|
60
60
|
return fetch('/user.ljson', {
|
@@ -63,6 +63,7 @@ export const createUser = (payload) => (dispatch, getState) => {
|
|
63
63
|
body: JSON.stringify({
|
64
64
|
client: payload,
|
65
65
|
authenticity_token: authenticityToken,
|
66
|
+
...recaptchaData,
|
66
67
|
}),
|
67
68
|
})
|
68
69
|
.then(async (response) => {
|
package/app/user-history.js
CHANGED
@@ -1,38 +1,35 @@
|
|
1
1
|
export const addSearchToLocalStorage = (search) => {
|
2
|
-
//
|
3
|
-
|
4
|
-
|
5
|
-
// <searchHistory> format: [{channel: 'sales'}, {channel: 'sales'}]
|
6
|
-
const searchHistory = JSON.parse(localStorage.getItem?.('searchHistory') || null) || [];
|
7
|
-
|
8
|
-
// Get current page number from Homeflow state.
|
9
|
-
const pageNumber = Homeflow.getState?.().properties?.pagination?.current_page;
|
2
|
+
// Get search history from local storage
|
3
|
+
let searchHistory = localStorage.getItem('searchHistory');
|
4
|
+
searchHistory = searchHistory ? JSON.parse(searchHistory) : [];
|
10
5
|
|
11
6
|
// Make a new instance of the search object without the page number.
|
12
|
-
const
|
7
|
+
const searchWithoutPageNumber = { ...search, page: null };
|
13
8
|
|
14
9
|
// Make a new instance of the last search object without the page number.
|
15
|
-
const
|
10
|
+
const lastSearchWithoutPageNumber = { ...searchHistory[0], page: null };
|
16
11
|
|
17
12
|
/**
|
18
|
-
* If the
|
13
|
+
* If the searchWithoutPageNumber and lastSearchWithoutPageNumber match,
|
19
14
|
* just update the the last search in local storage to have the search's page number if
|
20
15
|
* it has one.
|
21
16
|
*/
|
22
|
-
if ((
|
23
|
-
searchHistory[0].page
|
24
|
-
|
17
|
+
if ((JSON.stringify(searchWithoutPageNumber) === JSON.stringify(lastSearchWithoutPageNumber)) && search?.page) {
|
18
|
+
const lastSearchWithUpdatedPageNumber = { ...searchHistory[0], page: search.page };
|
19
|
+
searchHistory.shift();
|
20
|
+
searchHistory.unshift(lastSearchWithUpdatedPageNumber);
|
21
|
+
localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
|
25
22
|
}
|
26
23
|
|
27
24
|
/**
|
28
25
|
* If the search and last search without page numbers don't match add
|
29
26
|
* the search to local storage.
|
30
27
|
*/
|
31
|
-
if (
|
32
|
-
// Add
|
33
|
-
|
34
|
-
|
35
|
-
));
|
28
|
+
if ((JSON.stringify(searchWithoutPageNumber) !== JSON.stringify(lastSearchWithoutPageNumber))) {
|
29
|
+
// Add search to the front of the array ...
|
30
|
+
searchHistory.unshift(search);
|
31
|
+
// ... and ensure only 10 most recent searches in history.
|
32
|
+
localStorage.setItem('searchHistory', JSON.stringify(searchHistory.slice(0, 10)));
|
36
33
|
}
|
37
34
|
};
|
38
35
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "homeflowjs",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.38",
|
4
4
|
"sideEffects": [
|
5
5
|
"modal/**/*",
|
6
6
|
"user/default-profile/**/*",
|
@@ -73,6 +73,6 @@
|
|
73
73
|
"jest": "^26.6.0",
|
74
74
|
"msw": "^0.28.1",
|
75
75
|
"react-autosuggest": "^10.0.2",
|
76
|
-
"redux-mock-store": "^1.5.
|
76
|
+
"redux-mock-store": "^1.5.5"
|
77
77
|
}
|
78
78
|
}
|
@@ -15,16 +15,12 @@ const BackToSearchButton = ({ children, ...otherProps }) => {
|
|
15
15
|
const property = Homeflow.get('property');
|
16
16
|
const currentPropertyID = property?.propertyId || property?.property_id;
|
17
17
|
|
18
|
-
if (currentPropertyID === secondToLastSearch?.clickedProperty)
|
19
|
-
if ('expandedPolygon' in secondToLastSearch) {
|
20
|
-
const thirdToLastSearch = JSON.parse(searchHistory)?.[2] || null;
|
21
|
-
validSearch = thirdToLastSearch;
|
22
|
-
} else {
|
23
|
-
validSearch = secondToLastSearch;
|
24
|
-
}
|
25
|
-
}
|
18
|
+
if (currentPropertyID === secondToLastSearch?.clickedProperty) validSearch = secondToLastSearch;
|
26
19
|
}
|
27
20
|
|
21
|
+
// delete the 'place' so we don't get [object Object] in URL if place is found in object.
|
22
|
+
if (validSearch?.place) delete validSearch.place;
|
23
|
+
|
28
24
|
const href = `/search?${buildQueryString(validSearch)}`;
|
29
25
|
|
30
26
|
return (
|
@@ -47,6 +47,18 @@ export default function PropertyResultsPagination({
|
|
47
47
|
* currentPage = 3;
|
48
48
|
* pageCount = 100;
|
49
49
|
*'Should output: [ 3, 4, 5, 6]'
|
50
|
+
*
|
51
|
+
* currentPage = 100;
|
52
|
+
* pageCount = 100;
|
53
|
+
* 'Should output: [ 96, 97, 98, 99]'
|
54
|
+
*
|
55
|
+
* currentPage = 3;
|
56
|
+
* pageCount = 3;
|
57
|
+
* 'Should output: [ 2 ]'
|
58
|
+
*
|
59
|
+
* currentPage = 1;
|
60
|
+
* pageCount = 3;
|
61
|
+
* 'Should output: [ 2 ]'
|
50
62
|
* @returns {number[]} An array of page numbers for pagination.
|
51
63
|
*/
|
52
64
|
const getPaginationIncrements = () => {
|
@@ -59,10 +71,18 @@ export default function PropertyResultsPagination({
|
|
59
71
|
startIncrement++;
|
60
72
|
endIncrement++;
|
61
73
|
}
|
74
|
+
|
62
75
|
if (endIncrement >= pageCount) {
|
63
76
|
endIncrement = pageCount;
|
64
77
|
}
|
65
78
|
|
79
|
+
// If on the final page
|
80
|
+
if (currentPage === pageCount) {
|
81
|
+
startIncrement = pageCount >= paginationIncrements
|
82
|
+
? currentPage - paginationIncrements : currentPage - (paginationIncrements - currentPage);
|
83
|
+
endIncrement = currentPage;
|
84
|
+
}
|
85
|
+
|
66
86
|
for (let i = startIncrement; i < endIncrement; i++) {
|
67
87
|
pagination.push(i);
|
68
88
|
}
|
@@ -41,10 +41,9 @@ export const buildQueryString = (search) => {
|
|
41
41
|
// only include either q or place ID
|
42
42
|
if ((key === 'q' && !search.isQuerySearch)
|
43
43
|
|| (key === 'placeId' && search.isQuerySearch)
|
44
|
+
|| (key === 'place')
|
44
45
|
|| (key === 'poly' && search.isQuerySearch)) return;
|
45
46
|
|
46
|
-
if (key === 'place') return queryParams.push(`place_id=${value?.place_id}`);
|
47
|
-
|
48
47
|
// Don't include branch_id if a search term has been entered
|
49
48
|
if (key === 'branch_id' && search.isQuerySearch) return;
|
50
49
|
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import {
|
3
|
-
import
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { HashRouter } from 'react-router-dom';
|
4
|
+
import { fireEvent, render } from '@testing-library/react';
|
5
|
+
import userEvent from '@testing-library/user-event';
|
4
6
|
import { rest } from 'msw';
|
5
7
|
import { setupServer } from 'msw/node';
|
8
|
+
import configureStore from 'redux-mock-store';
|
6
9
|
|
7
|
-
import withHomeflowState, { resetStore } from
|
8
|
-
import store from '../../../store';
|
10
|
+
import withHomeflowState, { resetStore } from '../../../app/with-homeflow-state';
|
9
11
|
import SignInForm from './sign-in-form.component';
|
10
12
|
import UserProfileModal from '../user-profile/user-profile-modal.component';
|
11
13
|
|
@@ -57,7 +59,9 @@ describe('SignInForm Component', () => {
|
|
57
59
|
});
|
58
60
|
|
59
61
|
it('reports successful sign in', async () => {
|
60
|
-
const {
|
62
|
+
const {
|
63
|
+
findByText, findAllByText, getByText, getAllByText,
|
64
|
+
} = render(withHomeflowState(UserProfileModal));
|
61
65
|
|
62
66
|
await findByText('Register or Sign in');
|
63
67
|
userEvent.click(getByText('Register or Sign in'));
|
@@ -71,7 +75,7 @@ describe('SignInForm Component', () => {
|
|
71
75
|
const { findByText, getByText } = render(withHomeflowState(SignInForm));
|
72
76
|
|
73
77
|
server.use(
|
74
|
-
rest.post('/session', (req,
|
78
|
+
rest.post('/session', (req, res, ctx) => res(ctx.status(401))),
|
75
79
|
);
|
76
80
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
77
81
|
|
@@ -81,7 +85,9 @@ describe('SignInForm Component', () => {
|
|
81
85
|
});
|
82
86
|
|
83
87
|
it('reports succesful sign out', async () => {
|
84
|
-
const {
|
88
|
+
const {
|
89
|
+
findByText, findAllByText, getByText, getAllByText,
|
90
|
+
} = render(withHomeflowState(UserProfileModal));
|
85
91
|
|
86
92
|
await findByText('Register or Sign in');
|
87
93
|
userEvent.click(getByText('Register or Sign in'));
|
@@ -98,7 +104,7 @@ describe('SignInForm Component', () => {
|
|
98
104
|
|
99
105
|
it('clears sign-in credentials on sign-out', async () => {
|
100
106
|
const {
|
101
|
-
findByText, findAllByText, getAllByLabelText, getByText, getAllByText
|
107
|
+
findByText, findAllByText, getAllByLabelText, getByText, getAllByText,
|
102
108
|
} = render(withHomeflowState(UserProfileModal));
|
103
109
|
|
104
110
|
await findByText('Register or Sign in');
|
@@ -121,29 +127,34 @@ describe('SignInForm Component', () => {
|
|
121
127
|
});
|
122
128
|
|
123
129
|
it('populates new user data in profile edit page afte user registration', async () => {
|
124
|
-
const
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
130
|
+
const mockStore = configureStore([]);
|
131
|
+
const store = mockStore({
|
132
|
+
user: { currentUser: user, localUser: user },
|
133
|
+
app: { loading: { user: false }},
|
134
|
+
properties: { savedProperties: [] },
|
135
|
+
search: { savedSearches: [] },
|
136
|
+
});
|
137
|
+
|
138
|
+
const { findByText, getByDisplayValue } = render(
|
139
|
+
<Provider store={store}>
|
140
|
+
<HashRouter>
|
141
|
+
<UserProfileModal />
|
142
|
+
</HashRouter>
|
143
|
+
</Provider>,
|
144
|
+
);
|
138
145
|
|
139
|
-
|
140
|
-
|
146
|
+
expect(findByText(`Welcome, ${user.first_name}`)).toBeTruthy();
|
147
|
+
expect(findByText(`${user.first_name} ${user.last_name}`)).toBeTruthy();
|
148
|
+
expect(findByText(user.email)).toBeTruthy();
|
149
|
+
expect(findByText(user.tel_home)).toBeTruthy();
|
150
|
+
expect(findByText('Edit your profile')).toBeTruthy();
|
141
151
|
|
142
|
-
|
152
|
+
const button = await findByText('Edit your profile');
|
153
|
+
fireEvent.click(button);
|
143
154
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
155
|
+
getByDisplayValue(user.first_name);
|
156
|
+
getByDisplayValue(user.last_name);
|
157
|
+
getByDisplayValue(user.tel_home);
|
158
|
+
getByDisplayValue(user.email);
|
148
159
|
});
|
149
160
|
});
|
@@ -1,6 +1,7 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
2
2
|
import { connect } from 'react-redux';
|
3
3
|
import PropTypes from 'prop-types';
|
4
|
+
import { useRecaptcha } from '../../hooks';
|
4
5
|
|
5
6
|
import { setLocalUser, createUser } from '../../actions/user.actions';
|
6
7
|
import { setLoading } from '../../actions/app.actions';
|
@@ -19,6 +20,9 @@ const UserRegisterForm = (props) => {
|
|
19
20
|
...otherProps
|
20
21
|
} = props;
|
21
22
|
|
23
|
+
const formRef = useRef(null);
|
24
|
+
const [isRequestCompleted, setIsRequestCompleted] = useState(null);
|
25
|
+
|
22
26
|
const validateForm = () => {
|
23
27
|
if (localUser.password === localUser.password_confirmation) return true;
|
24
28
|
|
@@ -26,9 +30,8 @@ const UserRegisterForm = (props) => {
|
|
26
30
|
return false;
|
27
31
|
};
|
28
32
|
|
29
|
-
const
|
30
|
-
|
31
|
-
const { body } = Object.fromEntries(new FormData(e.target));
|
33
|
+
const onRecaptchaSubmit = (recaptchaData) => {
|
34
|
+
const { body } = Object.fromEntries(new FormData(formRef.current));
|
32
35
|
const formData = {
|
33
36
|
...localUser,
|
34
37
|
body,
|
@@ -39,37 +42,61 @@ const UserRegisterForm = (props) => {
|
|
39
42
|
...localUser.marketing_preferences, opt_in_marketing_statement: marketingStatement,
|
40
43
|
};
|
41
44
|
|
45
|
+
createUser(userParams)
|
46
|
+
.then(() => {
|
47
|
+
setLoading({ userRegister: false });
|
48
|
+
notify('You have been successfully registered!', 'success');
|
49
|
+
|
50
|
+
if (redirectHash) {
|
51
|
+
window.location.hash = redirectHash;
|
52
|
+
}
|
53
|
+
})
|
54
|
+
.catch(() => {
|
55
|
+
if (userParams?.body) {
|
56
|
+
window.location.href = '/';
|
57
|
+
} else if(errors?.password){
|
58
|
+
// this could be used for any kind of errors but we only need it for password errors right now
|
59
|
+
errors.password.forEach((error) => notify(error, 'error', { duration: 8000 }))
|
60
|
+
} else {
|
61
|
+
notify(
|
62
|
+
'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.',
|
63
|
+
'error',
|
64
|
+
{ duration: 8000 },
|
65
|
+
);
|
66
|
+
}
|
67
|
+
})
|
68
|
+
.finally(setIsRequestCompleted((prevState) => !prevState))
|
69
|
+
}
|
70
|
+
|
71
|
+
const { onRecaptchaRender, onResetRecaptcha } = useRecaptcha({
|
72
|
+
formID: 'registration-form',
|
73
|
+
formRef,
|
74
|
+
onRecaptchaSubmit,
|
75
|
+
});
|
76
|
+
|
77
|
+
const handleSubmit = (e) => {
|
78
|
+
e.preventDefault();
|
79
|
+
|
42
80
|
if (validateForm()) {
|
43
81
|
setLoading({ userRegister: true });
|
44
|
-
|
45
|
-
|
46
|
-
.then(() => {
|
47
|
-
setLoading({ userRegister: false });
|
48
|
-
notify('You have been successfully registered!', 'success');
|
49
|
-
|
50
|
-
if (redirectHash) {
|
51
|
-
window.location.hash = redirectHash;
|
52
|
-
}
|
53
|
-
})
|
54
|
-
.catch((errors) => {
|
55
|
-
if (userParams?.body) {
|
56
|
-
window.location.href = '/';
|
57
|
-
} else if(errors?.password){
|
58
|
-
// this could be used for any kind of errors but we only need it for password errors right now
|
59
|
-
errors.password.forEach((error) => notify(error, 'error', { duration: 8000 }))
|
60
|
-
} else {
|
61
|
-
notify(
|
62
|
-
'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.',
|
63
|
-
'error',
|
64
|
-
{ duration: 8000 },
|
65
|
-
);
|
66
|
-
}
|
67
|
-
});
|
82
|
+
setIsRequestCompleted(false);
|
83
|
+
onRecaptchaRender();
|
68
84
|
}
|
69
85
|
};
|
70
86
|
|
87
|
+
useEffect(() => {
|
88
|
+
if(isRequestCompleted) {
|
89
|
+
onResetRecaptcha();
|
90
|
+
}
|
91
|
+
}, [isRequestCompleted])
|
92
|
+
|
93
|
+
|
71
94
|
return (
|
72
|
-
<form
|
95
|
+
<form
|
96
|
+
onSubmit={handleSubmit}
|
97
|
+
ref={formRef}
|
98
|
+
{...otherProps}
|
99
|
+
>
|
73
100
|
<input
|
74
101
|
type="text"
|
75
102
|
name="body"
|