diff --git a/src/components/Board/Board.container.js b/src/components/Board/Board.container.js index 62a6aad54..533e568eb 100644 --- a/src/components/Board/Board.container.js +++ b/src/components/Board/Board.container.js @@ -1541,6 +1541,17 @@ export class BoardContainer extends Component { } }; + handleAddApiBoard = async boardId => { + if (!this.props.boards.find(board => board.id === boardId)) { + try { + const board = await API.getBoard(boardId); + this.props.addBoards([board]); + } catch (err) { + console.log(err.message); + } + } + }; + render() { const { navHistory, @@ -1695,6 +1706,8 @@ export class BoardContainer extends Component { this.props.communicator.boards.includes(board.id) )} userData={this.props.userData} + folders={this.props.boards} + onAddApiBoard={this.handleAddApiBoard} /> ); diff --git a/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.js b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.js new file mode 100644 index 000000000..3b8609b2d --- /dev/null +++ b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.js @@ -0,0 +1,365 @@ +import React, { Fragment, useMemo } from 'react'; +import { alpha, makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItem from '@material-ui/core/ListItem'; +import List from '@material-ui/core/List'; +import Divider from '@material-ui/core/Divider'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import CloseIcon from '@material-ui/icons/Close'; +import Slide from '@material-ui/core/Slide'; +import { + CircularProgress, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + InputBase +} from '@material-ui/core'; +import { Edit, Search as SearchIcon, Visibility } from '@material-ui/icons'; +import useAllBoardsFetcher from './useAllBoardsFetcher'; +import styles from './LoadBoardEditor.module.css'; +import { Alert, AlertTitle, Pagination } from '@material-ui/lab'; +import PropTypes from 'prop-types'; +import { intlShape } from 'react-intl'; +import communicatorMessages from '../../../Communicator/CommunicatorDialog/CommunicatorDialog.messages'; +import messages from './LoadBoardEditor.messages'; +import moment from 'moment'; +import { isCordova } from '../../../../cordova-util'; +import { debounce } from 'lodash'; + +const useStyles = makeStyles(theme => ({ + appBar: { + position: 'sticky' + }, + title: { + marginLeft: theme.spacing(2), + flex: 1 + }, + search: { + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.25) + }, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing(1), + width: 'auto' + } + }, + searchIcon: { + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + inputRoot: { + color: 'inherit' + }, + inputInput: { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, + transition: theme.transitions.create('width'), + width: '100%', + [theme.breakpoints.up('sm')]: { + width: '12ch', + '&:focus': { + width: '20ch' + } + } + } +})); + +const Transition = React.forwardRef(function Transition(props, ref) { + return ; +}); + +const BoardPagination = ({ pagesCount, currentPage, handleChange }) => { + return ( +
+ {pagesCount >= 1 && ( + + )} +
+ ); +}; + +const BoardInfoContent = ({ intl, pageBoards, selectedBoardId }) => { + const board = pageBoards.find(({ id }) => id === selectedBoardId); + const boardUrl = + window.location.origin + + '/' + + window.location.pathname.split('/')[1] + + '/' + + board.id; + + return ( + + + + {intl.formatMessage(communicatorMessages.boardInfoName)}:{' '} + {board.name} + + + {intl.formatMessage(communicatorMessages.boardDescription)}:{' '} + {board.description} + + + {intl.formatMessage(communicatorMessages.boardInfoDate)}:{' '} + {moment(board.lastEdited).format('DD/MM/YYYY')} + + + {intl.formatMessage(communicatorMessages.boardInfoTiles)}:{' '} + {board.tiles.length} + + + {intl.formatMessage(communicatorMessages.boardInfoId)}:{' '} + {board.id} + + {!isCordova() && ( + + )} + + + ); +}; + +const LoadBoardEditor = ({ intl, onLoadBoardChange, isLostedFolder }) => { + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + const { + pageBoards, + totalPages, + loading, + error, + fetchBoards + } = useAllBoardsFetcher(); + const [currentPage, setCurrentPage] = React.useState(1); + + const BoardsList = ({ onItemClick }) => { + return ( + + {pageBoards?.map(({ id, name, lastEdited }) => ( + + onItemClick(id)}> + + + + + ))} + + ); + }; + + const handleClickOpen = () => { + fetchBoards({}); + setSearchValue(''); + setCurrentPage(1); + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleChangeOnPage = (event, page) => { + setCurrentPage(page); + fetchBoards({ page, search: searchValue ?? null }); + }; + + const [openConfirmationDialog, setOpenConfirmationDialog] = React.useState( + false + ); + const [selectedBoardId, setSelectedBoardId] = React.useState(null); + + const handleOnItemClick = boardId => { + setSelectedBoardId(boardId); + setOpenConfirmationDialog(true); + }; + + const [searchValue, setSearchValue] = React.useState(''); + const debounceSearch = useMemo( + () => + debounce(value => { + setSearchValue(value); + setCurrentPage(1); + fetchBoards({ page: 1, search: value }); + }, 500), + [fetchBoards] + ); + + const onSearchChange = e => { + const searchValue = e.target.value; + debounceSearch(searchValue); + }; + + return ( + <> + {!isLostedFolder ? ( + + + + ) : ( + + )} + + + + + + + + {intl.formatMessage(messages.searchForAFolder)} + +
+
+ +
+ +
+
+
+
+ {!loading && !error && ( + + )} + {loading && ( +
+ +
+ )} + {error && ( + + + {intl.formatMessage(messages.errorGettingFolders)} + + + + )} + {!loading && !error && totalPages >= 1 && ( + + )} + {!loading && !error && totalPages === 0 && ( + + {intl.formatMessage(messages.noBoardsFound)}'{searchValue}' + + )} + {!loading && !error && ( + + )} +
+
+ setOpenConfirmationDialog(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + {intl.formatMessage(messages.confirmationTitle)} + + + + + + + + + ); +}; + +LoadBoardEditor.propTypes = { + intl: intlShape, + onLoadBoardChange: PropTypes.func, + isLostedFolder: PropTypes.bool +}; + +export default LoadBoardEditor; diff --git a/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.messages.js b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.messages.js new file mode 100644 index 000000000..32c84e812 --- /dev/null +++ b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.messages.js @@ -0,0 +1,46 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + confirmationTitle: { + id: 'cboard.components.LoadBoardEditor.confirmationTitle', + defaultMessage: + 'Are you sure you want change the board to open by this tile?' + }, + openBoardInNewTab: { + id: 'cboard.components.LoadBoardEditor.openBoardInNewTab', + defaultMessage: 'view in new tab' + }, + accept: { + id: 'cboard.components.LoadBoardEditor.accept', + defaultMessage: 'Accept' + }, + cancel: { + id: 'cboard.components.LoadBoardEditor.cancel', + defaultMessage: 'Cancel' + }, + searchFolder: { + id: 'cboard.components.LoadBoardEditor.searchFolder', + defaultMessage: 'Search folder' + }, + searchForAFolder: { + id: 'cboard.components.LoadBoardEditor.searchForAFolder', + defaultMessage: 'Search for a folder' + }, + searchPlaceholder: { + id: 'cboard.components.LoadBoardEditor.searchPlaceholder', + defaultMessage: 'Search…' + }, + errorGettingFolders: { + id: 'cboard.components.LoadBoardEditor.errorGettingFolders', + defaultMessage: + 'Error getting all your folders. Please be sure that you have internet connection to use this feature.' + }, + tryAgain: { + id: 'cboard.components.LoadBoardEditor.tryAgain', + defaultMessage: 'Try Again' + }, + noBoardsFound: { + id: 'cboard.components.LoadBoardEditor.noBoardsFound', + defaultMessage: 'No boards found for: ' + } +}); diff --git a/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.module.css b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.module.css new file mode 100644 index 000000000..40a8ac3d3 --- /dev/null +++ b/src/components/Board/TileEditor/LoadBoardEditor/LoadBoardEditor.module.css @@ -0,0 +1,28 @@ +.loaderContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 16px; +} + +.boardsListContainer { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.pagination { + padding: 8px; +} + +.boardsList { + display: flex; + flex-direction: column; + width: 100%; +} + +.searchButton { + margin-top: 8px; +} diff --git a/src/components/Board/TileEditor/LoadBoardEditor/useAllBoardsFetcher.js b/src/components/Board/TileEditor/LoadBoardEditor/useAllBoardsFetcher.js new file mode 100644 index 000000000..72f00af68 --- /dev/null +++ b/src/components/Board/TileEditor/LoadBoardEditor/useAllBoardsFetcher.js @@ -0,0 +1,34 @@ +import { useState } from 'react'; +import API from '../../../../api'; + +const useBoardsFetcher = () => { + const [pageBoards, setPageBoards] = useState(null); + const [totalPages, setTotalBoards] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchBoards = async ({ page = 1, search = '' }) => { + try { + const LIMIT = 10; + setLoading(true); + setError(null); + const response = await API.getMyBoards({ + limit: LIMIT, + sort: 'name', + page: page, + search + }); + setPageBoards(response.data); + const totalPages = Math.ceil(response.total / LIMIT); + setTotalBoards(totalPages); + setLoading(false); + } catch (error) { + setError(error); + setLoading(false); + } + }; + + return { fetchBoards, pageBoards, totalPages, loading, error }; +}; + +export default useBoardsFetcher; diff --git a/src/components/Board/TileEditor/TileEditor.component.js b/src/components/Board/TileEditor/TileEditor.component.js index 7d8bce603..00b4fd6ef 100644 --- a/src/components/Board/TileEditor/TileEditor.component.js +++ b/src/components/Board/TileEditor/TileEditor.component.js @@ -42,6 +42,9 @@ import { } from '../../../cordova-util'; import { convertImageUrlToCatchable } from '../../../helpers'; import PremiumFeature from '../../PremiumFeature'; +import LoadBoardEditor from './LoadBoardEditor/LoadBoardEditor'; +import { Typography } from '@material-ui/core'; +import { Alert, AlertTitle } from '@material-ui/lab'; export class TileEditor extends Component { static propTypes = { @@ -70,7 +73,9 @@ export class TileEditor extends Component { */ onAddSubmit: PropTypes.func.isRequired, boards: PropTypes.array, - userData: PropTypes.object + userData: PropTypes.object, + folders: PropTypes.array, + onAddApiBoard: PropTypes.func }; static defaultProps = { @@ -424,6 +429,13 @@ export class TileEditor extends Component { } }; + handleLoadBoardChange = ({ boardId }) => { + if (boardId) { + this.props.onAddApiBoard(boardId); + this.updateTileProperty('loadBoard', boardId); + } + }; + handleOnClickImageEditor = () => { this.setState({ openImageEditor: true }); }; @@ -451,7 +463,7 @@ export class TileEditor extends Component { }; render() { - const { open, intl, boards } = this.props; + const { open, intl, boards, folders } = this.props; const currentLabel = this.currentTileProp('labelKey') ? intl.formatMessage({ id: this.currentTileProp('labelKey') }) : this.currentTileProp('label'); @@ -465,7 +477,7 @@ export class TileEditor extends Component { ); const selectBoardElement = ( -
+
{intl.formatMessage(messages.existingBoards)} @@ -498,6 +510,27 @@ export class TileEditor extends Component { ? this.editingTile() : this.state.tile; + const loadBoard = this.currentTileProp('loadBoard'); + const loadBoardName = loadBoard + ? folders.find(({ id }) => id === loadBoard)?.name + : null; + const SHORT_ID_MAX_LENGTH = 14; + const isLocalId = loadBoard.length < SHORT_ID_MAX_LENGTH; + + const LostedFolderAlert = ({ isLocalId }) => { + const alertDescription = !isLocalId + ? intl.formatMessage(messages.loadBoardAlertDescription) + : intl.formatMessage(messages.loadBoardAlertDescriptionLocalId); + return ( + + + {intl.formatMessage(messages.loadBoardAlertTitle)} + + {alertDescription} + + ); + }; + return (
)} + + {this.currentTileProp('loadBoard')?.length > 0 && ( + <> + + {intl.formatMessage(messages.loadBoard)} + +
+ {loadBoardName ? ( + + {loadBoardName} + + ) : ( + + )} + +
+ + )} {this.currentTileProp('type') === 'folder' && selectBoardElement}
diff --git a/src/components/Board/TileEditor/TileEditor.css b/src/components/Board/TileEditor/TileEditor.css index 76ce606f4..ce08cad89 100644 --- a/src/components/Board/TileEditor/TileEditor.css +++ b/src/components/Board/TileEditor/TileEditor.css @@ -76,3 +76,14 @@ .TileEditor__radiogroup__formcontrollabel { margin-top: 5px; } + +.TileEditor__loadBoard_section { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-top: 2px; +} +.TileEditor__loadBoard_Alert { + width: 100%; + margin-top: 4px; +} diff --git a/src/components/Board/TileEditor/TileEditor.messages.js b/src/components/Board/TileEditor/TileEditor.messages.js index 300789b86..ea3f3d147 100644 --- a/src/components/Board/TileEditor/TileEditor.messages.js +++ b/src/components/Board/TileEditor/TileEditor.messages.js @@ -68,5 +68,28 @@ export default defineMessages({ editImage: { id: 'cboard.components.Board.TileEditor.editImage', defaultMessage: 'Edit image' + }, + loadBoard: { + id: 'cboard.components.Board.TileEditor.loadBoard', + defaultMessage: 'Load folder' + }, + loadBoardAlertTitle: { + id: 'cboard.components.Board.TileEditor.loadBoardAlertTitle', + defaultMessage: "We can't find this folder" + }, + loadBoardAlertDescription: { + id: 'cboard.components.Board.TileEditor.loadBoardAlertDescription', + defaultMessage: + 'Try to find it manualy on your remote folders by clicking on the search button.' + }, + loadBoardAlertDescriptionLocalId: { + id: 'cboard.components.Board.TileEditor.loadBoardAlertDescriptionLocalId', + defaultMessage: `It's looks like this folder is localy stored on the device that you + create it. If you want to use it, please make a change in it connected + to the internet. Or edit this value to use another folder.` + }, + loadBoardAlertSearch: { + id: 'cboard.components.Board.TileEditor.loadBoardAlertSearch', + defaultMessage: 'Search folder' } }); diff --git a/src/translations/src/cboard.json b/src/translations/src/cboard.json index 3cba6b95c..d6c376209 100644 --- a/src/translations/src/cboard.json +++ b/src/translations/src/cboard.json @@ -164,6 +164,18 @@ "cboard.components.Board.TileEditor.none": "None", "cboard.components.Board.TileEditor.symbols": "Symbols", "cboard.components.Board.TileEditor.editImage": "Edit image", + "cboard.components.Board.TileEditor.loadBoard": "Load folder", + "cboard.components.Board.TileEditor.loadBoardAlertTitle": "We can't find this folder", + "cboard.components.Board.TileEditor.loadBoardAlertDescription": "Try to find it manualy on your remote folders by clicking on the search button.", + "cboard.components.Board.TileEditor.loadBoardAlertDescriptionLocalId": "It's looks like this folder is localy stored on the device that you create it. If you want to use it, please make a change in it connected to the internet. Or edit this value to use another folder.", + "cboard.components.Board.TileEditor.loadBoardAlertSearch": "Search folder", + "cboard.components.LoadBoardEditor.searchFolder": "Search folder", + "cboard.components.LoadBoardEditor.searchForAFolder": "Search for a folder", + "cboard.components.LoadBoardEditor.searchPlaceholder": "Search…", + "cboard.components.LoadBoardEditor.errorGettingFolders": "Error getting all your folders. Please be sure that you have internet connection to use this feature.", + "cboard.components.LoadBoardEditor.tryAgain": "Try Again", + "cboard.components.LoadBoardEditor.noBoardsFound": "No boards found for '{searchValue}'", + "cboard.components.LoadBoardEditor.openBoardInNewTab": "Open in new tab", "cboard.components.Board.boardEditTitleCancel": "Cancel", "cboard.components.Board.boardEditTitleAccept": "Accept", "cboard.components.Board.userProfileLocked": "User Profile is locked, please unlock settings to see your user profile.", @@ -611,6 +623,9 @@ "cboard.components.UI.Downloader.processing": "Processing...", "cboard.components.UI.Downloader.processingDone": "Process Done!", "cboard.components.UI.Downloader.processingError": "There was an error processing the data, please try again.", + "cboard.components.LoadBoardEditor.confirmationTitle": "Are you sure you want change the board to open by this tile?", + "cboard.components.LoadBoardEditor.accept": "Accept", + "cboard.components.LoadBoardEditor.cancel": "Cancel", "cboard.board.home": "home", "cboard.vocalization.myNameIsAmberley": "my name is Amberley", "cboard.vocalization.niceToMeetYou": "nice to meet you",