{
- const fullscreenElement =
- document.fullscreenElement || document.webkitFullscreenElement
-
- return fullscreenElement?.classList.contains(
- getGridItemDomElementClassName(itemId)
- )
-}
diff --git a/src/components/Item/VisualizationItem/styles/Item.module.css b/src/components/Item/VisualizationItem/styles/Item.module.css
new file mode 100644
index 000000000..1714dd947
--- /dev/null
+++ b/src/components/Item/VisualizationItem/styles/Item.module.css
@@ -0,0 +1,17 @@
+.content {
+ composes: content from '../../styles/Item.module.css';
+ position: relative;
+}
+
+.fullscreen {
+ composes: fullscreen from '../../styles/Item.module.css';
+}
+
+.scrollbox {
+ overflow: auto;
+}
+
+.edit,
+.print {
+ flex: 1;
+}
diff --git a/src/components/Item/VisualizationItem/styles/ItemFooter.module.css b/src/components/Item/VisualizationItem/styles/ItemFooter.module.css
index 842cb06d3..8aea44210 100644
--- a/src/components/Item/VisualizationItem/styles/ItemFooter.module.css
+++ b/src/components/Item/VisualizationItem/styles/ItemFooter.module.css
@@ -9,7 +9,7 @@
margin-inline-end: 0px;
block-size: 1px;
border: none;
- background-color: var(--colors-grey100);
+ background-color: var(--colors-grey400);
}
.cover:hover {
diff --git a/src/components/Item/styles/Item.module.css b/src/components/Item/styles/Item.module.css
new file mode 100644
index 000000000..4fde9469f
--- /dev/null
+++ b/src/components/Item/styles/Item.module.css
@@ -0,0 +1,11 @@
+.content {
+ margin-block-start: 0;
+ margin-block-end: var(--item-content-padding);
+ margin-inline: var(--item-content-padding);
+ overflow: hidden;
+}
+
+.fullscreen {
+ margin-block-end: 0;
+ margin-inline: 0;
+}
diff --git a/src/components/ProgressiveLoadingContainer.js b/src/components/ProgressiveLoadingContainer.js
index 5dc4d06a5..ce2bdb7f1 100644
--- a/src/components/ProgressiveLoadingContainer.js
+++ b/src/components/ProgressiveLoadingContainer.js
@@ -1,26 +1,76 @@
+import i18n from '@dhis2/d2-i18n'
+import { Divider, spacers, CenteredContent } from '@dhis2/ui'
import debounce from 'lodash/debounce.js'
import pick from 'lodash/pick.js'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
+import { getVisualizationName } from '../modules/item.js'
+import {
+ APP,
+ MESSAGES,
+ RESOURCES,
+ REPORTS,
+ isVisualizationType,
+} from '../modules/itemTypes.js'
+import ItemHeader from './Item/ItemHeader/ItemHeader.js'
const defaultDebounceMs = 100
const defaultBufferFactor = 0.25
const observerConfig = { attributes: true, childList: false, subtree: false }
+const getItemHeader = ({ item, apps }) => {
+ if (isVisualizationType(item)) {
+ const title = getVisualizationName(item)
+ return
+ }
+
+ let title
+ if ([MESSAGES, RESOURCES, REPORTS].includes(item.type)) {
+ const titleMap = {
+ [MESSAGES]: i18n.t('Messages'),
+ [RESOURCES]: i18n.t('Resources'),
+ [REPORTS]: i18n.t('Reports'),
+ }
+ title = titleMap[item.type]
+ } else if (item.type === APP) {
+ let appDetails
+ const appKey = item.appKey
+
+ if (appKey) {
+ appDetails = apps.find((app) => app.key === appKey)
+ }
+
+ const hideTitle = appDetails?.settings?.dashboardWidget?.hideTitle
+ title = hideTitle ? null : appDetails.name
+ }
+
+ return !title ? null : (
+ <>
+
+
+ >
+ )
+}
+
class ProgressiveLoadingContainer extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
+ item: PropTypes.object.isRequired,
+ apps: PropTypes.array,
bufferFactor: PropTypes.number,
className: PropTypes.string,
+ dashboardIsCached: PropTypes.bool,
debounceMs: PropTypes.number,
forceLoad: PropTypes.bool,
- itemId: PropTypes.string,
+ fullsreenView: PropTypes.bool,
+ isOffline: PropTypes.bool,
style: PropTypes.object,
}
static defaultProps = {
debounceMs: defaultDebounceMs,
bufferFactor: defaultBufferFactor,
forceLoad: false,
+ fullsreenView: false,
}
state = {
@@ -30,6 +80,7 @@ class ProgressiveLoadingContainer extends Component {
debouncedCheckShouldLoad = null
handlerOptions = { passive: true }
observer = null
+ isObserving = null
checkShouldLoad() {
if (!this.containerRef) {
@@ -39,7 +90,15 @@ class ProgressiveLoadingContainer extends Component {
// force load item regardless of its position
if (this.forceLoad && !this.state.shouldLoad) {
this.setState({ shouldLoad: true })
- this.removeHandler()
+ if (!this.props.isOffline || this.props.dashboardIsCached) {
+ this.removeHandler()
+ }
+ return
+ }
+
+ // when in fullscreen view, load is not based on
+ // position relative to viewport but instead on forceLoad only
+ if (this.props.fullsreenView) {
return
}
@@ -52,7 +111,9 @@ class ProgressiveLoadingContainer extends Component {
rect.top < window.innerHeight + bufferPx
) {
this.setState({ shouldLoad: true })
- this.removeHandler()
+ if (!this.props.isOffline || this.props.dashboardIsCached) {
+ this.removeHandler()
+ }
}
}
@@ -84,6 +145,7 @@ class ProgressiveLoadingContainer extends Component {
this.observer = new MutationObserver(mutationCallback)
this.observer.observe(this.containerRef, observerConfig)
+ this.isObserving = true
}
removeHandler() {
@@ -98,6 +160,7 @@ class ProgressiveLoadingContainer extends Component {
})
this.observer.disconnect()
+ this.isObserving = false
}
componentDidMount() {
@@ -116,9 +179,16 @@ class ProgressiveLoadingContainer extends Component {
}
render() {
- const { children, className, style, ...props } = this.props
-
- const shouldLoad = this.state.shouldLoad || props.forceLoad
+ const {
+ children,
+ className,
+ style,
+ apps,
+ item,
+ dashboardIsCached,
+ isOffline,
+ ...props
+ } = this.props
const eventProps = pick(props, [
'onMouseDown',
@@ -127,15 +197,38 @@ class ProgressiveLoadingContainer extends Component {
'onTouchEnd',
])
+ const renderContent = this.state.shouldLoad || props.forceLoad
+
+ const getContent = () => {
+ if (isOffline && !dashboardIsCached && this.isObserving !== false) {
+ return !renderContent ? null : (
+
+ {getItemHeader({ item, apps })}
+
+ {i18n.t('Not available offline')}
+
+
+ )
+ } else {
+ return renderContent && children
+ }
+ }
+
return (
(this.containerRef = ref)}
style={style}
className={className}
- data-test={`dashboarditem-${props.itemId}`}
+ data-test={`dashboarditem-${item.id}`}
{...eventProps}
>
- {shouldLoad && children}
+ {getContent()}
)
}
diff --git a/src/components/styles/App.css b/src/components/styles/App.css
index cc3e22b4b..cd900b6f7 100644
--- a/src/components/styles/App.css
+++ b/src/components/styles/App.css
@@ -39,29 +39,6 @@ table.pivot * {
background-color: #48a999;
}
-div:fullscreen,
-div:-webkit-full-screen {
- background-color: white;
-}
-
-div:-webkit-full-screen {
- object-fit: contain;
- position: fixed !important;
- inset-block-start: 0px !important;
- inset-inline-end: 0px !important;
- inset-block-end: 0px !important;
- inset-inline-start: 0px !important;
- box-sizing: border-box !important;
- min-inline-size: 0px !important;
- max-inline-size: none !important;
- min-block-size: 0px !important;
- max-block-size: none !important;
- inline-size: 100% !important;
- block-size: 100% !important;
- transform: none !important;
- margin: 0px !important;
-}
-
@media print {
body {
inline-size: 100% !important;
diff --git a/src/components/styles/ItemGrid.css b/src/components/styles/ItemGrid.css
index be7aa3757..63e9f5ed3 100644
--- a/src/components/styles/ItemGrid.css
+++ b/src/components/styles/ItemGrid.css
@@ -14,8 +14,10 @@
.react-grid-item.edit,
.react-grid-item.EVENT_VISUALIZATION,
.react-grid-item.RESOURCES,
+.react-grid-item.REPORTS,
.react-grid-item.TEXT,
.react-grid-item.MESSAGES,
+.react-grid-item.EVENT_REPORT,
.react-grid-item.APP {
display: flex;
flex-direction: column;
@@ -59,47 +61,3 @@
.react-resizable-handle {
background: none;
}
-
-/* dashboard item - content */
-
-.dashboard-item-content {
- margin-block-start: 0;
- margin-block-end: var(--item-content-padding);
- margin-inline-start: var(--item-content-padding);
- margin-inline-end: var(--item-content-padding);
- overflow: auto;
-}
-
-.dashboard-item-content-hidden-title {
- margin-block-start: 5px;
- margin-block-end: 4px;
- margin-inline-start: 4px;
- margin-inline-end: 4px;
- overflow: auto;
-}
-
-.TEXT .dashboard-item-content {
- padding-block-end: var(--item-content-padding);
-}
-
-.EVENT_REPORT .dashboard-item-content {
- position: relative;
-}
-
-.CHART .dashboard-item-content,
-.VISUALIZATION .dashboard-item-content,
-.MAP .dashboard-item-content,
-.EVENT_CHART .dashboard-item-content,
-.EVENT_VISUALIZATION .dashboard-item-content {
- position: relative;
- overflow: hidden;
-}
-
-.react-grid-item.edit .dashboard-item-content,
-.react-grid-item.RESOURCES .dashboard-item-content,
-.react-grid-item.TEXT .dashboard-item-content,
-.react-grid-item.MESSAGES .dashboard-item-content,
-.react-grid-item.APP .dashboard-item-content,
-.react-grid-item.APP .dashboard-item-content-hidden-title {
- flex: 1;
-}
diff --git a/src/modules/itemTypes.js b/src/modules/itemTypes.js
index 985aa3cfd..fd919f301 100644
--- a/src/modules/itemTypes.js
+++ b/src/modules/itemTypes.js
@@ -71,6 +71,7 @@ export const itemTypeMap = {
appName: 'Data Visualizer',
appKey: 'data-visualizer',
defaultItemCount: 10,
+ supportsFullscreen: true,
},
[REPORT_TABLE]: {
id: REPORT_TABLE,
@@ -82,6 +83,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-data-visualizer/#/${id}`,
appName: 'Data Visualizer',
+ supportsFullscreen: true,
},
[CHART]: {
id: CHART,
@@ -93,6 +95,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-data-visualizer/#/${id}`,
appName: 'Data Visualizer',
+ supportsFullscreen: true,
},
[MAP]: {
id: MAP,
@@ -104,6 +107,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-maps/?id=${id}`,
appName: 'Maps',
+ supportsFullscreen: true,
},
[EVENT_REPORT]: {
id: EVENT_REPORT,
@@ -114,6 +118,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-event-reports/?id=${id}`,
appName: 'Event Reports',
+ supportsFullscreen: true,
},
[EVENT_CHART]: {
id: EVENT_CHART,
@@ -124,6 +129,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-event-visualizer/?id=${id}`,
appName: 'Event Visualizer',
+ supportsFullscreen: true,
},
[EVENT_VISUALIZATION]: {
id: EVENT_VISUALIZATION,
@@ -136,11 +142,13 @@ export const itemTypeMap = {
appUrl: (id) => `api/apps/line-listing/index.html#/${id}`,
appName: 'Line Listing',
appKey: 'line-listing',
+ supportsFullscreen: true,
},
[APP]: {
endPointName: 'apps',
propName: 'appKey',
pluralTitle: i18n.t('Apps'),
+ supportsFullscreen: true,
},
[REPORTS]: {
id: REPORTS,
@@ -158,6 +166,7 @@ export const itemTypeMap = {
return `api/reports/${id}/data.pdf?t=${new Date().getTime()}`
}
},
+ supportsFullscreen: true,
},
[RESOURCES]: {
id: RESOURCES,
@@ -165,6 +174,7 @@ export const itemTypeMap = {
propName: 'resources',
pluralTitle: i18n.t('Resources'),
appUrl: (id) => `api/documents/${id}/data`,
+ supportsFullscreen: true,
},
[USERS]: {
id: USERS,
@@ -173,22 +183,28 @@ export const itemTypeMap = {
pluralTitle: i18n.t('Users'),
appUrl: (id) =>
`dhis-web-dashboard-integration/profile.action?id=${id}`,
+ supportsFullscreen: false,
},
[TEXT]: {
id: TEXT,
propName: 'text',
+ supportsFullscreen: true,
},
[MESSAGES]: {
propName: 'messages',
+ supportsFullscreen: false,
},
[SPACER]: {
propName: 'text',
+ supportsFullscreen: false,
},
[PAGEBREAK]: {
propName: 'text',
+ supportsFullscreen: false,
},
[PRINT_TITLE_PAGE]: {
propName: 'text',
+ supportsFullscreen: false,
},
}
@@ -211,6 +227,9 @@ export const getItemUrl = (type, item, baseUrl) => {
return url
}
+export const itemTypeSupportsFullscreen = (type) =>
+ itemTypeMap[type]?.supportsFullscreen
+
export const getItemIcon = (type) => {
switch (type) {
case REPORT_TABLE:
diff --git a/src/pages/edit/ItemGrid.js b/src/pages/edit/ItemGrid.js
index 2e503d04a..fa2464fe7 100644
--- a/src/pages/edit/ItemGrid.js
+++ b/src/pages/edit/ItemGrid.js
@@ -60,7 +60,7 @@ const EditItemGrid = ({
'edit',
getGridItemDomElementClassName(item.id)
)}
- itemId={item.id}
+ item={item}
>
- {
+const ResponsiveItemGrid = ({ dashboardIsCached }) => {
+ const dashboardId = useSelector(sGetSelectedId)
+ const dashboardItems = useSelector(sGetSelectedDashboardItems)
const { width } = useWindowDimensions()
+ const { apps } = useCachedDataQuery()
const [expandedItems, setExpandedItems] = useState({})
const [displayItems, setDisplayItems] = useState(dashboardItems)
const [layoutSm, setLayoutSm] = useState([])
const [gridWidth, setGridWidth] = useState(0)
const [forceLoad, setForceLoad] = useState(false)
const { recordingState } = useCacheableSection(dashboardId)
+ const { isDisconnected: isOffline } = useDhis2ConnectionStatus()
const firstOfTypes = getFirstOfTypes(dashboardItems)
+ const slideshowElementRef = useRef(null)
+
+ const {
+ slideshowItemIndex,
+ sortedItems,
+ isEnteringSlideshow,
+ exitSlideshow,
+ nextItem,
+ prevItem,
+ } = useSlideshow(displayItems, slideshowElementRef)
+
+ const isSlideshowView = slideshowItemIndex !== null
useEffect(() => {
+ const getItemsWithAdjustedHeight = (items) =>
+ items.map((item) => {
+ const expandedItem = expandedItems[item.id]
+
+ if (expandedItem && expandedItem === true) {
+ const expandedHeight = isSmallScreen(width)
+ ? EXPANDED_HEIGHT_SM
+ : EXPANDED_HEIGHT
+ return Object.assign({}, item, {
+ h: item.h + expandedHeight,
+ smallOriginalH: getProportionalHeight(item, width),
+ })
+ }
+
+ return item
+ })
setLayoutSm(
getItemsWithAdjustedHeight(getSmallLayout(dashboardItems, width))
)
@@ -68,23 +104,6 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
setExpandedItems(newExpandedItems)
}
- const getItemsWithAdjustedHeight = (items) =>
- items.map((item) => {
- const expandedItem = expandedItems[item.id]
-
- if (expandedItem && expandedItem === true) {
- const expandedHeight = isSmallScreen(width)
- ? EXPANDED_HEIGHT_SM
- : EXPANDED_HEIGHT
- return Object.assign({}, item, {
- h: item.h + expandedHeight,
- smallOriginalH: getProportionalHeight(item, width),
- })
- }
-
- return item
- })
-
const getItemComponent = (item) => {
if (!layoutSm.length) {
return
@@ -94,16 +113,48 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
item.firstOfType = true
}
+ const itemIsFullscreen = isSlideshowView
+ ? sortedItems[slideshowItemIndex].id === item.id
+ : null
+
+ // Force load next and previous items for slideshow view
+ const nextslideshowItemIndex =
+ slideshowItemIndex === sortedItems.length - 1
+ ? 0
+ : slideshowItemIndex + 1
+ const prevslideshowItemIndex =
+ slideshowItemIndex === 0
+ ? sortedItems.length - 1
+ : slideshowItemIndex - 1
+
+ const itemIsNextPrevFullscreen =
+ isSlideshowView &&
+ (sortedItems[nextslideshowItemIndex].id === item.id ||
+ sortedItems[prevslideshowItemIndex].id === item.id)
+
return (
- {
dashboardMode={VIEW}
isRecording={forceLoad}
onToggleItemExpanded={onToggleItemExpanded}
+ isFullscreen={itemIsFullscreen}
+ sortIndex={sortedItems.findIndex((i) => i.id === item.id)}
/>
)
@@ -131,40 +184,50 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
}
return (
-
- {getItemComponents(displayItems)}
-
+
+ {getItemComponents(displayItems)}
+
+ {isSlideshowView && !isEnteringSlideshow && (
+
+ )}
+
)
}
ResponsiveItemGrid.propTypes = {
- dashboardId: PropTypes.string,
- dashboardItems: PropTypes.array,
+ dashboardIsCached: PropTypes.bool,
}
-const mapStateToProps = (state) => ({
- dashboardItems: sGetSelectedDashboardItems(state),
- dashboardId: sGetSelectedId(state),
-})
-
-export default connect(mapStateToProps)(ResponsiveItemGrid)
+export default ResponsiveItemGrid
diff --git a/src/pages/view/SlideshowControlbar.js b/src/pages/view/SlideshowControlbar.js
new file mode 100644
index 000000000..58b6deef8
--- /dev/null
+++ b/src/pages/view/SlideshowControlbar.js
@@ -0,0 +1,93 @@
+import i18n from '@dhis2/d2-i18n'
+import {
+ IconChevronRight24,
+ IconChevronLeft24,
+ IconCross24,
+ colors,
+} from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { SlideshowFiltersInfo } from './SlideshowFiltersInfo.js'
+import styles from './styles/SlideshowControlbar.module.css'
+
+const SlideshowControlbar = ({
+ slideshowItemIndex,
+ exitSlideshow,
+ nextItem,
+ prevItem,
+ numItems,
+}) => {
+ const navigationDisabled = numItems === 1
+
+ const NextArrow =
+ document.dir === 'ltr' ? IconChevronRight24 : IconChevronLeft24
+ const PrevArrow =
+ document.dir === 'ltr' ? IconChevronLeft24 : IconChevronRight24
+
+ return (
+
+ )
+}
+
+SlideshowControlbar.propTypes = {
+ exitSlideshow: PropTypes.func.isRequired,
+ nextItem: PropTypes.func.isRequired,
+ numItems: PropTypes.number.isRequired,
+ prevItem: PropTypes.func.isRequired,
+ slideshowItemIndex: PropTypes.number.isRequired,
+}
+
+export default SlideshowControlbar
diff --git a/src/pages/view/SlideshowFiltersInfo.js b/src/pages/view/SlideshowFiltersInfo.js
new file mode 100644
index 000000000..85f1514d0
--- /dev/null
+++ b/src/pages/view/SlideshowFiltersInfo.js
@@ -0,0 +1,101 @@
+import i18n from '@dhis2/d2-i18n'
+import { Layer, Popper, IconFilter16 } from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React, { useMemo, useState, useRef } from 'react'
+import { useSelector } from 'react-redux'
+import { sGetNamedItemFilters } from '../../reducers/itemFilters.js'
+import styles from './styles/SlideshowFiltersInfo.module.css'
+
+const popperModifiers = [
+ {
+ name: 'offset',
+ options: {
+ offset: [0, 8],
+ },
+ },
+]
+const FilterSection = ({ name, values }) => (
+
+)
+
+FilterSection.propTypes = {
+ name: PropTypes.string,
+ values: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string })),
+}
+
+export const SlideshowFiltersInfo = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const ref = useRef(null)
+ const filters = useSelector(sGetNamedItemFilters)
+ const totalFilterCount = useMemo(
+ () =>
+ filters.reduce((total, filter) => total + filter.values.length, 0),
+ [filters]
+ )
+
+ if (filters.length === 0) {
+ return null
+ }
+
+ let filterMessage = ''
+ let multipleFilters = true
+ if (filters.length === 1 && filters[0].values.length === 1) {
+ multipleFilters = false
+ filterMessage = i18n.t('{{name}}: {{filter}}', {
+ name: filters[0].name,
+ filter: filters[0].values[0].name,
+ nsSeparator: '>',
+ })
+ }
+
+ return (
+ <>
+ {!multipleFilters ? (
+
+ )}
+ >
+ )
+}
diff --git a/src/pages/view/ViewDashboard.js b/src/pages/view/ViewDashboard.js
index 23aece7c8..a053c3aab 100644
--- a/src/pages/view/ViewDashboard.js
+++ b/src/pages/view/ViewDashboard.js
@@ -137,6 +137,7 @@ const ViewDashboard = ({