diff --git a/CHANGELOG.md b/CHANGELOG.md index 914f338f..c0fa283f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [#274](https://github.com/os2display/display-admin-client/pull/274) + - Added screen status to screen list. + - Refactored screen status on screen edit. + - Change campaign icon in screen list to boolean text. - [#273](https://github.com/os2display/display-admin-client/pull/273) - Fixed calendar api feed source config endpoint. - [#272](https://github.com/os2display/display-admin-client/pull/272) diff --git a/infrastructure/itkdev/etc/confd/templates/config.tmpl b/infrastructure/itkdev/etc/confd/templates/config.tmpl index 90ce5b67..e5818c56 100644 --- a/infrastructure/itkdev/etc/confd/templates/config.tmpl +++ b/infrastructure/itkdev/etc/confd/templates/config.tmpl @@ -1,6 +1,7 @@ { "api": "{{ getenv "API_PATH" "/" }}", "touchButtonRegions": "{{ getenv "APP_TOUCH_BUTTON_REGIONS" "false"}}", + "showScreenStatus": "{{ getenv "APP_SHOW_SCREEN_STATUS" "true"}}", "rejseplanenApiKey": "{{ getenv "APP_REJSEPLANEN_API_KEY" "null"}}", "loginMethods": [ { diff --git a/infrastructure/os2display/etc/confd/templates/config.tmpl b/infrastructure/os2display/etc/confd/templates/config.tmpl index 90ce5b67..86721256 100644 --- a/infrastructure/os2display/etc/confd/templates/config.tmpl +++ b/infrastructure/os2display/etc/confd/templates/config.tmpl @@ -1,6 +1,7 @@ { "api": "{{ getenv "API_PATH" "/" }}", "touchButtonRegions": "{{ getenv "APP_TOUCH_BUTTON_REGIONS" "false"}}", + "showScreenStatus": "{{ getenv "APP_SHOW_SCREEN_STATUS" "false"}}", "rejseplanenApiKey": "{{ getenv "APP_REJSEPLANEN_API_KEY" "null"}}", "loginMethods": [ { diff --git a/public/example_config.json b/public/example_config.json index 3ecbf9a6..ed053e47 100644 --- a/public/example_config.json +++ b/public/example_config.json @@ -1,6 +1,7 @@ { "api": "/", "touchButtonRegions": false, + "showScreenStatus": false, "rejseplanenApiKey": null, "loginMethods": [ { diff --git a/src/app.jsx b/src/app.jsx index 6f1d9c1a..22c453bc 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -64,6 +64,8 @@ function App() { const [page, setPage] = useState(1); const [createdBy, setCreatedBy] = useState("all"); const [isPublished, setIsPublished] = useState("all"); + const [exists, setExists] = useState(null); + const [screenUserLatestRequest, setScreenUserLatestRequest] = useState(null); const userStore = { authenticated: { get: authenticated, set: setAuthenticated }, @@ -80,6 +82,11 @@ function App() { page: { get: page, set: setPage }, createdBy: { get: createdBy, set: setCreatedBy }, isPublished: { get: isPublished, set: setIsPublished }, + exists: { get: exists, set: setExists }, + screenUserLatestRequest: { + get: screenUserLatestRequest, + set: setScreenUserLatestRequest, + }, }; useEffect(() => { diff --git a/src/components/screen/screen-form.jsx b/src/components/screen/screen-form.jsx index 0c702c3b..6d5c2c62 100644 --- a/src/components/screen/screen-form.jsx +++ b/src/components/screen/screen-form.jsx @@ -1,9 +1,8 @@ import { React, useEffect, useState } from "react"; -import { Button, Form, Spinner, Alert } from "react-bootstrap"; +import { Button, Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; -import { useDispatch } from "react-redux"; import ContentBody from "../util/content-body/content-body"; import ContentFooter from "../util/content-footer/content-footer"; import FormInput from "../util/forms/form-input"; @@ -13,13 +12,13 @@ import GridGenerationAndSelect from "./util/grid-generation-and-select"; import MultiSelectComponent from "../util/forms/multiselect-dropdown/multi-dropdown"; import idFromUrl from "../util/helpers/id-from-url"; import { - api, useGetV2LayoutsQuery, useGetV2ScreensByIdScreenGroupsQuery, } from "../../redux/api/api.generated.ts"; -import { displayError } from "../util/list/toast-component/display-toast"; import FormCheckbox from "../util/forms/form-checkbox"; import "./screen-form.scss"; +import ScreenStatus from "./screen-status"; +import { displayError } from "../util/list/toast-component/display-toast"; /** * The screen form component. @@ -49,11 +48,10 @@ function ScreenForm({ }) { const { t } = useTranslation("common", { keyPrefix: "screen-form" }); const navigate = useNavigate(); - const dispatch = useDispatch(); + const [layoutError, setLayoutError] = useState(false); const [selectedLayout, setSelectedLayout] = useState(); const [layoutOptions, setLayoutOptions] = useState(); - const [bindKey, setBindKey] = useState(""); const { data: layouts } = useGetV2LayoutsQuery({ page: 1, itemsPerPage: 20, @@ -112,57 +110,6 @@ function ScreenForm({ }); }; - const handleBindScreen = () => { - if (bindKey) { - dispatch( - api.endpoints.postScreenBindKey.initiate({ - id: idFromUrl(screen["@id"]), - screenBindObject: JSON.stringify({ - bindKey, - }), - }) - ).then((response) => { - if (response.error) { - const err = response.error; - displayError( - t("error-messages.error-binding", { - status: err.status, - }), - err - ); - } else { - // Set screenUser to true, to indicate it has been set. - handleInput({ target: { id: "screenUser", value: true } }); - } - }); - } - }; - - const handleUnbindScreen = () => { - if (screen?.screenUser) { - setBindKey(""); - - dispatch( - api.endpoints.postScreenUnbind.initiate({ - id: idFromUrl(screen["@id"]), - }) - ).then((response) => { - if (response.error) { - const err = response.error; - displayError( - t("error-messages.error-unbinding", { - status: err.status, - }), - err - ); - } else { - // Set screenUser to null, to indicate it has been removed. - handleInput({ target: { id: "screenUser", value: null } }); - } - }); - } - }; - const isVertical = () => { if (screen?.orientation?.length > 0) { return screen.orientation[0].id === "vertical"; @@ -203,35 +150,7 @@ function ScreenForm({ {Object.prototype.hasOwnProperty.call(screen, "@id") && (

{t("bind-header")}

- {screen?.screenUser && ( - <> -
- - {t("already-bound")} - -
- - - )} - {!screen?.screenUser && ( - <> -
- - {t("not-bound")} - -
- { - setBindKey(target?.value); - }} - name="bindKey" - value={bindKey} - label={t("bindkey-label")} - className="mb-3" - /> - - - )} +
)} @@ -371,6 +290,7 @@ ScreenForm.propTypes = { screenUser: PropTypes.string, size: PropTypes.string, title: PropTypes.string, + status: PropTypes.shape({}), playlists: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string, id: PropTypes.number }) ), diff --git a/src/components/screen/screen-list.jsx b/src/components/screen/screen-list.jsx index 3086f411..a9de56d8 100644 --- a/src/components/screen/screen-list.jsx +++ b/src/components/screen/screen-list.jsx @@ -18,6 +18,7 @@ import { displayError, } from "../util/list/toast-component/display-toast"; import "./screen-list.scss"; +import ConfigLoader from "../../config-loader"; /** * The screen list component. @@ -31,6 +32,8 @@ function ScreenList() { searchText: { get: searchText }, page: { get: page }, createdBy: { get: createdBy }, + exists: { get: exists }, + screenUserLatestRequest: { get: screenUserLatestRequest }, } = useContext(ListContext); const { selected, setSelected } = useModal(); @@ -40,6 +43,7 @@ function ScreenList() { const [loadingMessage, setLoadingMessage] = useState( t("loading-messages.loading-screens") ); + const [showScreenStatus, setShowScreenStatus] = useState(false); // Delete call const [ @@ -51,6 +55,7 @@ function ScreenList() { const { data, error: screensGetError, + isFetching, isLoading, refetch, } = useGetV2ScreensQuery({ @@ -58,18 +63,22 @@ function ScreenList() { order: { title: "asc" }, search: searchText, createdBy, + exists, + "screenUser.latestRequest": screenUserLatestRequest, }); + useEffect(() => { + ConfigLoader.loadConfig().then((config) => { + setShowScreenStatus(config.showScreenStatus); + }); + }, []); + useEffect(() => { if (data) { setListData(data); } }, [data]); - useEffect(() => { - refetch(); - }, [searchText, page, createdBy]); - // If the tenant is changed, data should be refetched useEffect(() => { if (context.selectedTenant.get) { @@ -110,21 +119,22 @@ function ScreenList() { setLoadingMessage(t("loading-messages.deleting-screen")); }; + // Error with retrieving list of screen + useEffect(() => { + if (screensGetError) { + displayError(t("error-messages.screens-load-error"), screensGetError); + } + }, [screensGetError]); + // The columns for the table. const columns = ScreenColumns({ handleDelete, apiCall: useGetV2ScreensByIdScreenGroupsQuery, infoModalRedirect: "/group/edit", infoModalTitle: t("info-modal.screen-in-groups"), + displayStatus: showScreenStatus, }); - // Error with retrieving list of screen - useEffect(() => { - if (screensGetError) { - displayError(t("error-messages.screens-load-error"), screensGetError); - } - }, [screensGetError]); - return ( <> )} diff --git a/src/components/screen/screen-status.jsx b/src/components/screen/screen-status.jsx new file mode 100644 index 00000000..2b1a2290 --- /dev/null +++ b/src/components/screen/screen-status.jsx @@ -0,0 +1,281 @@ +import dayjs from "dayjs"; +import PropTypes from "prop-types"; +import { React, JSX, useState, useEffect } from "react"; +import { Alert, Button } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCheck, + faCheckCircle, + faExclamationCircle, + faExclamationTriangle, + faInfoCircle, + faPlus, +} from "@fortawesome/free-solid-svg-icons"; +import { useNavigate } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import idFromUrl from "../util/helpers/id-from-url"; +import { api } from "../../redux/api/api.generated.ts"; +import { displayError } from "../util/list/toast-component/display-toast"; +import FormInput from "../util/forms/form-input"; +import ConfigLoader from "../../config-loader"; + +/** + * Displays screen status. + * + * @param {object} props The props. + * @param {object} props.screen The screen. + * @param {string | null} props.mode The display mode: 'default' or 'minimal' + * @param {Function} props.handleInput Handler for change in input. + * @returns {JSX.Element} The status element. + */ +function ScreenStatus({ screen, handleInput = () => {}, mode = "default" }) { + const { t } = useTranslation("common", { keyPrefix: "screen-status" }); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [clientRelease, setClientRelease] = useState(null); + const [bindKey, setBindKey] = useState(""); + const [showScreenStatus, setShowScreenStatus] = useState(false); + + const { status } = screen; + + const handleBindScreen = () => { + if (bindKey) { + dispatch( + api.endpoints.postScreenBindKey.initiate({ + id: idFromUrl(screen["@id"]), + screenBindObject: JSON.stringify({ + bindKey, + }), + }) + ).then((response) => { + if (response.error) { + const err = response.error; + displayError( + t("error-messages.error-binding", { + status: err.status, + }), + err + ); + } else { + // Set screenUser to true, to indicate it has been set. + handleInput({ target: { id: "screenUser", value: true } }); + } + }); + } + }; + + const handleUnbindScreen = () => { + if (screen?.screenUser) { + setBindKey(""); + + dispatch( + api.endpoints.postScreenUnbind.initiate({ + id: idFromUrl(screen["@id"]), + }) + ).then((response) => { + if (response.error) { + const err = response.error; + displayError( + t("error-messages.error-unbinding", { + status: err.status, + }), + err + ); + } else { + // Set screenUser and status to null, to indicate it has been removed. + handleInput({ target: { id: "screenUser", value: null } }); + } + }); + } + }; + + useEffect(() => { + if (status) { + const now = dayjs().startOf("minute").valueOf(); + + if (status?.clientMeta?.host) { + fetch(`${status.clientMeta.host}/release.json?ts=${now}`) + .then((res) => res.json()) + .then((data) => setClientRelease(data)); + } + } + }, [status]); + + useEffect(() => { + ConfigLoader.loadConfig().then((config) => { + setShowScreenStatus(config.showScreenStatus); + }); + }, []); + + if (mode === "minimal") { + if (!status) { + return ( + + ); + } + + const latestRequest = dayjs(status.latestRequestDateTime); + const inOneHour = dayjs().add(1, "hours"); + + if (status?.clientMeta?.tokenExpired || latestRequest > inOneHour) { + return ( + + ); + } + + if (clientRelease) { + if (status?.releaseVersion !== clientRelease?.releaseVersion) { + return ; + } + + if (status?.releaseTimestamp !== clientRelease?.releaseTimestamp) { + return ; + } + } + + return ( +
+ +
+ ); + } + + const getStatusAlert = () => { + let message = t("already-bound"); + let variant = "success"; + let icon = ; + const screenBound = !!screen.screenUser; + const notRunningLatestRelease = + status && + (clientRelease?.releaseTimestamp !== status?.releaseTimestamp || + clientRelease?.releaseVersion !== status?.releaseVersion); + + if (!screenBound) { + message = t("not-bound"); + variant = "danger"; + } else if (status?.clientMeta?.tokenExpired) { + message = t("token-expired"); + variant = "danger"; + } else if ( + status && + dayjs(status?.latestRequestDateTime) < dayjs().subtract(1, "hour") + ) { + message = t("latest-request-warning"); + variant = "danger"; + icon = ; + } else if (notRunningLatestRelease) { + message = t("release-warning"); + variant = "warning"; + icon = ; + } + + return ( + +
+ {icon} + {" "} + {message} +
+ + {!screenBound && ( + <> + { + setBindKey(target?.value); + }} + name="bindKey" + value={bindKey} + label={t("bindkey-label")} + className="mb-3" + /> + + + )} + + {screenBound && ( + <> + {showScreenStatus && ( +
    + {status?.latestRequestDateTime && ( +
  • + {t("latest-request")}:{" "} + {dayjs(status?.latestRequestDateTime).format( + "D/M YYYY HH:mm" + )} +
  • + )} + {status?.releaseVersion && ( +
  • + {t("release-version")}: {status?.releaseVersion} + {notRunningLatestRelease && ( + <> + {" "} + ({t("newest")}: {clientRelease?.releaseVersion}) + + )} +
  • + )} + {status?.releaseTimestamp && ( +
  • + {t("release-timestamp")}:{" "} + {dayjs(status?.releaseTimestamp * 1000).format( + "D/M YYYY HH:mm" + )} + {notRunningLatestRelease && ( + <> + {" "}({t("newest")}:{" "} + {clientRelease?.releaseTimestamp && + dayjs(clientRelease?.releaseTimestamp * 1000).format( + "D/M YYYY HH:mm" + )} + ) + + )} +
  • + )} +
+ )} + + + + )} +
+ ); + }; + + return <>{getStatusAlert()}; +} + +ScreenStatus.propTypes = { + screen: PropTypes.shape({ + "@id": PropTypes.string.isRequired, + screenUser: PropTypes.string, + status: PropTypes.shape({ + releaseVersion: PropTypes.string, + releaseTimestamp: PropTypes.number, + latestRequestDateTime: PropTypes.string, + clientMeta: PropTypes.shape({ + ip: PropTypes.string, + host: PropTypes.string, + userAgent: PropTypes.string, + tokenExpired: PropTypes.bool, + }), + }), + }).isRequired, + mode: PropTypes.string, + handleInput: PropTypes.func, +}; + +export default ScreenStatus; diff --git a/src/components/screen-list/campaign-icon.jsx b/src/components/screen/util/campaign-icon.jsx similarity index 57% rename from src/components/screen-list/campaign-icon.jsx rename to src/components/screen/util/campaign-icon.jsx index 4ea8a018..145bef7a 100644 --- a/src/components/screen-list/campaign-icon.jsx +++ b/src/components/screen/util/campaign-icon.jsx @@ -1,30 +1,38 @@ import { React, useEffect, useState } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; -import idFromUrl from "../util/helpers/id-from-url"; -import calculateIsPublished from "../util/helpers/calculate-is-published"; +import { useTranslation } from "react-i18next"; +import Spinner from "react-bootstrap/Spinner"; +import idFromUrl from "../../util/helpers/id-from-url"; +import calculateIsPublished from "../../util/helpers/calculate-is-published"; import { api, useGetV2ScreensByIdCampaignsQuery, useGetV2ScreensByIdScreenGroupsQuery, -} from "../../redux/api/api.generated.ts"; +} from "../../../redux/api/api.generated.ts"; /** * An icon to show if the screen has an active campaign. * * @param {object} props - The props. * @param {string} props.id The id of the screen. + * @param {number} props.delay Delay the fetch. * @returns {object} The campaign icon. */ -function CampaignIcon({ id }) { +function CampaignIcon({ id, delay }) { + const { t } = useTranslation("common", { keyPrefix: "campaign-icon" }); const dispatch = useDispatch(); - const [isOverriddenByCampaign, setIsOverriddenByCampaign] = useState(false); + const [isOverriddenByCampaign, setIsOverriddenByCampaign] = useState(null); const [screenCampaignsChecked, setScreenCampaignsChecked] = useState(false); const [allCampaigns, setAllCampaigns] = useState([]); - const { data: campaigns } = useGetV2ScreensByIdCampaignsQuery({ id }); - const { data: groups } = useGetV2ScreensByIdScreenGroupsQuery({ id }); + const [getData, setGetData] = useState(false); + + const { data: campaigns, isLoading } = useGetV2ScreensByIdCampaignsQuery( + { id }, + { skip: !getData } + ); + const { data: groups, isLoading: isLoadingScreenGroups } = + useGetV2ScreensByIdScreenGroupsQuery({ id }, { skip: !getData }); useEffect(() => { if (campaigns) { @@ -65,16 +73,43 @@ function CampaignIcon({ id }) { } }, [allCampaigns]); - return ( - - ); + useEffect(() => { + const timeout = setTimeout(() => { + setGetData(true); + }, delay); + + return () => { + clearTimeout(timeout); + }; + }, []); + + if (!getData || isLoading || isLoadingScreenGroups) { + return ( +
+
+ ); + } + + return isOverriddenByCampaign + ? t("overridden-by-campaign") + : t("not-overridden-by-campaign"); } +CampaignIcon.defaultProps = { + delay: 1000, +}; + CampaignIcon.propTypes = { id: PropTypes.string.isRequired, + delay: PropTypes.number, }; export default CampaignIcon; diff --git a/src/components/screen/util/screen-columns.jsx b/src/components/screen/util/screen-columns.jsx index 03d4612c..74d5e6c7 100644 --- a/src/components/screen/util/screen-columns.jsx +++ b/src/components/screen/util/screen-columns.jsx @@ -1,10 +1,11 @@ import { React } from "react"; import { useTranslation } from "react-i18next"; import ListButton from "../../util/list/list-button"; -import CampaignIcon from "../../screen-list/campaign-icon"; +import CampaignIcon from "./campaign-icon"; import SelectColumnHoc from "../../util/select-column-hoc"; import ColumnHoc from "../../util/column-hoc"; import idFromUrl from "../../util/helpers/id-from-url"; +import ScreenStatus from "../screen-status"; /** * Columns for screens lists. @@ -14,6 +15,7 @@ import idFromUrl from "../../util/helpers/id-from-url"; * @param {string} props.infoModalRedirect - The url for redirecting in the info modal. * @param {string} props.infoModalTitle - The info modal title. * @param {string} props.dataKey The data key for mapping the data. + * @param {boolean} props.displayStatus Should status be displayed? * @returns {object} The columns for the screens lists. */ function getScreenColumns({ @@ -21,19 +23,20 @@ function getScreenColumns({ infoModalRedirect, infoModalTitle, dataKey, + displayStatus, }) { const { t } = useTranslation("common", { keyPrefix: "screen-list" }); const columns = [ { - // eslint-disable-next-line react/prop-types - content: ({ inScreenGroups }) => ( + content: (screen) => ( ), key: "groups", @@ -47,10 +50,20 @@ function getScreenColumns({ key: "campaign", label: t("columns.campaign"), // eslint-disable-next-line react/destructuring-assignment - content: (d) => , + content: (d) => , }, ]; + if (displayStatus) { + columns.push({ + path: "status", + label: t("columns.status"), + content: (screen) => { + return ; + }, + }); + } + return columns; } diff --git a/src/components/util/forms/select.jsx b/src/components/util/forms/select.jsx index 4998fd25..5c6f5106 100644 --- a/src/components/util/forms/select.jsx +++ b/src/components/util/forms/select.jsx @@ -9,7 +9,7 @@ import { Tooltip } from "react-tooltip"; /** * @param {object} props The props. * @param {string} props.name The name of the select component. - * @param {string} props.label The label of the select component. + * @param {string | null} props.label The label of the select component. * @param {string} props.value The selected value. * @param {Array} props.options The options for the select component. * @param {Function} props.onChange The callback for when something is selected. @@ -25,7 +25,7 @@ import { Tooltip } from "react-tooltip"; */ function Select({ name, - label, + label = null, options, onChange, errors = null, @@ -65,10 +65,12 @@ function Select({ /* eslint-disable jsx-a11y/anchor-is-valid */ return ( - + {label && ( + + )} {tooltip !== null && ( <> @@ -118,7 +120,7 @@ Select.propTypes = { disabled: PropTypes.bool, tooltip: PropTypes.string, errors: PropTypes.arrayOf(PropTypes.string), - label: PropTypes.string.isRequired, + label: PropTypes.string, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/src/components/util/list/list-button.jsx b/src/components/util/list/list-button.jsx index 7c77d201..3b6bd8fa 100644 --- a/src/components/util/list/list-button.jsx +++ b/src/components/util/list/list-button.jsx @@ -11,24 +11,32 @@ import useModal from "../../../context/modal-context/modal-context-hook"; * @param {string} props.redirectTo - The url for redirecting in the info modal. * @param {string} props.modalTitle - The info modal title. * @param {string} props.dataKey The data key for mapping the data. + * @param {number} props.delayApiCall Delay the calling of the api. * @returns {object} - The list button. */ function ListButton({ - apiCall = () => {}, - dataKey = "", redirectTo, displayData, modalTitle, + delayApiCall = 0, + apiCall = () => {}, + dataKey = "", }) { const { setModal } = useModal(); const [label, setLabel] = useState(""); + const [getData, setGetData] = useState(false); + let data; + if (!Array.isArray(displayData)) { - data = apiCall({ - id: idFromUrl(displayData), - page: 1, - itemsPerPage: 0, - }); + data = apiCall( + { + id: idFromUrl(displayData), + page: 1, + itemsPerPage: 0, + }, + { skip: !getData } + ); } useEffect(() => { @@ -50,6 +58,16 @@ function ListButton({ }; }, [data]); + useEffect(() => { + const timeout = setTimeout(() => { + setGetData(true); + }, delayApiCall); + + return () => { + clearTimeout(timeout); + }; + }, []); + return ( <> {label && ( @@ -96,6 +114,7 @@ ListButton.propTypes = { dataKey: PropTypes.string, modalTitle: PropTypes.string.isRequired, redirectTo: PropTypes.string.isRequired, + delayApiCall: PropTypes.number, }; export default ListButton; diff --git a/src/components/util/list/list.jsx b/src/components/util/list/list.jsx index 968dd9aa..78808d03 100644 --- a/src/components/util/list/list.jsx +++ b/src/components/util/list/list.jsx @@ -3,6 +3,7 @@ import { Button, Col, Row } from "react-bootstrap"; import { useNavigate, useLocation } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; +import dayjs from "dayjs"; import Table from "../table/table"; import UserContext from "../../../context/user-context"; import SearchBox from "../search-box/search-box"; @@ -13,6 +14,7 @@ import ListLoading from "../loading-component/list-loading"; import localStorageKeys from "../local-storage-keys"; import FormCheckbox from "../forms/form-checkbox"; import ListContext from "../../../context/list-context"; +import Select from "../forms/select"; /** * @param {object} props - The props. @@ -22,15 +24,19 @@ import ListContext from "../../../context/list-context"; * @param {Function} props.handleDelete - For deleting elements in the list. * element with success. * @param {boolean} props.displayPublished - Whether to display the published filter - * @param {Function} props.showCreatedByFilter - Callback for created by filter. + * @param {boolean} props.showCreatedByFilter - Callback for created by filter. * @param {boolean} props.displaySearch - Should search be displayed. + * @param {boolean} props.enableScreenStatus - Should screen status be displayed? + * @param {boolean} props.isFetching - Is fetching. * @returns {object} The List. */ function List({ data, columns, totalItems, + enableScreenStatus, showCreatedByFilter = true, + isFetching = false, handleDelete = null, displayPublished = false, displaySearch = true, @@ -42,6 +48,8 @@ function List({ page: { set: setPage }, createdBy: { set: setCreatedBy }, isPublished: { set: setIsPublished }, + exists: { set: setExists }, + screenUserLatestRequest: { set: setScreenUserLatestRequest }, } = useContext(ListContext); const { email: { get: email }, @@ -52,16 +60,15 @@ function List({ const { search } = useLocation(); const searchParams = new URLSearchParams(search).get("search"); const pageParams = new URLSearchParams(search).get("page"); - - let createdByParams; - if (showCreatedByFilter) { - createdByParams = new URLSearchParams(search).get("createdBy"); - } - - let publishedParams; - if (displayPublished) { - publishedParams = new URLSearchParams(search).get("published"); - } + const createdByParams = showCreatedByFilter + ? new URLSearchParams(search).get("createdBy") + : undefined; + const publishedParams = displayPublished + ? new URLSearchParams(search).get("published") + : undefined; + const screenStatusParam = enableScreenStatus + ? new URLSearchParams(search).get("screenStatus") + : undefined; // At least one row must be selected for deletion. const disableDeleteButton = !selected.length > 0; @@ -78,6 +85,11 @@ function List({ params.append("published", published); } + if (enableScreenStatus) { + params.delete("screenStatus"); + params.append("screenStatus", screenStatusParam); + } + // page const page = pageParams || 1; params.delete("page"); @@ -109,12 +121,16 @@ function List({ /** * @param {string} dataKey - Which data to delete/update - * @param {object} value - The update value + * @param {string | null} value - The update value */ const updateUrlParams = (dataKey, value) => { const params = new URLSearchParams(search); params.delete(dataKey); - params.append(dataKey, value); + + if (value !== null) { + params.append(dataKey, value); + } + navigate({ search: params.toString(), }); @@ -128,6 +144,7 @@ function List({ params.append("search", newSearchText); params.delete("page"); params.append("page", 1); + navigate({ search: params.toString(), }); @@ -161,6 +178,10 @@ function List({ handleDelete(); }; + const onScreenStatus = ({ target }) => { + updateUrlParams("screenStatus", target.value); + }; + /** Sets page from url using callback */ useEffect(() => { if (pageParams) { @@ -199,6 +220,30 @@ function List({ } }, [publishedParams]); + useEffect(() => { + if (screenStatusParam) { + const anHourAgo = dayjs().startOf("hour").subtract(1, "hours"); + + switch (screenStatusParam) { + case "active": + setExists({ screenUser: true }); + setScreenUserLatestRequest({ after: anHourAgo.valueOf() }); + break; + case "inactive": + setExists({ screenUser: true }); + setScreenUserLatestRequest({ before: anHourAgo.valueOf() }); + break; + case "not-connected": + setExists({ screenUser: false }); + setScreenUserLatestRequest(null); + break; + default: + setExists(null); + setScreenUserLatestRequest(null); + } + } + }, [screenStatusParam]); + return ( <> @@ -208,6 +253,38 @@ function List({ )} <> + {enableScreenStatus && ( + +