diff --git a/i18n/en.pot b/i18n/en.pot index 25ae93aef..9fbf4b1d2 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2020-06-10T01:32:16.274Z\n" -"PO-Revision-Date: 2020-06-10T01:32:16.274Z\n" +"POT-Creation-Date: 2020-08-25T05:26:00.043Z\n" +"PO-Revision-Date: 2020-08-25T05:26:00.043Z\n" msgid "Cancel" msgstr "" @@ -23,10 +23,10 @@ msgid "" "this dashboard?" msgstr "" -msgid "Exit without saving" +msgid "Exit Print preview" msgstr "" -msgid "Go to dashboards" +msgid "Print preview" msgstr "" msgid "Save changes" @@ -35,6 +35,12 @@ msgstr "" msgid "Translate" msgstr "" +msgid "Exit without saving" +msgstr "" + +msgid "Go to dashboards" +msgstr "" + msgid "Search for a dashboard" msgstr "" @@ -44,13 +50,42 @@ msgstr "" msgid "Show less" msgstr "" +msgid "No dashboards found. Use the + button to create a new dashboard." +msgstr "" + +msgid "Requested dashboard not found" +msgstr "" + msgid "No access" msgstr "" -msgid "No dashboards found. Use the + button to create a new dashboard." +msgid "Exit print preview" msgstr "" -msgid "Requested dashboard not found" +msgid "Print" +msgstr "" + +msgid "The pages below are a preview of how the dashboard layout will be printed." +msgstr "" + +msgid "" +"The pages below are a preview of how each dashboard item will be printed on " +"a separate page." +msgstr "" + +msgid "" +"Use the default printer settings for best results and check that all " +"dashboard items have finished loading before printing. This text will not " +"be printed." +msgstr "" + +msgid "dashboard layout" +msgstr "" + +msgid "one item per page" +msgstr "" + +msgid "Print Preview" msgstr "" msgid "{{count}} selected" @@ -73,6 +108,9 @@ msgstr "" msgid "Item type \"{{type}}\" not supported" msgstr "" +msgid "Filters applied" +msgstr "" + msgid "Spacer" msgstr "" @@ -142,7 +180,16 @@ msgstr "" msgid "Dashboard description" msgstr "" -msgid "No description" +msgid "Hide description" +msgstr "" + +msgid "Show description" +msgstr "" + +msgid "Unstar dashboard" +msgstr "" + +msgid "Star dashboard" msgstr "" msgid "Edit" @@ -151,6 +198,18 @@ msgstr "" msgid "Share" msgstr "" +msgid "More" +msgstr "" + +msgid "Dashboard layout" +msgstr "" + +msgid "One item per page" +msgstr "" + +msgid "No description" +msgstr "" + msgid "Visualizations" msgstr "" diff --git a/package.json b/package.json index 71ec66657..14b8d68fc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-grid-layout": "^0.18.3", + "react-grid-layout": "^1.0.0", "react-redux": "^7.2.1", "react-router-dom": "^5.2.0", "redux": "^4.0.5", diff --git a/src/actions/dashboards.js b/src/actions/dashboards.js index a3d0bf11a..3986d03f6 100644 --- a/src/actions/dashboards.js +++ b/src/actions/dashboards.js @@ -10,6 +10,7 @@ import { sGetDashboardById, sGetDashboardsSortedByStarred, } from '../reducers/dashboards' +import { NON_EXISTING_DASHBOARD_ID } from '../reducers/selected' import { sGetUserUsername } from '../reducers/user' import { tSetSelectedDashboardById, acSetSelectedId } from './selected' import { acClearEditDashboard } from './editDashboard' @@ -81,7 +82,7 @@ export const tSelectDashboard = id => async (dispatch, getState) => { if (dashboardToSelect) { dispatch(tSetSelectedDashboardById(dashboardToSelect.id)) } else { - dispatch(acSetSelectedId()) + dispatch(acSetSelectedId(NON_EXISTING_DASHBOARD_ID)) } } catch (err) { return onError(err) diff --git a/src/actions/editDashboard.js b/src/actions/editDashboard.js index d9f9b18e6..cdc6a664a 100644 --- a/src/actions/editDashboard.js +++ b/src/actions/editDashboard.js @@ -10,6 +10,8 @@ import { ADD_DASHBOARD_ITEM, UPDATE_DASHBOARD_ITEM, REMOVE_DASHBOARD_ITEM, + SET_PRINT_PREVIEW_VIEW, + CLEAR_PRINT_PREVIEW_VIEW, } from '../reducers/editDashboard' import { sGetEditDashboardRoot } from '../reducers/editDashboard' import { updateDashboard, postDashboard } from '../api/editDashboard' @@ -17,8 +19,10 @@ import { tSetSelectedDashboardById } from '../actions/selected' import { NEW_ITEM_SHAPE, getGridItemProperties, + getPageBreakItemShape, + getPrintTitlePageItemShape, } from '../components/ItemGrid/gridUtil' -import { itemTypeMap } from '../modules/itemTypes' +import { itemTypeMap, PAGEBREAK, PRINT_TITLE_PAGE } from '../modules/itemTypes' import { convertUiItemsToBackend } from '../modules/uiBackendItemConverter' const onError = error => { @@ -48,6 +52,14 @@ export const acClearEditDashboard = () => ({ type: RECEIVED_NOT_EDITING, }) +export const acSetPrintPreviewView = () => ({ + type: SET_PRINT_PREVIEW_VIEW, +}) + +export const acClearPrintPreviewView = () => ({ + type: CLEAR_PRINT_PREVIEW_VIEW, +}) + export const acSetDashboardTitle = value => ({ type: RECEIVED_TITLE, value, @@ -71,14 +83,26 @@ export const acAddDashboardItem = item => { const id = generateUid() const gridItemProperties = getGridItemProperties(id) + let shape + if (type === PAGEBREAK) { + const yPos = item.yPos || 0 + shape = getPageBreakItemShape(yPos, item.isStatic) + } else if (type === PRINT_TITLE_PAGE) { + shape = getPrintTitlePageItemShape() + } else { + shape = NEW_ITEM_SHAPE + } + return { type: ADD_DASHBOARD_ITEM, value: { id, type, + position: item.position || null, [itemPropName]: item.content, ...NEW_ITEM_SHAPE, ...gridItemProperties, + ...shape, }, } } diff --git a/src/actions/editItemFilters.js b/src/actions/editItemFilters.js deleted file mode 100644 index ca5d53653..000000000 --- a/src/actions/editItemFilters.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - REMOVE_EDIT_ITEM_FILTER, - SET_EDIT_ITEM_FILTERS, - CLEAR_EDIT_ITEM_FILTERS, -} from '../reducers/editItemFilters' - -// actions - -export const acRemoveEditItemFilter = id => ({ - type: REMOVE_EDIT_ITEM_FILTER, - id, -}) - -export const acClearEditItemFilters = () => ({ - type: CLEAR_EDIT_ITEM_FILTERS, -}) - -export const acSetEditItemFilters = filters => ({ - type: SET_EDIT_ITEM_FILTERS, - filters, -}) diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index f7b613382..000000000 --- a/src/actions/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { tSetSelectedDashboardById } from './selected' -import { acSetFilterName } from './dashboardsFilter' - -export const tSelectDashboardById = (id, name) => dispatch => { - // select dashboard by id - dispatch(tSetSelectedDashboardById(id, name)) - - // reset filter - dispatch(acSetFilterName()) -} diff --git a/src/actions/printDashboard.js b/src/actions/printDashboard.js new file mode 100644 index 000000000..06fae0cb9 --- /dev/null +++ b/src/actions/printDashboard.js @@ -0,0 +1,80 @@ +import { generateUid } from 'd2/uid' + +import { + SET_PRINT_DASHBOARD, + CLEAR_PRINT_DASHBOARD, + SET_PRINT_DASHBOARD_LAYOUT, + ADD_PRINT_DASHBOARD_ITEM, + REMOVE_PRINT_DASHBOARD_ITEM, + UPDATE_PRINT_DASHBOARD_ITEM, +} from '../reducers/printDashboard' +import { + NEW_ITEM_SHAPE, + getGridItemProperties, + getPageBreakItemShape, + getPrintTitlePageItemShape, +} from '../components/ItemGrid/gridUtil' +import { itemTypeMap, PAGEBREAK } from '../modules/itemTypes' + +// actions + +export const acSetPrintDashboard = (dashboard, items) => { + const val = { + ...dashboard, + dashboardItems: items, + } + + return { + type: SET_PRINT_DASHBOARD, + value: val, + } +} + +export const acClearPrintDashboard = () => ({ + type: CLEAR_PRINT_DASHBOARD, +}) + +export const acUpdatePrintDashboardLayout = value => ({ + type: SET_PRINT_DASHBOARD_LAYOUT, + value, +}) + +export const acAddPrintDashboardItem = item => { + const type = item.type + delete item.type + const itemPropName = itemTypeMap[type].propName + + const id = generateUid() + const gridItemProperties = getGridItemProperties(id) + + let shape + if (type === PAGEBREAK) { + const yPos = item.yPos || 0 + shape = getPageBreakItemShape(yPos, item.isStatic) + } else { + shape = getPrintTitlePageItemShape() + } + + return { + type: ADD_PRINT_DASHBOARD_ITEM, + value: { + id, + type, + position: item.position || null, + [itemPropName]: item.content, + ...NEW_ITEM_SHAPE, + ...gridItemProperties, + ...shape, + }, + } +} + +export const acRemovePrintDashboardItem = value => ({ + type: REMOVE_PRINT_DASHBOARD_ITEM, + value, +}) + +export const acUpdatePrintDashboardItem = item => ({ + type: UPDATE_PRINT_DASHBOARD_ITEM, + value: item, +}) diff --git a/src/actions/selected.js b/src/actions/selected.js index e92f6a01f..818ab5177 100644 --- a/src/actions/selected.js +++ b/src/actions/selected.js @@ -1,6 +1,4 @@ import { getCustomDashboards, sGetDashboardById } from '../reducers/dashboards' -import { sGetIsEditing } from '../reducers/editDashboard' -import { sGetEditItemFiltersRoot } from '../reducers/editItemFilters' import { SET_SELECTED_ID, SET_SELECTED_ISLOADING, @@ -11,15 +9,17 @@ import { import { sGetUserUsername } from '../reducers/user' import { acSetDashboardItems, acAppendDashboards } from './dashboards' -import { acClearEditItemFilters } from './editItemFilters' -import { acClearItemFilters, acSetItemFilters } from './itemFilters' +import { acClearItemFilters } from './itemFilters' import { tGetMessages } from '../components/Item/MessagesItem/actions' import { acReceivedSnackbarMessage, acCloseSnackbar } from './snackbar' import { acAddVisualization } from './visualizations' import { apiFetchDashboard } from '../api/dashboards' import { storePreferredDashboardId } from '../api/localStorage' -import { apiGetShowDescription } from '../api/description' +import { + apiGetShowDescription, + apiPostShowDescription, +} from '../api/description' import { withShape } from '../components/ItemGrid/gridUtil' import { loadingDashboardMsg } from '../components/SnackbarMessage/SnackbarMessage' @@ -53,20 +53,6 @@ export const acSetSelectedShowDescription = value => ({ }) // thunks - -export const tLoadDashboard = id => async dispatch => { - try { - const dash = await apiFetchDashboard(id) - - dispatch(acAppendDashboards(dash)) - - return Promise.resolve(dash) - } catch (err) { - console.log('Error: ', err) - return err - } -} - export const tSetSelectedDashboardById = id => async (dispatch, getState) => { dispatch(acSetSelectedIsLoading(true)) @@ -86,13 +72,14 @@ export const tSetSelectedDashboardById = id => async (dispatch, getState) => { }, 500) const onSuccess = selected => { + dispatch(acAppendDashboards(selected)) + const customDashboard = getCustomDashboards(selected)[0] dispatch(acSetDashboardItems(withShape(customDashboard.dashboardItems))) storePreferredDashboardId(sGetUserUsername(getState()), id) - // add visualizations to store customDashboard.dashboardItems.forEach(item => { switch (item.type) { case REPORT_TABLE: @@ -110,18 +97,7 @@ export const tSetSelectedDashboardById = id => async (dispatch, getState) => { } }) - const state = getState() - if (id === sGetSelectedId(state)) { - if (sGetIsEditing(state)) { - // disable filters when switching to edit mode - dispatch(acClearItemFilters()) - } else { - // enable filters when switching to view mode - dispatch(acSetItemFilters(sGetEditItemFiltersRoot(state))) - } - } else { - // clear filters when switching dashboard - dispatch(acClearEditItemFilters()) + if (id !== sGetSelectedId(getState())) { dispatch(acClearItemFilters()) } @@ -142,9 +118,9 @@ export const tSetSelectedDashboardById = id => async (dispatch, getState) => { } try { - const selected = await dispatch(tLoadDashboard(id)) + const dashboard = await apiFetchDashboard(id) - return onSuccess(selected) + return onSuccess(dashboard) } catch (err) { return onError(err) } @@ -167,3 +143,21 @@ export const tSetShowDescription = () => async dispatch => { return onError(err) } } + +export const tUpdateShowDescription = value => async dispatch => { + const onSuccess = () => { + dispatch(acSetSelectedShowDescription(value)) + } + + const onError = error => { + console.log('Error (apiGetShowDescription): ', error) + return error + } + + try { + await apiPostShowDescription(value) + return onSuccess() + } catch (err) { + return onError(err) + } +} diff --git a/src/components/App.css b/src/components/App.css index 9efd3f573..81077ca02 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -2,6 +2,19 @@ body { background-color: #f4f6f8; } +@media print { + body { + width: 100% !important; + padding: 0 !important; + margin: 0 !important; + background-color: white; + } + + .app-shell-app { + overflow: visible !important; + } +} + .dashboard-wrapper { background-color: #f4f6f8; padding-left: 10px; diff --git a/src/components/App.js b/src/components/App.js index 486396e10..2859b9299 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -5,7 +5,13 @@ import PropTypes from 'prop-types' import i18n from '@dhis2/d2-i18n' import { CssVariables } from '@dhis2/ui' -import { EDIT, VIEW, NEW } from './Dashboard/dashboardModes' +import { + EDIT, + VIEW, + NEW, + PRINT, + PRINT_LAYOUT, +} from './Dashboard/dashboardModes' import { acReceivedUser } from '../actions/user' import { tFetchDashboards } from '../actions/dashboards' import { tSetControlBarRows } from '../actions/controlBar' @@ -63,6 +69,20 @@ export class App extends Component { )} /> + ( + + )} + /> + ( + + )} + /> diff --git a/src/components/ControlBar/EditBar.js b/src/components/ControlBar/EditBar.js index addffaf43..b56703a2c 100644 --- a/src/components/ControlBar/EditBar.js +++ b/src/components/ControlBar/EditBar.js @@ -5,13 +5,14 @@ import { Redirect } from 'react-router-dom' import i18n from '@dhis2/d2-i18n' import ControlBar from './ControlBar' import TranslationDialog from '@dhis2/d2-ui-translation-dialog' -import { Button } from '@dhis2/ui' +import { Button, ButtonStrip } from '@dhis2/ui' import ConfirmDeleteDialog from './ConfirmDeleteDialog' import { tSaveDashboard, acClearEditDashboard, } from '../../actions/editDashboard' +import { acClearPrintDashboard } from '../../actions/printDashboard' import { tDeleteDashboard, acSetDashboardDisplayName, @@ -19,7 +20,12 @@ import { import { sGetEditDashboardRoot, sGetIsNewDashboard, + sGetIsPrintPreviewView, } from '../../reducers/editDashboard' +import { + acSetPrintPreviewView, + acClearPrintPreviewView, +} from '../../actions/editDashboard' import { CONTROL_BAR_ROW_HEIGHT, MIN_ROW_COUNT, @@ -54,6 +60,15 @@ export class EditBar extends Component { }) } + onPrintPreview = () => { + if (this.props.isPrintPreviewView) { + this.props.clearPrintPreview() + this.props.clearPrintDashboard() + } else { + this.props.showPrintPreview() + } + } + onDiscard = () => { this.props.onDiscardChanges() const redirectUrl = this.props.dashboardId @@ -141,12 +156,41 @@ export class EditBar extends Component { /> ) : null + renderActionButtons = () => { + const printPreviewText = this.props.isPrintPreviewView + ? i18n.t('Exit Print preview') + : i18n.t('Print preview') + return ( +
+ + + + + {this.props.dashboardId ? ( + + ) : null} + {this.props.dashboardId && this.props.deleteAccess ? ( + + ) : null} + +
+ ) + } + render() { if (this.state.redirectUrl) { return } - const { dashboardId, deleteAccess, updateAccess } = this.props + const { updateAccess } = this.props const controlBarHeight = getControlBarHeight(MIN_ROW_COUNT) const discardBtnText = updateAccess @@ -157,32 +201,7 @@ export class EditBar extends Component { <>
- {updateAccess ? ( -
- - - - - {dashboardId ? ( - - - - ) : null} - {dashboardId && deleteAccess ? ( - - ) : null} -
- ) : null} + {updateAccess ? this.renderActionButtons() : null}
- - + - +
- - - + - +
- - - + - +
- - - + - - + +
- - + +
, + [EDIT]: , + [NEW]: , + [PRINT]: , + [PRINT_LAYOUT]: , +} -class Dashboard extends Component { +export class Dashboard extends Component { setDashboard = () => { if (this.props.dashboardsLoaded) { - const id = this.props.match.params.dashboardId || null + const id = this.props.match.params.dashboardId || null || null this.props.selectDashboard(id) + this.setHeaderbarVisibility() + this.scrollToTop() } } + setHeaderbarVisibility = () => { + const header = document.getElementsByTagName('header')[0] + if (isPrintMode(this.props.mode)) { + header.classList.add('hidden') + } else { + header.classList.remove('hidden') + } + } + scrollToTop = () => { window.scrollTo(0, 0) } @@ -33,26 +75,68 @@ class Dashboard extends Component { } render() { - switch (this.props.mode) { - case EDIT: - return - case NEW: - return - default: - return + const { id, mode, dashboardsLoaded, dashboardsIsEmpty } = this.props + + if (!dashboardsLoaded || id === null) { + return ( + + + + + + ) + } + + if (mode === NEW) { + return dashboardMap[mode] + } + + if (dashboardsIsEmpty) { + return ( + <> + + + + + ) } + + if (id === NON_EXISTING_DASHBOARD_ID) { + return ( + <> + + + + + ) + } + + return dashboardMap[mode] } } Dashboard.propTypes = { + dashboardsIsEmpty: PropTypes.bool, dashboardsLoaded: PropTypes.bool, + id: PropTypes.string, match: PropTypes.object, mode: PropTypes.string, selectDashboard: PropTypes.func, } const mapStateToProps = state => { - return { dashboardsLoaded: !sDashboardsIsFetching(state) } + const dashboards = sGetAllDashboards(state) + return { + dashboardsIsEmpty: isEmpty(dashboards), + dashboardsLoaded: !sDashboardsIsFetching(state), + id: sGetSelectedId(state), + } } export default connect(mapStateToProps, { diff --git a/src/components/Dashboard/DashboardContent.js b/src/components/Dashboard/DashboardContent.js deleted file mode 100644 index 64b607689..000000000 --- a/src/components/Dashboard/DashboardContent.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import TitleBar from '../TitleBar/TitleBar' -import ItemGrid from '../ItemGrid/ItemGrid' -import FilterBar from '../FilterBar/FilterBar' - -export const DashboardContent = props => ( - <> - - - - -) - -DashboardContent.propTypes = { - editMode: PropTypes.bool, -} - -export default DashboardContent diff --git a/src/components/Dashboard/EditDashboard.js b/src/components/Dashboard/EditDashboard.js index 9b3a4de96..3b364383e 100644 --- a/src/components/Dashboard/EditDashboard.js +++ b/src/components/Dashboard/EditDashboard.js @@ -3,29 +3,20 @@ import { connect } from 'react-redux' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' +import DashboardVerticalOffset from './DashboardVerticalOffset' +import TitleBar from '../TitleBar/TitleBar' +import ItemGrid from '../ItemGrid/ItemGrid' +import EditBar from '../ControlBar/EditBar' +import LayoutPrintPreview from './PrintLayoutDashboard' +import NoContentMessage from '../../widgets/NoContentMessage' import { acSetEditDashboard } from '../../actions/editDashboard' import { sGetSelectedId } from '../../reducers/selected' import { sGetDashboardById, sGetDashboardItems, - sDashboardsIsFetching, } from '../../reducers/dashboards' -import DashboardVerticalOffset from './DashboardVerticalOffset' -import DashboardContent from './DashboardContent' -import EditBar from '../ControlBar/EditBar' -import NoContentMessage from '../../widgets/NoContentMessage' +import { sGetIsPrintPreviewView } from '../../reducers/editDashboard' -export const Content = ({ updateAccess }) => { - return updateAccess ? ( - - ) : ( - - ) -} - -Content.propTypes = { - updateAccess: PropTypes.bool, -} export class EditDashboard extends Component { state = { initialized: false, @@ -48,15 +39,15 @@ export class EditDashboard extends Component { } } - getDashboardContent = () => { - const contentNotReady = - !this.props.dashboardsLoaded || this.props.id === null + renderGrid = () => { + if (this.props.isPrintPreviewView) { + return + } return (
- {contentNotReady ? null : ( - - )} + +
) } @@ -66,7 +57,11 @@ export class EditDashboard extends Component { <> - {this.getDashboardContent()} + {this.props.updateAccess ? ( + this.renderGrid() + ) : ( + + )} ) } @@ -74,8 +69,7 @@ export class EditDashboard extends Component { EditDashboard.propTypes = { dashboard: PropTypes.object, - dashboardsLoaded: PropTypes.bool, - id: PropTypes.string, + isPrintPreviewView: PropTypes.bool, items: PropTypes.array, setEditDashboard: PropTypes.func, updateAccess: PropTypes.bool, @@ -90,10 +84,9 @@ const mapStateToProps = state => { return { dashboard, - id, updateAccess, items: sGetDashboardItems(state), - dashboardsLoaded: !sDashboardsIsFetching(state), + isPrintPreviewView: sGetIsPrintPreviewView(state), } } diff --git a/src/components/Dashboard/NewDashboard.js b/src/components/Dashboard/NewDashboard.js index 55ffd7a67..ea610f53d 100644 --- a/src/components/Dashboard/NewDashboard.js +++ b/src/components/Dashboard/NewDashboard.js @@ -2,10 +2,14 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import PropTypes from 'prop-types' -import { acSetEditNewDashboard } from '../../actions/editDashboard' import DashboardVerticalOffset from './DashboardVerticalOffset' import EditBar from '../ControlBar/EditBar' -import DashboardContent from './DashboardContent' +import TitleBar from '../TitleBar/TitleBar' +import ItemGrid from '../ItemGrid/ItemGrid' +import LayoutPrintPreview from './PrintLayoutDashboard' + +import { acSetEditNewDashboard } from '../../actions/editDashboard' +import { sGetIsPrintPreviewView } from '../../reducers/editDashboard' class NewDashboard extends Component { componentDidMount() { @@ -17,18 +21,28 @@ class NewDashboard extends Component { <> -
- -
+ {this.props.isPrintPreviewView ? ( + + ) : ( +
+ + +
+ )} ) } } NewDashboard.propTypes = { + isPrintPreviewView: PropTypes.bool, setNewDashboard: PropTypes.func, } -export default connect(null, { +const mapStateToProps = state => ({ + isPrintPreviewView: sGetIsPrintPreviewView(state), +}) + +export default connect(mapStateToProps, { setNewDashboard: acSetEditNewDashboard, })(NewDashboard) diff --git a/src/components/Dashboard/PrintActionsBar.js b/src/components/Dashboard/PrintActionsBar.js new file mode 100644 index 000000000..deb2304de --- /dev/null +++ b/src/components/Dashboard/PrintActionsBar.js @@ -0,0 +1,36 @@ +import React from 'react' +import PropTypes from 'prop-types' +import i18n from '@dhis2/d2-i18n' +import { Button } from '@dhis2/ui' +import { Link } from 'react-router-dom' +import LessHorizontalIcon from '../../icons/LessHorizontal' +import { a4LandscapeWidthPx } from '../../modules/printUtils' + +import classes from './styles/PrintActionsBar.module.css' + +const PrintActionsBar = ({ id }) => { + const width = + a4LandscapeWidthPx < window.innerWidth + ? a4LandscapeWidthPx + : window.innerWidth + + return ( +
+
+ + + + +
+
+ ) +} + +PrintActionsBar.propTypes = { + id: PropTypes.string, +} + +export default PrintActionsBar diff --git a/src/components/Dashboard/PrintDashboard.js b/src/components/Dashboard/PrintDashboard.js new file mode 100644 index 000000000..850644f89 --- /dev/null +++ b/src/components/Dashboard/PrintDashboard.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import sortBy from 'lodash/sortBy' + +import PrintInfo from './PrintInfo' +import PrintActionsBar from './PrintActionsBar' +import PrintItemGrid from '../ItemGrid/PrintItemGrid' +import { + acSetPrintDashboard, + acAddPrintDashboardItem, + acRemovePrintDashboardItem, +} from '../../actions/printDashboard' +import { sGetSelectedId } from '../../reducers/selected' +import { + sGetDashboardById, + sGetDashboardItems, +} from '../../reducers/dashboards' +import { PAGEBREAK, PRINT_TITLE_PAGE, SPACER } from '../../modules/itemTypes' +import { a4LandscapeWidthPx } from '../../modules/printUtils' + +import classes from './styles/PrintDashboard.module.css' + +import './styles/print.css' +import './styles/print-oipp.css' + +export class PrintDashboard extends Component { + state = { + initialized: false, + } + + initPrintDashboard = () => { + if (this.props.dashboard) { + this.setState({ initialized: true }) + + //sort the items by Y pos so they print in order of top to bottom + const sortedItems = sortBy(this.props.items, ['y', 'x']) + + this.props.setPrintDashboard(this.props.dashboard, sortedItems) + + // remove spacers - don't want empty pages + let spacerCount = 0 + this.props.items.forEach(item => { + if (item.type === SPACER) { + spacerCount += 1 + this.props.removeDashboardItem(item.id) + } + }) + + // insert page breaks into the document flow to create the "pages" + // when previewing the print + for ( + let i = 0; + i < (this.props.items.length - spacerCount) * 2; + i += 2 + ) { + this.props.addDashboardItem({ + type: PAGEBREAK, + position: i, + isStatic: false, + }) + } + + this.props.addDashboardItem({ type: PRINT_TITLE_PAGE }) + } + } + + componentDidMount() { + this.initPrintDashboard() + } + + componentDidUpdate() { + if (!this.state.initialized) { + this.initPrintDashboard() + } + } + + render() { + return ( + <> + +
+ +
+ +
+
+ + ) + } +} + +PrintDashboard.propTypes = { + addDashboardItem: PropTypes.func, + dashboard: PropTypes.object, + items: PropTypes.array, + removeDashboardItem: PropTypes.func, + setPrintDashboard: PropTypes.func, +} + +const mapStateToProps = state => { + const id = sGetSelectedId(state) + const dashboard = id ? sGetDashboardById(state, id) : null + + return { + dashboard, + id, + items: sGetDashboardItems(state), + } +} + +export default connect(mapStateToProps, { + setPrintDashboard: acSetPrintDashboard, + addDashboardItem: acAddPrintDashboardItem, + removeDashboardItem: acRemovePrintDashboardItem, +})(PrintDashboard) diff --git a/src/components/Dashboard/PrintInfo.js b/src/components/Dashboard/PrintInfo.js new file mode 100644 index 000000000..bb711d705 --- /dev/null +++ b/src/components/Dashboard/PrintInfo.js @@ -0,0 +1,49 @@ +import React from 'react' +import PropTypes from 'prop-types' +import i18n from '@dhis2/d2-i18n' +import { a4LandscapeWidthPx } from '../../modules/printUtils' + +import classes from './styles/PrintInfo.module.css' + +const PrintInfo = ({ isLayout }) => { + const maxWidth = + a4LandscapeWidthPx < window.innerWidth + ? a4LandscapeWidthPx + : window.innerWidth + + const infoPartOne = isLayout + ? i18n.t( + 'The pages below are a preview of how the dashboard layout will be printed.' + ) + : i18n.t( + 'The pages below are a preview of how each dashboard item will be printed on a separate page.' + ) + + const infoPartTwo = i18n.t( + 'Use the default printer settings for best results and check that all dashboard items have finished loading before printing. This text will not be printed.' + ) + + const infoHeader = isLayout + ? i18n.t('dashboard layout') + : i18n.t('one item per page') + + return ( +
+

+ {`${i18n.t('Print Preview')}: ${infoHeader}`} +

+
+

{`${infoPartOne} ${infoPartTwo}`}

+
+
+
+ ) +} + +PrintInfo.propTypes = { + isLayout: PropTypes.bool, +} + +export default PrintInfo diff --git a/src/components/Dashboard/PrintLayoutDashboard.js b/src/components/Dashboard/PrintLayoutDashboard.js new file mode 100644 index 000000000..f98d67fe9 --- /dev/null +++ b/src/components/Dashboard/PrintLayoutDashboard.js @@ -0,0 +1,152 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' + +import PrintInfo from './PrintInfo' +import PrintActionsBar from './PrintActionsBar' +import PrintLayoutItemGrid from '../ItemGrid/PrintLayoutItemGrid' +import { + acSetPrintDashboard, + acAddPrintDashboardItem, + acUpdatePrintDashboardItem, +} from '../../actions/printDashboard' +import { sGetSelectedId } from '../../reducers/selected' +import { + sGetEditDashboardRoot, + sGetEditDashboardItems, +} from '../../reducers/editDashboard' +import { + sGetDashboardById, + sGetDashboardItems, +} from '../../reducers/dashboards' +import { PAGEBREAK, PRINT_TITLE_PAGE } from '../../modules/itemTypes' +import { a4LandscapeWidthPx } from '../../modules/printUtils' + +import classes from './styles/PrintLayoutDashboard.module.css' + +import './styles/print.css' +import './styles/print-layout.css' + +const isEven = n => n % 2 == 0 + +const addPageBreaks = ({ items, addDashboardItem }) => { + // add enough page breaks so that each item could + // be put on its own page. Due to the react-grid-layout + // unit system, we have to estimate roughly the size of each + // page. At regular intervals adding an extra unit, like a leap year :P + let yPos = 0 + const yPosList = [] + for (let i = 0; i < items.length; ++i) { + if (i === 0) { + yPos += 35 + } else if (i === 4 || i === 10) { + yPos += 40 + } else if (isEven(i)) { + yPos += 39 + } else { + yPos += 40 + } + yPosList.push(yPos) + } + + for (let i = 0; i < items.length; ++i) { + addDashboardItem({ type: PAGEBREAK, yPos: yPosList[i] }) + } +} + +export class PrintLayoutDashboard extends Component { + state = { + initialized: false, + } + + initPrintLayoutDashboard = () => { + if (this.props.dashboard) { + this.setState({ initialized: true }) + + this.props.setPrintDashboard(this.props.dashboard, this.props.items) + + // If any items are taller than one page, reduce it to one + // page (react-grid-layout units) + this.props.items.forEach(item => { + if (item.h > 34) { + this.props.updateDashboardItem( + Object.assign({}, item, { h: 34 }) + ) + } + }) + + addPageBreaks(this.props) + + if (!this.props.fromEdit) { + this.props.addDashboardItem({ type: PRINT_TITLE_PAGE }) + } + } + } + + componentDidMount() { + this.initPrintLayoutDashboard() + } + + componentDidUpdate() { + if (!this.state.initialized) { + this.initPrintLayoutDashboard() + } + } + + render() { + const style = this.props.fromEdit ? { marginTop: '100px' } : {} + return ( + <> + {!this.props.fromEdit && ( + + )} +
+ {!this.props.fromEdit && } +
+ +
+
+ + ) + } +} + +PrintLayoutDashboard.propTypes = { + addDashboardItem: PropTypes.func, + dashboard: PropTypes.object, + fromEdit: PropTypes.bool, + items: PropTypes.array, + setPrintDashboard: PropTypes.func, + updateDashboardItem: PropTypes.func, +} + +const mapStateToProps = (state, ownProps) => { + const id = sGetSelectedId(state) + + if (ownProps.fromEdit) { + const dashboard = sGetEditDashboardRoot(state) + + return { + dashboard, + id, + items: sGetEditDashboardItems(state), + } + } + + const dashboard = id ? sGetDashboardById(state, id) : null + + return { + dashboard, + id, + items: sGetDashboardItems(state), + } +} + +export default connect(mapStateToProps, { + setPrintDashboard: acSetPrintDashboard, + addDashboardItem: acAddPrintDashboardItem, + updateDashboardItem: acUpdatePrintDashboardItem, +})(PrintLayoutDashboard) diff --git a/src/components/Dashboard/ViewDashboard.js b/src/components/Dashboard/ViewDashboard.js index 20aa4586f..519647d1e 100644 --- a/src/components/Dashboard/ViewDashboard.js +++ b/src/components/Dashboard/ViewDashboard.js @@ -1,72 +1,54 @@ -import React from 'react' +import React, { useEffect } from 'react' import { connect } from 'react-redux' -import isEmpty from 'lodash/isEmpty' -import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' -import { - sGetAllDashboards, - sDashboardsIsFetching, -} from '../../reducers/dashboards' -import { sGetSelectedId } from '../../reducers/selected' +import TitleBar from '../TitleBar/TitleBar' +import ItemGrid from '../ItemGrid/ItemGrid' +import FilterBar from '../FilterBar/FilterBar' import DashboardsBar from '../ControlBar/DashboardsBar' import DashboardVerticalOffset from './DashboardVerticalOffset' -import DashboardContent from './DashboardContent' -import NoContentMessage from '../../widgets/NoContentMessage' - -export const Content = ({ hasDashboardContent, dashboardsIsEmpty }) => { - const msg = dashboardsIsEmpty - ? i18n.t( - 'No dashboards found. Use the + button to create a new dashboard.' - ) - : i18n.t('Requested dashboard not found') - - return hasDashboardContent ? ( - - ) : ( - - ) -} - -Content.propTypes = { - dashboardsIsEmpty: PropTypes.bool, - hasDashboardContent: PropTypes.bool, -} - -export const ViewDashboard = ({ id, dashboardsIsEmpty, dashboardsLoaded }) => { - const hasDashboardContent = id && !dashboardsIsEmpty - const contentNotReady = !dashboardsLoaded || id === null +import { sGetIsEditing } from '../../reducers/editDashboard' +import { sGetIsPrinting } from '../../reducers/printDashboard' +import { acClearEditDashboard } from '../../actions/editDashboard' +import { acClearPrintDashboard } from '../../actions/printDashboard' + +export const ViewDashboard = props => { + useEffect(() => { + if (props.dashboardIsEditing) { + props.clearEditDashboard() + } else if (props.dashboardIsPrinting) { + props.clearPrintDashboard() + } + }, [props.dashboardIsEditing, props.dashboardIsPrinting]) return ( <>
- {contentNotReady ? null : ( - - )} + + +
) } ViewDashboard.propTypes = { - dashboardsIsEmpty: PropTypes.bool, - dashboardsLoaded: PropTypes.bool, - id: PropTypes.string, + clearEditDashboard: PropTypes.func, + clearPrintDashboard: PropTypes.func, + dashboardIsEditing: PropTypes.bool, + dashboardIsPrinting: PropTypes.bool, } const mapStateToProps = state => { - const dashboards = sGetAllDashboards(state) - return { - id: sGetSelectedId(state), - dashboardsIsEmpty: isEmpty(dashboards), - dashboardsLoaded: !sDashboardsIsFetching(state), + dashboardIsEditing: sGetIsEditing(state), + dashboardIsPrinting: sGetIsPrinting(state), } } -export default connect(mapStateToProps)(ViewDashboard) +export default connect(mapStateToProps, { + clearEditDashboard: acClearEditDashboard, + clearPrintDashboard: acClearPrintDashboard, +})(ViewDashboard) diff --git a/src/components/Dashboard/__tests__/Dashboard.spec.js b/src/components/Dashboard/__tests__/Dashboard.spec.js new file mode 100644 index 000000000..c5717a5e0 --- /dev/null +++ b/src/components/Dashboard/__tests__/Dashboard.spec.js @@ -0,0 +1,110 @@ +import React from 'react' +import { shallow } from 'enzyme' +import toJson from 'enzyme-to-json' + +import { Dashboard } from '../Dashboard' +import { NEW, VIEW, EDIT, PRINT, PRINT_LAYOUT } from '../dashboardModes' +import { NON_EXISTING_DASHBOARD_ID } from '../../../reducers/selected' + +jest.mock('../../ControlBar/DashboardsBar', () => 'DashboardsBar') +jest.mock('../DashboardVerticalOffset', () => 'DashboardVerticalOffset') +jest.mock('../../../widgets/NoContentMessage', () => 'NoContentMessage') +jest.mock('../ViewDashboard', () => 'ViewDashboard') +jest.mock('../EditDashboard', () => 'EditDashboard') +jest.mock('../NewDashboard', () => 'NewDashboard') +jest.mock('../PrintDashboard', () => 'PrintDashboard') +jest.mock('../PrintLayoutDashboard', () => 'PrintLayoutDashboard') + +describe('Dashboard', () => { + let props + let shallowDashboard + const dashboard = () => { + if (!shallowDashboard) { + shallowDashboard = shallow() + } + return shallowDashboard + } + + Object.defineProperty(global.document, 'getElementsByTagName', { + value: () => [ + { + classList: { + add: Function.prototype, + remove: Function.prototype, + }, + }, + ], + }) + + Object.defineProperty(global.window, 'scrollTo', { + value: Function.prototype, + }) + + beforeEach(() => { + props = { + dashboardsIsEmpty: false, + dashboardsLoaded: false, + id: null, + match: { params: { dashboardId: null } }, + mode: VIEW, + selectDashboard: jest.fn(), + } + shallowDashboard = undefined + }) + it('renders correctly when dashboards not loaded and id is null', () => { + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders correctly when dashboards loaded and id is null', () => { + props.dashboardsLoaded = true + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders correctly when dashboards not loaded and id is not null', () => { + props.id = 'rainbowdash' + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders NEW dashboard', () => { + props.dashboardsLoaded = true + props.id = 'rainbowdash' + props.mode = NEW + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders correctly when no dashboards found', () => { + props.dashboardsLoaded = true + props.dashboardsIsEmpty = true + props.id = 'rainbowdash' + props.mode = VIEW + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders correctly when dashboard id is not valid', () => { + props.dashboardsLoaded = true + props.id = NON_EXISTING_DASHBOARD_ID + props.mode = VIEW + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders EDIT dashboard', () => { + props.dashboardsLoaded = true + props.id = 'rainbowdash' + props.mode = EDIT + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders PRINT dashboard', () => { + props.dashboardsLoaded = true + props.id = 'rainbowdash' + props.mode = PRINT + expect(toJson(dashboard())).toMatchSnapshot() + }) + + it('renders PRINT_LAYOUT dashboard', () => { + props.dashboardsLoaded = true + props.id = 'rainbowdash' + props.mode = PRINT_LAYOUT + expect(toJson(dashboard())).toMatchSnapshot() + }) +}) diff --git a/src/components/Dashboard/__tests__/EditDashboard.spec.js b/src/components/Dashboard/__tests__/EditDashboard.spec.js index 64aafd423..fe16d0c63 100644 --- a/src/components/Dashboard/__tests__/EditDashboard.spec.js +++ b/src/components/Dashboard/__tests__/EditDashboard.spec.js @@ -1,89 +1,44 @@ import React from 'react' import { shallow } from 'enzyme' +import toJson from 'enzyme-to-json' -import { EditDashboard, Content } from '../EditDashboard' -import { NoContentMessage } from '../../../widgets/NoContentMessage' +import { EditDashboard } from '../EditDashboard' -jest.mock('../DashboardContent', () => () =>
) // eslint-disable-line react/display-name -jest.mock('../../ControlBar/EditBar', () => () =>
) // eslint-disable-line react/display-name -jest.mock('../DashboardVerticalOffset', () => () =>
) // eslint-disable-line react/display-name +jest.mock('../DashboardVerticalOffset', () => 'DashboardVerticalOffset') +jest.mock('../../ControlBar/EditBar', () => 'EditBar') +jest.mock('../../TitleBar/TitleBar', () => 'TitleBar') +jest.mock('../../ItemGrid/ItemGrid', () => 'ItemGrid') +jest.mock('../../../widgets/NoContentMessage', () => 'NoContentMessage') +jest.mock('../PrintLayoutDashboard', () => 'LayoutPrintPreview') describe('EditDashboard', () => { let props - let shallowEditDashboard - const editDashboard = () => { - if (!shallowEditDashboard) { - shallowEditDashboard = shallow() - } - return shallowEditDashboard - } - - const assertContent = hasContent => { - const content = editDashboard().find(Content) - - expect(content.length).toBe(1) - expect(content.dive().find(NoContentMessage)).toHaveLength( - hasContent ? 0 : 1 - ) - } beforeEach(() => { props = { dashboard: undefined, id: undefined, - updateAccess: undefined, + updateAccess: true, items: undefined, dashboardsLoaded: undefined, + isPrintPreviewView: undefined, } - shallowEditDashboard = undefined }) - describe('when "dashboardsLoaded" is false', () => { - it('does not render any children inside the div', () => { - props.dashboardsLoaded = false - - expect( - editDashboard() - .find('.dashboard-wrapper') - .children().length - ).toBe(0) - }) + it('renders message when not enough access', () => { + props.updateAccess = false + const tree = shallow() + expect(toJson(tree)).toMatchSnapshot() }) - describe('when "dashboardsLoaded" is true', () => { - beforeEach(() => { - props.dashboardsLoaded = true - }) - - describe('when "id" is null', () => { - it('does not render any children inside the div', () => { - props.id = null - expect( - editDashboard() - .find('.dashboard-wrapper') - .children().length - ).toBe(0) - }) - }) - - describe('when id is not null', () => { - beforeEach(() => { - props.id = 'abc123' - }) - - describe('when updateAccess is true', () => { - it('renders DashboardContent', () => { - props.updateAccess = true - assertContent(true) - }) - }) + it('renders message when enough access', () => { + const tree = shallow() + expect(toJson(tree)).toMatchSnapshot() + }) - describe('when updateAccess is false', () => { - it('renders a NoContentMessage', () => { - props.updateAccess = false - assertContent(false) - }) - }) - }) + it('renders print preview', () => { + props.isPrintPreviewView = true + const tree = shallow() + expect(toJson(tree)).toMatchSnapshot() }) }) diff --git a/src/components/Dashboard/__tests__/ViewDashboard.spec.js b/src/components/Dashboard/__tests__/ViewDashboard.spec.js index c04de77a3..f3dac4493 100644 --- a/src/components/Dashboard/__tests__/ViewDashboard.spec.js +++ b/src/components/Dashboard/__tests__/ViewDashboard.spec.js @@ -1,92 +1,51 @@ import React from 'react' import { shallow } from 'enzyme' -import { ViewDashboard, Content } from '../ViewDashboard' -import { NoContentMessage } from '../../../widgets/NoContentMessage' +import toJson from 'enzyme-to-json' +import { ViewDashboard } from '../ViewDashboard' -jest.mock('../DashboardContent', () => () =>
) // eslint-disable-line react/display-name -jest.mock('../../ControlBar/DashboardsBar', () => () =>
) // eslint-disable-line react/display-name -jest.mock('../DashboardVerticalOffset', () => () =>
) // eslint-disable-line react/display-name +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useEffect: f => f(), +})) + +jest.mock('../../ControlBar/DashboardsBar', () => 'DashboardsBar') +jest.mock('../DashboardVerticalOffset', () => 'DashboardVerticalOffset') +jest.mock('../../TitleBar/TitleBar', () => 'TitleBar') +jest.mock('../../FilterBar/FilterBar', () => 'FilterBar') +jest.mock('../../ItemGrid/ItemGrid', () => 'ItemGrid') describe('ViewDashboard', () => { let props - let shallowViewDashboard - const viewDashboard = () => { - if (!shallowViewDashboard) { - shallowViewDashboard = shallow() - } - return shallowViewDashboard - } - - const assertContent = hasContent => { - const content = viewDashboard().find(Content) - - expect(content.length).toBe(1) - expect(content.dive().find(NoContentMessage)).toHaveLength( - hasContent ? 0 : 1 - ) - } beforeEach(() => { props = { - id: undefined, - dashboardsIsEmpty: undefined, - dashboardsLoaded: undefined, + clearEditDashboard: jest.fn(), + clearPrintDashboard: jest.fn(), + dashboardIsEditing: false, + dashboardIsPrinting: false, } - shallowViewDashboard = undefined }) - describe('when "dashboardsLoaded" is false', () => { - it('does not render any children inside the div', () => { - props.dashboardsLoaded = false - - expect( - viewDashboard() - .find('.dashboard-wrapper') - .children().length - ).toBe(0) - }) + afterEach(() => { + jest.clearAllMocks() }) - describe('when "dashboardsLoaded" is true', () => { - beforeEach(() => { - props.dashboardsLoaded = true - }) - - describe('when "id" is null', () => { - it('does not render any children inside the div', () => { - props.id = null - expect( - viewDashboard() - .find('.dashboard-wrapper') - .children().length - ).toBe(0) - }) - }) - - describe('when "dashboardsIsEmpty" is true', () => { - beforeEach(() => { - props.dashboardsIsEmpty = true - }) - - it('renders a NoContentMessage', () => { - assertContent(false) - }) - }) - - describe('when "dashboardsIsEmpty" is false', () => { - beforeEach(() => { - props.dashboardsIsEmpty = false - }) + it('renders correctly default', () => { + const tree = shallow() + expect(toJson(tree)).toMatchSnapshot() + }) - describe('when id is not null', () => { - beforeEach(() => { - props.id = '123xyz' - }) + it('clears edit dashboard after redirecting from Edit mode', () => { + props.dashboardIsEditing = true + shallow() + expect(props.clearEditDashboard).toHaveBeenCalled() + expect(props.clearPrintDashboard).not.toHaveBeenCalled() + }) - it('renders DashboardContent', () => { - assertContent(true) - }) - }) - }) + it('clears print dashboard after redirecting from Print mode', () => { + props.dashboardIsPrinting = true + shallow() + expect(props.clearEditDashboard).not.toHaveBeenCalled() + expect(props.clearPrintDashboard).toHaveBeenCalled() }) }) diff --git a/src/components/Dashboard/__tests__/__snapshots__/Dashboard.spec.js.snap b/src/components/Dashboard/__tests__/__snapshots__/Dashboard.spec.js.snap new file mode 100644 index 000000000..83e5f23de --- /dev/null +++ b/src/components/Dashboard/__tests__/__snapshots__/Dashboard.spec.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dashboard renders EDIT dashboard 1`] = ``; + +exports[`Dashboard renders NEW dashboard 1`] = ``; + +exports[`Dashboard renders PRINT dashboard 1`] = ``; + +exports[`Dashboard renders PRINT_LAYOUT dashboard 1`] = ``; + +exports[`Dashboard renders correctly when dashboard id is not valid 1`] = ` + + + + + +`; + +exports[`Dashboard renders correctly when dashboards loaded and id is null 1`] = ` + + + + + +`; + +exports[`Dashboard renders correctly when dashboards not loaded and id is not null 1`] = ` + + + + + +`; + +exports[`Dashboard renders correctly when dashboards not loaded and id is null 1`] = ` + + + + + +`; + +exports[`Dashboard renders correctly when no dashboards found 1`] = ` + + + + + +`; diff --git a/src/components/Dashboard/__tests__/__snapshots__/EditDashboard.spec.js.snap b/src/components/Dashboard/__tests__/__snapshots__/EditDashboard.spec.js.snap new file mode 100644 index 000000000..514654f9a --- /dev/null +++ b/src/components/Dashboard/__tests__/__snapshots__/EditDashboard.spec.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EditDashboard renders message when enough access 1`] = ` + + + +
+ + +
+
+`; + +exports[`EditDashboard renders message when not enough access 1`] = ` + + + + + +`; + +exports[`EditDashboard renders print preview 1`] = ` + + + + + +`; diff --git a/src/components/Dashboard/__tests__/__snapshots__/ViewDashboard.spec.js.snap b/src/components/Dashboard/__tests__/__snapshots__/ViewDashboard.spec.js.snap new file mode 100644 index 000000000..2f9cd8ca2 --- /dev/null +++ b/src/components/Dashboard/__tests__/__snapshots__/ViewDashboard.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewDashboard renders correctly default 1`] = ` + + + +
+ + + +
+
+`; diff --git a/src/components/Dashboard/dashboardModes.js b/src/components/Dashboard/dashboardModes.js index c20c0f475..e97e3667e 100644 --- a/src/components/Dashboard/dashboardModes.js +++ b/src/components/Dashboard/dashboardModes.js @@ -1,3 +1,9 @@ export const EDIT = 'edit' export const NEW = 'new' export const VIEW = 'view' +export const PRINT = 'print' +export const PRINT_LAYOUT = 'print-layout' + +export const isEditMode = mode => [EDIT, NEW].includes(mode) +export const isPrintMode = mode => [PRINT, PRINT_LAYOUT].includes(mode) +export const isViewMode = mode => mode === VIEW diff --git a/src/components/Dashboard/styles/PrintActionsBar.module.css b/src/components/Dashboard/styles/PrintActionsBar.module.css new file mode 100644 index 000000000..f8b84c85d --- /dev/null +++ b/src/components/Dashboard/styles/PrintActionsBar.module.css @@ -0,0 +1,23 @@ +.container { + background-color: var(--colors-grey600); + width: 100%; + padding: 3px; +} + +.inner { + margin-left: 30px; + display: flex; + justify-content: space-between; +} + +.link { + display: inline-block; + text-decoration: none; + margin-right: 4px; +} + +@media print { + .container { + display: none; + } +} diff --git a/src/components/Dashboard/styles/PrintDashboard.module.css b/src/components/Dashboard/styles/PrintDashboard.module.css new file mode 100644 index 000000000..95c2e39b3 --- /dev/null +++ b/src/components/Dashboard/styles/PrintDashboard.module.css @@ -0,0 +1,27 @@ +.wrapper { + background-color: #f4f6f8; + margin-left: 30px; +} + +.pageOuter { + padding: 10px; + background-color: white; + border-radius: 3px; + box-shadow: 0px 0px 3px 0px var(--colors-grey500); + margin-bottom: 50px; +} + +@media print { + .pageWrapper { + display: none; + } + + .wrapper { + background-color: white; + } + + .pageOuter { + box-shadow: none; + border-radius: none; + } +} diff --git a/src/components/Dashboard/styles/PrintInfo.module.css b/src/components/Dashboard/styles/PrintInfo.module.css new file mode 100644 index 000000000..ccb9f9729 --- /dev/null +++ b/src/components/Dashboard/styles/PrintInfo.module.css @@ -0,0 +1,28 @@ +.infoWrapper { + margin-bottom: 15px; + width: 100%; +} + +.infoHeader { + font-size: 18px; + font-weight: 600; + color: #3e4a59; +} + +.info { + font-size: 14px; + line-height: 1.3; + color: #3e4a59; + max-width: 800px; +} + +.divider { + width: 1102px; + margin-left: 0px; +} + +@media print { + .infoWrapper { + display: none; + } +} diff --git a/src/components/Dashboard/styles/PrintLayoutDashboard.module.css b/src/components/Dashboard/styles/PrintLayoutDashboard.module.css new file mode 100644 index 000000000..95c2e39b3 --- /dev/null +++ b/src/components/Dashboard/styles/PrintLayoutDashboard.module.css @@ -0,0 +1,27 @@ +.wrapper { + background-color: #f4f6f8; + margin-left: 30px; +} + +.pageOuter { + padding: 10px; + background-color: white; + border-radius: 3px; + box-shadow: 0px 0px 3px 0px var(--colors-grey500); + margin-bottom: 50px; +} + +@media print { + .pageWrapper { + display: none; + } + + .wrapper { + background-color: white; + } + + .pageOuter { + box-shadow: none; + border-radius: none; + } +} diff --git a/src/components/Dashboard/styles/print-layout.css b/src/components/Dashboard/styles/print-layout.css new file mode 100644 index 000000000..7437b79b2 --- /dev/null +++ b/src/components/Dashboard/styles/print-layout.css @@ -0,0 +1,16 @@ +@media print { + .react-grid-item.layout.PAGEBREAK { + display: none !important; + } +} + +.react-grid-item.layout.PAGEBREAK { + margin-left: -23px; + min-height: 50px !important; + height: 50px !important; + margin-bottom: 40px !important; +} + +.react-grid-item.layout.SPACER { + visibility: hidden; +} diff --git a/src/components/Dashboard/styles/print-oipp.css b/src/components/Dashboard/styles/print-oipp.css new file mode 100644 index 000000000..1168da6d0 --- /dev/null +++ b/src/components/Dashboard/styles/print-oipp.css @@ -0,0 +1,45 @@ +@media print { + .react-grid-layout.print { + display: block !important; + } + + .react-grid-item.oipp.PAGEBREAK { + height: 20px !important; + visibility: hidden; + } + + .react-grid-item.oipp:not(.PAGEBREAK) { + margin-top: 0px !important; + } +} + +.react-grid-layout.print { + display: flex; + flex-direction: column; +} + +.react-grid-item.oipp { + transform: none !important; +} +.react-grid-item.oipp:not(.PAGEBREAK) { + break-inside: avoid; + break-after: page; +} + +.react-grid-item.oipp:not(.PAGEBREAK) { + position: static !important; + margin-bottom: 20px; + margin-left: 30px; + margin-top: 30px; +} + +.react-grid-item.oipp.PAGEBREAK { + position: relative !important; + margin-left: -14px; + min-height: 50px !important; + height: 50px !important; +} + +.react-grid-item.oipp.SPACER { + display: none; +} diff --git a/src/components/Dashboard/styles/print.css b/src/components/Dashboard/styles/print.css new file mode 100644 index 000000000..695056dce --- /dev/null +++ b/src/components/Dashboard/styles/print.css @@ -0,0 +1,62 @@ +header.hidden { + display: none; +} + +.react-grid-item.print { + display: flex; + flex-direction: column; +} + +.react-grid-item.print .dashboard-item-content { + flex: 1; +} + +.react-grid-item.PAGEBREAK.removed { + display: none !important; +} + +.react-grid-item.PAGEBREAK { + width: 1120px !important; + border: none !important; + box-shadow: none !important; + background-color: #f4f6f8; +} + +.react-grid-item.PAGEBREAK::before { + content: ''; + position: absolute; + top: -3px; + left: 3px; + width: 1102px; + height: 3px; + box-shadow: 0px 0px 3px 0px #6e7a8a; +} + +.react-grid-item.PAGEBREAK::after { + content: ''; + position: absolute; + bottom: -3px; + left: 3px; + width: 1102px; + height: 3px; + box-shadow: 0px 0px 3px 0px #6e7a8a; +} + +.react-grid-item.PRINT_TITLE_PAGE { + border: none; + box-shadow: none; + justify-content: center; +} + +.react-grid-item.print .mapboxgl-control-container { + display: none; +} + +.react-grid-item.print .dhis2-map-legend { + display: none; +} + +@page { + size: A4 landscape; + margin: 10pt; +} diff --git a/src/components/FilterBar/FilterBar.js b/src/components/FilterBar/FilterBar.js index f530b7fc9..1466360d6 100644 --- a/src/components/FilterBar/FilterBar.js +++ b/src/components/FilterBar/FilterBar.js @@ -1,24 +1,20 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import PropTypes from 'prop-types' -import { createSelector } from 'reselect' -import { sGetDimensions } from '../../reducers/dimensions' -import { sGetItemFiltersRoot } from '../../reducers/itemFilters' +import FilterBadge from './FilterBadge' + +import { sGetNamedItemFilters } from '../../reducers/itemFilters' import { sGetControlBarUserRows } from '../../reducers/controlBar' import { getControlBarHeight } from '../ControlBar/controlBarDimensions' import { acRemoveItemFilter } from '../../actions/itemFilters' -import { acRemoveEditItemFilter } from '../../actions/editItemFilters' import { acSetActiveModalDimension } from '../../actions/activeModalDimension' -import FilterBadge from './FilterBadge' - import classes from './styles/FilterBar.module.css' export class FilterBar extends Component { onBadgeRemove = id => { this.props.removeItemFilter(id) - this.props.removeEditItemFilter(id) } onBadgeClick = id => { @@ -30,6 +26,7 @@ export class FilterBar extends Component { render() { const { filters, userRows } = this.props + const top = getControlBarHeight(userRows) + 10 return filters.length ? ( @@ -49,7 +46,6 @@ export class FilterBar extends Component { FilterBar.propTypes = { filters: PropTypes.array.isRequired, - removeEditItemFilter: PropTypes.func.isRequired, removeItemFilter: PropTypes.func.isRequired, setActiveModalDimension: PropTypes.func, userRows: PropTypes.number, @@ -58,35 +54,14 @@ FilterBar.propTypes = { FilterBar.defaultProps = { filters: [], removeItemFIlter: Function.prototype, - removeEditItemFilter: Function.prototype, } -// simplify the filters structure to: -// [{ id: 'pe', name: 'Period', values: [{ id: 2019: name: '2019' }, {id: 'LAST_MONTH', name: 'Last month' }]}, ...] -const filtersSelector = createSelector( - [sGetItemFiltersRoot, sGetDimensions], - (filters, dimensions) => - Object.keys(filters).reduce((arr, id) => { - arr.push({ - id: id, - name: dimensions.find(dimension => dimension.id === id).name, - values: filters[id].map(value => ({ - id: value.id, - name: value.displayName || value.name, - })), - }) - - return arr - }, []) -) - const mapStateToProps = state => ({ - filters: filtersSelector(state), + filters: sGetNamedItemFilters(state), userRows: sGetControlBarUserRows(state), }) export default connect(mapStateToProps, { setActiveModalDimension: acSetActiveModalDimension, removeItemFilter: acRemoveItemFilter, - removeEditItemFilter: acRemoveEditItemFilter, })(FilterBar) diff --git a/src/components/Item/AppItem/Item.js b/src/components/Item/AppItem/Item.js index 7834ba3da..2ef13e594 100644 --- a/src/components/Item/AppItem/Item.js +++ b/src/components/Item/AppItem/Item.js @@ -4,10 +4,15 @@ import { connect } from 'react-redux' import NotInterestedIcon from '@material-ui/icons/NotInterested' import { FILTER_ORG_UNIT } from '../../../actions/itemFilters' -import { sGetItemFiltersRoot } from '../../../reducers/itemFilters' -import ItemHeader from '../ItemHeader' +import { + sGetItemFiltersRoot, + DEFAULT_STATE_ITEM_FILTERS, +} from '../../../reducers/itemFilters' +import ItemHeader from '../ItemHeader/ItemHeader' import Line from '../../../widgets/Line' +import { isEditMode } from '../../Dashboard/dashboardModes' + const getIframeSrc = (appDetails, item, itemFilters) => { let iframeSrc = `${appDetails.launchUrl}?dashboardItemId=${item.id}` @@ -26,7 +31,7 @@ const getIframeSrc = (appDetails, item, itemFilters) => { return iframeSrc } -const AppItem = ({ item, itemFilters }, context) => { +const AppItem = ({ dashboardMode, item, itemFilters }, context) => { let appDetails const appKey = item.appKey @@ -39,7 +44,11 @@ const AppItem = ({ item, itemFilters }, context) => { return appDetails && appDetails.name && appDetails.launchUrl ? ( <> - +