diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index e8bda9623f..577c1b478b 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -2,9 +2,7 @@ import browserHistory from '../../../browserHistory'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; import { startLoader, stopLoader } from './loader'; -import { setToastText, showToast } from './toast'; - -const TOAST_DISPLAY_TIME_MS = 1500; +import { showToast } from './toast'; export function getCollections(username) { return (dispatch) => { @@ -47,8 +45,7 @@ export function createCollection(collection) { dispatch(stopLoader()); const newCollection = response.data; - dispatch(setToastText(`Created "${newCollection.name}"`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Created "${newCollection.name}"`)); const pathname = `/${newCollection.owner.username}/collections/${newCollection.id}`; const location = { pathname, state: { skipSavingPath: true } }; @@ -80,8 +77,7 @@ export function addToCollection(collectionId, projectId) { const collectionName = response.data.name; - dispatch(setToastText(`Added to "${collectionName}`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Added to "${collectionName}`)); return response.data; }) @@ -110,8 +106,7 @@ export function removeFromCollection(collectionId, projectId) { const collectionName = response.data.name; - dispatch(setToastText(`Removed from "${collectionName}`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Removed from "${collectionName}`)); return response.data; }) diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 1d8943336b..3675dec4b2 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -5,7 +5,7 @@ import browserHistory from '../../../browserHistory'; import apiClient from '../../../utils/apiClient'; import getConfig from '../../../utils/getConfig'; import * as ActionTypes from '../../../constants'; -import { showToast, setToastText } from './toast'; +import { showToast } from './toast'; import { setUnsavedChanges, justOpenedProject, @@ -174,24 +174,21 @@ export function saveProject( dispatch(projectSaveSuccess()); if (!autosave) { if (state.ide.justOpenedProject && state.preferences.autosave) { - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved', 5500)); setTimeout( - () => dispatch(setToastText('Toast.AutosaveEnabled')), + () => dispatch(showToast('Toast.AutosaveEnabled', 5500)), 1500 ); dispatch(resetJustOpenedProject()); } else { - dispatch(showToast(1500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved')); } } }) .catch((error) => { const { response } = error; dispatch(endSavingProject()); - dispatch(setToastText('Toast.SketchFailedSave')); - dispatch(showToast(1500)); + dispatch(showToast('Toast.SketchFailedSave')); if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else if (response.status === 409) { @@ -224,24 +221,21 @@ export function saveProject( dispatch(projectSaveSuccess()); if (!autosave) { if (state.preferences.autosave) { - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved', 5500)); setTimeout( - () => dispatch(setToastText('Toast.AutosaveEnabled')), + () => dispatch(showToast('Toast.AutosaveEnabled')), 1500 ); dispatch(resetJustOpenedProject()); } else { - dispatch(showToast(1500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved')); } } }) .catch((error) => { const { response } = error; dispatch(endSavingProject()); - dispatch(setToastText('Toast.SketchFailedSave')); - dispatch(showToast(1500)); + dispatch(showToast('Toast.SketchFailedSave')); if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else { diff --git a/client/modules/IDE/actions/toast.js b/client/modules/IDE/actions/toast.js index b1e43d9e59..d436213cef 100644 --- a/client/modules/IDE/actions/toast.js +++ b/client/modules/IDE/actions/toast.js @@ -1,39 +1,12 @@ -import * as ActionTypes from '../../../constants'; +import { setToast, hideToast } from '../reducers/toast'; -export function hideToast() { - return { - type: ActionTypes.HIDE_TOAST - }; -} +export { hideToast } from '../reducers/toast'; -/** - * Temporary fix until #2206 is merged. - * Supports legacy two-action syntax: - * dispatch(setToastText('Toast.SketchFailedSave')); - * dispatch(showToast(1500)); - * And also supports proposed single-action syntax with message and optional timeout. - * dispatch(showToast('Toast.SketchFailedSave')); - * dispatch(showToast('Toast.SketchSaved', 5500)); - */ -export function showToast(textOrTime, timeout = 1500) { - return (dispatch) => { - let time = timeout; - if (typeof textOrTime === 'string') { - // eslint-disable-next-line no-use-before-define - dispatch(setToastText(textOrTime)); - } else { - time = textOrTime; - } - dispatch({ - type: ActionTypes.SHOW_TOAST - }); - setTimeout(() => dispatch(hideToast()), time); - }; -} +export const TOAST_DISPLAY_TIME_MS = 1500; -export function setToastText(text) { - return { - type: ActionTypes.SET_TOAST_TEXT, - text - }; -} +export const showToast = (text, timeout = TOAST_DISPLAY_TIME_MS) => ( + dispatch +) => { + dispatch(setToast(text)); + setTimeout(() => dispatch(hideToast()), timeout); +}; diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index cc5b17379c..88d8f50aad 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -88,4 +88,5 @@ AddToCollectionSketchList.propTypes = { }).isRequired }; + export default AddToCollectionSketchList; diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 9d641f5e66..4edb0e9280 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -9,7 +9,6 @@ import { find } from 'lodash'; import * as ProjectActions from '../../actions/project'; import * as ProjectsActions from '../../actions/projects'; import * as CollectionsActions from '../../actions/collections'; -import * as ToastActions from '../../actions/toast'; import * as SortingActions from '../../actions/sorting'; import getSortedCollections from '../../selectors/collections'; import Loader from '../../../App/components/loader'; @@ -301,7 +300,6 @@ function mapDispatchToProps(dispatch) { CollectionsActions, ProjectsActions, ProjectActions, - ToastActions, SortingActions ), dispatch diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index dafbe21517..4e7da10cff 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -1,192 +1,185 @@ -import PropTypes from 'prop-types'; -import React, { useState, useRef } from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { withTranslation } from 'react-i18next'; -import MenuItem from '../../../../components/Dropdown/MenuItem'; -import TableDropdown from '../../../../components/Dropdown/TableDropdown'; -import * as ProjectActions from '../../actions/project'; -import * as CollectionsActions from '../../actions/collections'; -import * as IdeActions from '../../actions/ide'; -import * as ToastActions from '../../actions/toast'; -import dates from '../../../../utils/formatDate'; - -const formatDateCell = (date, mobile = false) => - dates.format(date, { showTime: !mobile }); - -const CollectionListRowBase = (props) => { - const [renameOpen, setRenameOpen] = useState(false); - const [renameValue, setRenameValue] = useState(''); - const renameInput = useRef(null); - - const closeAll = () => { - setRenameOpen(false); - }; - - const updateName = () => { - const isValid = renameValue.trim().length !== 0; - if (isValid) { - props.editCollection(props.collection.id, { - name: renameValue.trim() - }); - } - }; - - const handleAddSketches = () => { - closeAll(); - props.onAddSketches(); - }; - - const handleCollectionDelete = () => { - closeAll(); - if ( - window.confirm( - props.t('Common.DeleteConfirmation', { - name: props.collection.name - }) - ) - ) { - props.deleteCollection(props.collection.id); - } - }; - - const handleRenameOpen = () => { - closeAll(); - setRenameOpen(true); - setRenameValue(props.collection.name); - if (renameInput.current) { - renameInput.current.focus(); - } - }; - - const handleRenameChange = (e) => { - setRenameValue(e.target.value); - }; - - const handleRenameEnter = (e) => { - if (e.key === 'Enter') { - updateName(); - closeAll(); - } - }; - - const handleRenameBlur = () => { - updateName(); - closeAll(); - }; - - const renderActions = () => { - const userIsOwner = props.user.username === props.username; - - return ( - - - {props.t('CollectionListRow.AddSketch')} - - - {props.t('CollectionListRow.Delete')} - - - {props.t('CollectionListRow.Rename')} - - - ); - }; - - const renderCollectionName = () => { - const { collection, username } = props; - - return ( - <> - - {renameOpen ? '' : collection.name} - - {renameOpen && ( - e.stopPropagation()} - ref={renameInput} - /> - )} - - ); - }; - - const { collection, mobile } = props; - - return ( - - - {renderCollectionName()} - - {formatDateCell(collection.createdAt, mobile)} - {formatDateCell(collection.updatedAt, mobile)} - - {mobile && 'sketches: '} - {(collection.items || []).length} - - {renderActions()} - - ); -}; - -CollectionListRowBase.propTypes = { - collection: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - owner: PropTypes.shape({ - username: PropTypes.string.isRequired - }).isRequired, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired, - items: PropTypes.arrayOf( - PropTypes.shape({ - project: PropTypes.shape({ - id: PropTypes.string.isRequired - }) - }) - ) - }).isRequired, - username: PropTypes.string.isRequired, - user: PropTypes.shape({ - username: PropTypes.string, - authenticated: PropTypes.bool.isRequired - }).isRequired, - deleteCollection: PropTypes.func.isRequired, - editCollection: PropTypes.func.isRequired, - onAddSketches: PropTypes.func.isRequired, - mobile: PropTypes.bool, - t: PropTypes.func.isRequired -}; - -CollectionListRowBase.defaultProps = { - mobile: false -}; - -function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators( - Object.assign( - {}, - CollectionsActions, - ProjectActions, - IdeActions, - ToastActions - ), - dispatch - ); -} - -export default withTranslation()( - connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) -); +import PropTypes from 'prop-types'; +import React, { useState, useRef } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; +import MenuItem from '../../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../../components/Dropdown/TableDropdown'; +import * as ProjectActions from '../../actions/project'; +import * as CollectionsActions from '../../actions/collections'; +import * as IdeActions from '../../actions/ide'; +import dates from '../../../../utils/formatDate'; + +const formatDateCell = (date, mobile = false) => + dates.format(date, { showTime: !mobile }); + +const CollectionListRowBase = (props) => { + const [renameOpen, setRenameOpen] = useState(false); + const [renameValue, setRenameValue] = useState(''); + const renameInput = useRef(null); + + const closeAll = () => { + setRenameOpen(false); + }; + + const updateName = () => { + const isValid = renameValue.trim().length !== 0; + if (isValid) { + props.editCollection(props.collection.id, { + name: renameValue.trim() + }); + } + }; + + const handleAddSketches = () => { + closeAll(); + props.onAddSketches(); + }; + + const handleCollectionDelete = () => { + closeAll(); + if ( + window.confirm( + props.t('Common.DeleteConfirmation', { + name: props.collection.name + }) + ) + ) { + props.deleteCollection(props.collection.id); + } + }; + + const handleRenameOpen = () => { + closeAll(); + setRenameOpen(true); + setRenameValue(props.collection.name); + if (renameInput.current) { + renameInput.current.focus(); + } + }; + + const handleRenameChange = (e) => { + setRenameValue(e.target.value); + }; + + const handleRenameEnter = (e) => { + if (e.key === 'Enter') { + updateName(); + closeAll(); + } + }; + + const handleRenameBlur = () => { + updateName(); + closeAll(); + }; + + const renderActions = () => { + const userIsOwner = props.user.username === props.username; + + return ( + + + {props.t('CollectionListRow.AddSketch')} + + + {props.t('CollectionListRow.Delete')} + + + {props.t('CollectionListRow.Rename')} + + + ); + }; + + const renderCollectionName = () => { + const { collection, username } = props; + + return ( + <> + + {renameOpen ? '' : collection.name} + + {renameOpen && ( + e.stopPropagation()} + ref={renameInput} + /> + )} + + ); + }; + + const { collection, mobile } = props; + + return ( + + + {renderCollectionName()} + + {formatDateCell(collection.createdAt, mobile)} + {formatDateCell(collection.updatedAt, mobile)} + + {mobile && 'sketches: '} + {(collection.items || []).length} + + {renderActions()} + + ); +}; + +CollectionListRowBase.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + owner: PropTypes.shape({ + username: PropTypes.string.isRequired + }).isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + items: PropTypes.arrayOf( + PropTypes.shape({ + project: PropTypes.shape({ + id: PropTypes.string.isRequired + }) + }) + ) + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteCollection: PropTypes.func.isRequired, + editCollection: PropTypes.func.isRequired, + onAddSketches: PropTypes.func.isRequired, + mobile: PropTypes.bool, + t: PropTypes.func.isRequired +}; + +CollectionListRowBase.defaultProps = { + mobile: false +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators( + Object.assign({}, CollectionsActions, ProjectActions, IdeActions), + dispatch + ); +} + +export default withTranslation()( + connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) +); diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 2237227766..0337502a73 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -13,7 +13,6 @@ import dates from '../../../utils/formatDate'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; -import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; import * as IdeActions from '../actions/ide'; import getSortedSketches from '../selectors/projects'; @@ -470,13 +469,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign( - {}, - ProjectsActions, - CollectionsActions, - ToastActions, - SortingActions - ), + Object.assign({}, ProjectsActions, CollectionsActions, SortingActions), dispatch ); } diff --git a/client/modules/IDE/reducers/toast.js b/client/modules/IDE/reducers/toast.js index b680c7cfef..9c092862a6 100644 --- a/client/modules/IDE/reducers/toast.js +++ b/client/modules/IDE/reducers/toast.js @@ -1,21 +1,24 @@ -import * as ActionTypes from '../../../constants'; +import { createSlice } from '@reduxjs/toolkit'; const initialState = { isVisible: false, text: '' }; -const toast = (state = initialState, action) => { - switch (action.type) { - case ActionTypes.SHOW_TOAST: - return Object.assign({}, state, { isVisible: true }); - case ActionTypes.HIDE_TOAST: - return Object.assign({}, state, { isVisible: false }); - case ActionTypes.SET_TOAST_TEXT: - return Object.assign({}, state, { text: action.text }); - default: - return state; +const toastSlice = createSlice({ + name: 'toast', + initialState, + reducers: { + setToast: (state, action) => { + state.isVisible = true; + state.text = action.payload; + }, + hideToast: (state) => { + state.isVisible = false; + } } -}; +}); + +export const { setToast, hideToast } = toastSlice.actions; -export default toast; +export default toastSlice.reducer; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 8e05a681f3..6293fb7dbb 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -4,7 +4,7 @@ import browserHistory from '../../browserHistory'; import apiClient from '../../utils/apiClient'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; -import { showToast, setToastText } from '../IDE/actions/toast'; +import { showToast } from '../IDE/actions/toast'; export function authError(error) { return { @@ -279,8 +279,7 @@ export function updateSettings(formValues) { submitSettings(formValues) .then((response) => { dispatch(updateSettingsSuccess(response.data)); - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SettingsSaved')); + dispatch(showToast('Toast.SettingsSaved', 5500)); resolve(); }) .catch((error) => resolve({ error })) diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 939b2b851f..28bdb5305f 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -10,7 +10,6 @@ import classNames from 'classnames'; import * as ProjectActions from '../../IDE/actions/project'; import * as ProjectsActions from '../../IDE/actions/projects'; import * as CollectionsActions from '../../IDE/actions/collections'; -import * as ToastActions from '../../IDE/actions/toast'; import * as SortingActions from '../../IDE/actions/sorting'; import * as IdeActions from '../../IDE/actions/ide'; import { getCollection } from '../../IDE/selectors/collections'; @@ -350,13 +349,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign( - {}, - CollectionsActions, - ProjectsActions, - ToastActions, - SortingActions - ), + Object.assign({}, CollectionsActions, ProjectsActions, SortingActions), dispatch ); } diff --git a/client/modules/User/components/Notification.jsx b/client/modules/User/components/Notification.jsx index c7189c3af1..9f257f68ad 100644 --- a/client/modules/User/components/Notification.jsx +++ b/client/modules/User/components/Notification.jsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import Cookies from 'js-cookie'; import { useDispatch } from 'react-redux'; -import { showToast, setToastText } from '../../IDE/actions/toast'; +import { showToast } from '../../IDE/actions/toast'; function Notification() { const dispatch = useDispatch(); @@ -9,10 +9,9 @@ function Notification() { const notification = Cookies.get('p5-notification'); if (!notification) { // show the toast - dispatch(showToast(30000)); const text = `There is a scheduled outage on Sunday, April 9 3AM - 5AM UTC. The entire site will be down, so please plan accordingly.`; - dispatch(setToastText(text)); + dispatch(showToast(text, 30000)); Cookies.set('p5-notification', true, { expires: 365 }); } });