From 097862cb244acf436cead182fd0bcf801eba05c9 Mon Sep 17 00:00:00 2001 From: Andrei Surzhan Date: Fri, 31 Jul 2020 20:21:31 -0400 Subject: [PATCH 1/2] Added support for languages retrived from API --- public/index.html | 7 -- src/actions/actionTypes.js | 4 + src/actions/languageActions.js | 61 +++++++++++++ src/api/languageApi.js | 9 ++ src/containers/ChecklistItemBlock.js | 1 - src/containers/LanguageDialog.js | 127 ++++++++++++++------------- src/pages/ChecklistPage.js | 11 ++- src/reducers/index.js | 4 +- src/reducers/initialState.js | 4 + src/reducers/languageReducer.js | 27 ++++++ 10 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 src/actions/languageActions.js create mode 100644 src/api/languageApi.js create mode 100644 src/reducers/languageReducer.js diff --git a/public/index.html b/public/index.html index 5b61727..17dce82 100644 --- a/public/index.html +++ b/public/index.html @@ -18,13 +18,6 @@ - - - - diff --git a/src/actions/actionTypes.js b/src/actions/actionTypes.js index ef50a86..db743a3 100644 --- a/src/actions/actionTypes.js +++ b/src/actions/actionTypes.js @@ -41,5 +41,9 @@ export const REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS'; export const REQUEST_REGISTRATION = 'REQUEST_REGISTRATION'; export const REGISTRATION_FAILURE = 'REGISTRATION_FAILURE'; +export const REQUEST_LANGUAGES = 'REQUEST_LANGUAGES'; +export const GET_LANGUAGES_SUCCESS = 'GET_LANGUAGES_SUCCESS'; +export const GET_LANGUAGES_FAILURE = 'GET_LANGUAGES_FAILURE'; + export const CHECK_AUTH = 'CHECK_AUTH'; export const SET_AUTH_TO_FALSE = 'SET_AUTH_TO_FALSE'; diff --git a/src/actions/languageActions.js b/src/actions/languageActions.js new file mode 100644 index 0000000..24687bc --- /dev/null +++ b/src/actions/languageActions.js @@ -0,0 +1,61 @@ +import * as types from './actionTypes'; +import LanguageApi from '../api/languageApi'; + +const languageApi = new LanguageApi(); + +export function getLanguagesError(message) { + return { + type: types.GET_LANGUAGES_FAILURE, + isFetching: false, + isAuthenticated: false, + message + }; +} + +export function requestLanguages() { + return { + type: types.REQUEST_LANGUAGES, + isFetching: true, + languages: [] + }; +} + +export function getLanguagesSuccess(languages) { + return { + type: types.GET_LANGUAGES_SUCCESS, + isFetching: false, + languages + }; +} + +export function getLanguages() { + return async (dispatch, getState) => { + try { + dispatch(requestLanguages()); + + const languages = await languageApi.getAllLanguages(); + + dispatch(getLanguagesSuccess(languages)); + } catch (e) { + dispatch(getLanguagesError(e.message)); + } + }; +} + +export function getLanguageNameByCode(code) { + return async (dispatch, getState) => { + try { + const languages = getState().languages.languages; + for (let i = 0; i < languages.languages.length; i++) { + const lang = languages.languages[i]; + + if (lang.code === code) { + return lang.name; + } + } + + return null; + } catch (e) { + } + }; +} diff --git a/src/api/languageApi.js b/src/api/languageApi.js new file mode 100644 index 0000000..6e661bf --- /dev/null +++ b/src/api/languageApi.js @@ -0,0 +1,9 @@ +import Api from './api'; + +class LanguageApi extends Api { + async getAllLanguages() { + return await this.request(null, 'GET', `${this.basicUrl}/language`, true); + } +} + +export default LanguageApi; diff --git a/src/containers/ChecklistItemBlock.js b/src/containers/ChecklistItemBlock.js index 6d8f4f5..70a76b5 100644 --- a/src/containers/ChecklistItemBlock.js +++ b/src/containers/ChecklistItemBlock.js @@ -47,7 +47,6 @@ class ChecklistItemBlock extends React.Component { handleShare(event) { event.stopPropagation(); - console.log('Share Checklist', this.props.checklist); } handleOkClick(event) { diff --git a/src/containers/LanguageDialog.js b/src/containers/LanguageDialog.js index 6a6a683..ac76a87 100644 --- a/src/containers/LanguageDialog.js +++ b/src/containers/LanguageDialog.js @@ -9,7 +9,6 @@ import FormLabel from '@material-ui/core/FormLabel'; import FormGroup from '@material-ui/core/FormGroup'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '../common/components/Checkbox'; -import { SupportedLanguages } from '../utils/enums'; import { connect } from 'react-redux'; import * as userActions from '../actions/userActions'; import { bindActionCreators } from 'redux'; @@ -26,91 +25,98 @@ class LanguageDialog extends React.Component { constructor(props) { super(); + const languageViewModels = this.getLanguageViewModels(props.languages, props.user.user.languages); + this.state = { - languages: this.filterLanguages(new SupportedLanguages().languages, props.user.user.languages), + languageViewModels, user: Object.assign({}, props.user.user) }; - this.filterLanguages = this.filterLanguages.bind(this); this.handleCancelClick = this.handleCancelClick.bind(this); this.handleCheckboxChange = this.handleCheckboxChange.bind(this); this.handleOkClick = this.handleOkClick.bind(this); this.handleClose = this.handleClose.bind(this); } - filterLanguages(languages, userLanguages) { - return ( - languages - .map(l => { - return { - name: l.name, - code: l.code, - checked: userLanguages.includes(l.code) - }; - }) - // Sort Alphabetically - .sort((a, b) => { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); - - if (nameA < nameB) { - return -1; - } - - if (nameA > nameB) { - return 1; - } - - return 0; - }) - // Checked at the top of the list - .sort((a, b) => { - if (a.checked && !b.checked) { - return -1; - } - if (!a.checked && b.checked) { - return 1; - } - - return 0; - }) - ); + getLanguageViewModels = (languages, userLanguages) => { + return languages + .map(this.createLanguageViewModel(userLanguages)) + .sort(this.sortLanguageViewModelsByName) + .sort(this.sortLanguageViewModelsByChecked); + } + + createLanguageViewModel(userLanguages) { + return language => { + return { + name: language.name, + code: language.code, + checked: userLanguages.includes(language.code) + }; + } + } + + sortLanguageViewModelsByName(a, b) { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + + if (nameA < nameB) { + return -1; + } + + if (nameA > nameB) { + return 1; + } + + return 0; + } + + sortLanguageViewModelsByChecked(a, b) { + if (a.checked && !b.checked) { + return -1; + } + if (!a.checked && b.checked) { + return 1; + } + + return 0; } handleCancelClick() { this.props.onClose(); } - handleCheckboxChange = languageName => event => { - const languages = [...this.state.languages]; + handleCheckboxChange(selectedLanguage) { + return event => { + const languageViewModels = [...this.state.languageViewModels]; - for (let i = 0; i < languages.length; i++) { - const language = languages[i]; + for (let i = 0; i < languageViewModels.length; i++) { + const language = languageViewModels[i]; - if (language.name === languageName) { - language.checked = event.target.checked; + if (language.code === selectedLanguage.code) { + language.checked = event.target.checked; - break; + break; + } } - } - this.setState({ - languages - }); - }; + this.setState({ + languageViewModels + }); + }; + } handleOkClick(event) { const user = Object.assign({}, this.state.user); - const languages = [...this.state.languages]; - const langCodes = languages.filter(l => l.checked).map(l => l.code); + const languageViewModels = [...this.state.languageViewModels]; + const langCodes = languageViewModels.filter(l => l.checked).map(l => l.code); if (langCodes.length !== user.languages.length || !langCodes.every(l => user.languages.includes(l))) { user.languages = langCodes; - this.props.actions.updateUser(user); + this.props.userActions.updateUser(user); this.setState({ - languages: this.filterLanguages(languages, user.languages), + languageViewModels, user }); @@ -139,14 +145,14 @@ class LanguageDialog extends React.Component { Select language - {this.state.languages.map(language => ( + {this.state.languageViewModels.map(language => ( } @@ -169,13 +175,14 @@ class LanguageDialog extends React.Component { function mapStateToProps(state, ownProps) { return { - user: state.user + user: state.user, + languages: state.languages.languages }; } function mapDispatchToProps(dispatch) { return { - actions: bindActionCreators(userActions, dispatch) + userActions: bindActionCreators(userActions, dispatch) }; } diff --git a/src/pages/ChecklistPage.js b/src/pages/ChecklistPage.js index 918f044..98f77e2 100644 --- a/src/pages/ChecklistPage.js +++ b/src/pages/ChecklistPage.js @@ -14,6 +14,7 @@ import ExpansionPanel from '../components/ExpansionPanel'; import ExpansionPanelSummary from '../components/ExpansionPanelSummary'; import * as checklistActions from '../actions/checklistActions'; import * as userActions from '../actions/userActions'; +import * as languageActions from '../actions/languageActions'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { PropTypes } from 'prop-types'; @@ -48,9 +49,11 @@ class ChecklistPage extends React.Component { constructor(props, context) { super(props, context); + const hasUserAnyLanguages = this.props.user.user.languages.length === 0; + this.state = { user: Object.assign({}, this.props.user.user), - openLanguageDialog: this.props.user.user.languages.length === 0, + openLanguageDialog: hasUserAnyLanguages, drawerIsOpened: false, openElementDialog: false, checklist: { @@ -75,6 +78,7 @@ class ChecklistPage extends React.Component { componentWillMount() { this.props.checklistActions.loadChecklists(); + this.props.languageActions.getLanguages(); } handleLogoutClick(event) { @@ -252,6 +256,7 @@ function mapStateToProps(state, ownProps) { isAuthenticated: state.user.isAuthenticated, user: state.user, checklists: state.checklists.checklists, + languages: state.languages.languages, isFetching: state.checklists.isFetching, isApiAddChecklist: state.checklists.isApiAddChecklist }; @@ -260,7 +265,9 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { checklistActions: bindActionCreators(checklistActions, dispatch), - userActions: bindActionCreators(userActions, dispatch) + userActions: bindActionCreators(userActions, dispatch), + languageActions: bindActionCreators(languageActions, dispatch) + }; } diff --git a/src/reducers/index.js b/src/reducers/index.js index c568f69..45068ee 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,10 +1,12 @@ import { combineReducers } from 'redux'; import checklists from './checklistReducer'; import user from './userReducer'; +import languages from './languageReducer'; const rootReducer = combineReducers({ checklists, - user + user, + languages }); export default rootReducer; diff --git a/src/reducers/initialState.js b/src/reducers/initialState.js index 63e0d04..067dbad 100644 --- a/src/reducers/initialState.js +++ b/src/reducers/initialState.js @@ -9,5 +9,9 @@ export default { isFetching: false, user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null, isAuthenticated: isTokenValid() + }, + languages: { + languages: [], + isFetching: false } }; diff --git a/src/reducers/languageReducer.js b/src/reducers/languageReducer.js new file mode 100644 index 0000000..9a731c7 --- /dev/null +++ b/src/reducers/languageReducer.js @@ -0,0 +1,27 @@ +import * as types from '../actions/actionTypes'; +import initialState from './initialState'; + +export default function languageReducer(state = initialState.languages, action) { + switch (action.type) { + case types.REQUEST_LANGUAGES: + return Object.assign({}, state, { + isFetching: action.isFetching, + languages: action.languages, + user: action.creds + }); + case types.GET_LANGUAGES_SUCCESS: + return Object.assign({}, state, { + isFetching: action.isFetching, + languages: action.languages, + user: action.user + }); + case types.GET_LANGUAGES_FAILURE: + return Object.assign({}, state, { + isFetching: action.isFetching, + languages: action.languages, + errorMessage: action.message + }); + default: + return state; + } +} From c116aa4967bd1b4eb9d1e18e443bfe4892ed27df Mon Sep 17 00:00:00 2001 From: Andrei Surzhan Date: Sat, 23 Jan 2021 14:31:31 -0500 Subject: [PATCH 2/2] possible to get languages from DB --- src/actions/userActions.js | 6 +- src/containers/LanguageDialog.js | 131 +++++++++++-------------------- src/pages/ChecklistPage.js | 18 ++--- src/reducers/userReducer.js | 14 ++++ 4 files changed, 69 insertions(+), 100 deletions(-) diff --git a/src/actions/userActions.js b/src/actions/userActions.js index 43cda3a..1e4f743 100644 --- a/src/actions/userActions.js +++ b/src/actions/userActions.js @@ -82,10 +82,9 @@ export function registeryError(message) { }; } -export function requestUpdateUser(creds) { +export function requestUpdateUser() { return { type: types.REQUEST_UPDATE_USER, - creds, isFetching: true, requiresAuth: true }; @@ -95,8 +94,7 @@ export function updateUserSuccess(user) { return { type: types.UPDATE_USER_SUCCESS, user, - isFetching: false, - requiresAuth: true + isFetching: false }; } diff --git a/src/containers/LanguageDialog.js b/src/containers/LanguageDialog.js index ac76a87..87bf302 100644 --- a/src/containers/LanguageDialog.js +++ b/src/containers/LanguageDialog.js @@ -9,6 +9,7 @@ import FormLabel from '@material-ui/core/FormLabel'; import FormGroup from '@material-ui/core/FormGroup'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '../common/components/Checkbox'; +import Spinner from '../common/components/Spinner'; import { connect } from 'react-redux'; import * as userActions from '../actions/userActions'; import { bindActionCreators } from 'redux'; @@ -17,111 +18,54 @@ import { withStyles } from '@material-ui/core/styles'; const styles = theme => ({ dialogContent: { width: '400px', - height: '400px' + maxHeight: '400px' } }); class LanguageDialog extends React.Component { - constructor(props) { - super(); - - const languageViewModels = this.getLanguageViewModels(props.languages, props.user.user.languages); + constructor(props, context) { + super(props, context); this.state = { - languageViewModels, - user: Object.assign({}, props.user.user) + user: Object.assign({}, this.props.user.user) }; + this.originalLanguages = [...this.props.user.user.languages]; + this.handleCancelClick = this.handleCancelClick.bind(this); this.handleCheckboxChange = this.handleCheckboxChange.bind(this); this.handleOkClick = this.handleOkClick.bind(this); this.handleClose = this.handleClose.bind(this); } - getLanguageViewModels = (languages, userLanguages) => { - return languages - .map(this.createLanguageViewModel(userLanguages)) - .sort(this.sortLanguageViewModelsByName) - .sort(this.sortLanguageViewModelsByChecked); - } - - createLanguageViewModel(userLanguages) { - return language => { - return { - name: language.name, - code: language.code, - checked: userLanguages.includes(language.code) - }; - } - } - - sortLanguageViewModelsByName(a, b) { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); - - if (nameA < nameB) { - return -1; - } - - if (nameA > nameB) { - return 1; - } - - return 0; - } - - sortLanguageViewModelsByChecked(a, b) { - if (a.checked && !b.checked) { - return -1; - } - if (!a.checked && b.checked) { - return 1; - } - - return 0; - } - handleCancelClick() { this.props.onClose(); } handleCheckboxChange(selectedLanguage) { return event => { - const languageViewModels = [...this.state.languageViewModels]; - - for (let i = 0; i < languageViewModels.length; i++) { - const language = languageViewModels[i]; + const user = Object.assign({}, this.props.user.user); - if (language.code === selectedLanguage.code) { - language.checked = event.target.checked; + if(user.languages.includes(selectedLanguage.code)) { + const index = user.languages.indexOf(selectedLanguage.code); - break; - } + user.languages.splice(index, 1); + } else { + user.languages.push(selectedLanguage.code); } this.setState({ - languageViewModels + user }); }; } handleOkClick(event) { - const user = Object.assign({}, this.state.user); - const languageViewModels = [...this.state.languageViewModels]; - const langCodes = languageViewModels.filter(l => l.checked).map(l => l.code); - - if (langCodes.length !== user.languages.length || !langCodes.every(l => user.languages.includes(l))) { - user.languages = langCodes; - - this.props.userActions.updateUser(user); - - this.setState({ - languageViewModels, - user - }); - + if (isArraysEqual(this.originalLanguages, this.state.user.languages)) { this.props.onClose(); } else { + this.props.userActions.updateUser(this.state.user); + this.props.onClose(); } } @@ -145,19 +89,22 @@ class LanguageDialog extends React.Component { Select language - {this.state.languageViewModels.map(language => ( - - } - /> - ))} + {this.props.languages.isFetching + ? () + : this.props.languages.languages.map(language => ( + + } + /> + )) + } @@ -176,7 +123,7 @@ class LanguageDialog extends React.Component { function mapStateToProps(state, ownProps) { return { user: state.user, - languages: state.languages.languages + languages: state.languages }; } @@ -186,6 +133,16 @@ function mapDispatchToProps(dispatch) { }; } +function isArraysEqual(a, b) { + const arrayA = [...a]; + const arrayB = [...b]; + + arrayA.sort(); + arrayB.sort(); + + return arrayA.length === arrayB.length && arrayA.every((element, index) => element === arrayB[index]); +} + LanguageDialog.propTypes = { open: PropTypes.bool.isRequired, classes: PropTypes.object.isRequired, diff --git a/src/pages/ChecklistPage.js b/src/pages/ChecklistPage.js index 98f77e2..b1f1169 100644 --- a/src/pages/ChecklistPage.js +++ b/src/pages/ChecklistPage.js @@ -138,7 +138,7 @@ class ChecklistPage extends React.Component { } }); } - + handAddNewChecklistButtonClick(event) { this.setState({ openElementDialog: true @@ -161,7 +161,7 @@ class ChecklistPage extends React.Component { render() { const { classes } = this.props; - const isFetching = this.props.isFetching; + const isChecklistsFetching = this.props.isChecklistsFetching; const isApiAddChecklist = this.props.isApiAddChecklist; const drawerOptions = [ { @@ -189,21 +189,21 @@ class ChecklistPage extends React.Component { aria-label="Add checklist" size="medium" className={classes.addChecklistButton} - onClick={isFetching ? undefined : this.handAddNewChecklistButtonClick} + onClick={isChecklistsFetching ? undefined : this.handAddNewChecklistButtonClick} > - {isFetching ? ( + {isChecklistsFetching ? ( ) : ( this.props.checklists.map(checklist => ( @@ -257,7 +257,7 @@ function mapStateToProps(state, ownProps) { user: state.user, checklists: state.checklists.checklists, languages: state.languages.languages, - isFetching: state.checklists.isFetching, + isChecklistsFetching: state.checklists.isFetching, isApiAddChecklist: state.checklists.isApiAddChecklist }; } @@ -267,16 +267,16 @@ function mapDispatchToProps(dispatch) { checklistActions: bindActionCreators(checklistActions, dispatch), userActions: bindActionCreators(userActions, dispatch), languageActions: bindActionCreators(languageActions, dispatch) - }; } ChecklistPage.propTypes = { classes: PropTypes.object.isRequired, - isFetching: PropTypes.bool.isRequired, + isChecklistsFetching: PropTypes.bool.isRequired, isAuthenticated: PropTypes.bool.isRequired, user: PropTypes.object.isRequired, - checklists: PropTypes.array.isRequired + checklists: PropTypes.array.isRequired, + languages: PropTypes.array.isRequired }; export default connect( diff --git a/src/reducers/userReducer.js b/src/reducers/userReducer.js index 06f357e..78043c6 100644 --- a/src/reducers/userReducer.js +++ b/src/reducers/userReducer.js @@ -59,6 +59,20 @@ export default function userReducer(state = initialState.auth, action) { isAuthenticated: action.isAuthenticated, user: {} }); + case types.REQUEST_UPDATE_USER: + return Object.assign({}, state, { + isFetching: action.isFetching + }); + case types.UPDATE_USER_SUCCESS: + return Object.assign({}, state, { + isFetching: action.isFetching, + user: action.user + }); + case types.UPDATE_USER_FAILURE: + return Object.assign({}, state, { + isFetching: action.isFetching, + errorMessage: action.message + }); default: return state; }