diff --git a/README.md b/README.md index 56e85147..de1c916d 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,29 @@ The react component is `src/components/custom/edit/sample/rui/RUIIntegration.js` The tool is only available for human `Block` tissue samples with an ancestor of `Organ` The `Register location` button will display on the edit sample page and launch the CCF-RUI tool. + +## Content Management +### Banner +Currently, two locations offer adding a banner via the `.env` file without having to rebuild the image. These are: +``` +NEXT_PUBLIC_BANNER_LOGIN # This is located before the Login section +NEXT_PUBLIC_BANNER_SEARCH_ENTITIES # Located right before the main search results area +``` +These environment variables take a json object with the following properties: + +| Property | Type | Description | +|---------------------------|---------------|---------------------------------------------------------------------------------------------------------------------| +| **theme** | *enum string* | `['info', 'danger', 'warning']` | +| **title** | *html string* | A title for the `Alert`, which is the actual banner. (Going forward we will call this just 'banner'.) | +| **content** | *html string* | The main banner content. | +| **dismissible** | *boolean* | Add a close button to the banner. | +| **keepDismissed** | *boolean* | Keep the banner dismissed on close. The banner will show again on refresh if this is set to `false` or `undefined`. | +| **className** | *string* | A class name for the banner. | +| **innerClassName** | *string* | A class name for inner wrapper of the banner. | +| **outerWrapperClassName** | *string* | A class name for the div that wraps the banner. | +| **beforeBanner** | *html string* | Set some content before the banner. | +| **beforeBannerClassName** | *string* | Set a class name on div of `beforeBanner`. | +| **afterBanner** | *html string* | Set some content after the banner. | +| **afterBannerClassName** | *string* | Set a class name on div of `afterBanner`. | +| **sectionClassName** | *string* | A class name for the `c-SenNetBanner` section. | +| **ariaLabel** | *string* | For accessibility, add a unique label to the `c-SenNetBanner` section | \ No newline at end of file diff --git a/src/components/SenNetBanner.jsx b/src/components/SenNetBanner.jsx index a2cddbd5..d2a89864 100644 --- a/src/components/SenNetBanner.jsx +++ b/src/components/SenNetBanner.jsx @@ -1,30 +1,41 @@ import React, {useContext, useEffect, useState} from 'react' import PropTypes from 'prop-types' -import {getBanner} from "../config/config" +import {getBanner, STORAGE_KEY} from "../config/config" import {Alert} from 'react-bootstrap' function SenNetBanner({name}) { const [banner, setBanner] = useState(null) const [showBanner, setShowBanner] = useState(true) + const [dismissed, setDismissed] = useState(false) + const STORE_KEY = STORAGE_KEY(`banner.${name}.dismissed`) const handleCloseBanner = () => { - if (banner.dismissible) { + if (banner?.dismissible) { setShowBanner(false) + if (banner.keepDismissed) { + localStorage.setItem(STORE_KEY, true) + } } } useEffect(() => { - setBanner(getBanner(name)) + const _banner = getBanner(name) + setBanner(_banner) + if (_banner?.keepDismissed && localStorage.getItem(STORE_KEY)) { + setDismissed(true) + } }, []) return ( <> - {banner &&
+ {banner && !dismissed &&
{banner.beforeBanner &&
} -
+
- {banner.title && } -
+
+ {banner.title && } +
+
{banner.afterBanner &&
} diff --git a/src/components/custom/js/functions.js b/src/components/custom/js/functions.js index 29ec3316..bda90fef 100644 --- a/src/components/custom/js/functions.js +++ b/src/components/custom/js/functions.js @@ -434,10 +434,12 @@ export const matchArrayOrder = (ordering, data, key1 = 'name', key2 = 'id') => { } -export const deleteFromLocalStorage = (needle, fn = 'endsWith') => { +export const deleteFromLocalStorage = (needle, fn = 'startsWith') => { Object.keys(localStorage) .filter(x => x[fn](needle)) .forEach(x => localStorage.removeItem(x)) } + +export const deleteFromLocalStorageWithSuffix = (needle) => deleteFromLocalStorage(needle, 'endsWith') diff --git a/src/components/custom/search/ColumnsDropdown.jsx b/src/components/custom/search/ColumnsDropdown.jsx index 5aaff116..2cafaf70 100644 --- a/src/components/custom/search/ColumnsDropdown.jsx +++ b/src/components/custom/search/ColumnsDropdown.jsx @@ -4,7 +4,7 @@ import Select from 'react-select' import $ from 'jquery' import {parseJson} from "../../../lib/services"; import {COLS_ORDER_KEY} from "../../../config/config"; -import {deleteFromLocalStorage} from "../js/functions"; +import {deleteFromLocalStorageWithSuffix} from "../js/functions"; function ColumnsDropdown({ getTableColumns, setHiddenColumns, currentColumns, filters, searchContext, defaultHiddenColumns = [] }) { @@ -83,8 +83,8 @@ function ColumnsDropdown({ getTableColumns, setHiddenColumns, currentColumns, fi // Have to listen to click from here instead of in handleClearFiltersClick // to manage value states of this independent component $('body').on('click', clearBtnSelector, () => { - deleteFromLocalStorage(STORE_KEY) - deleteFromLocalStorage(COLS_ORDER_KEY('')) + deleteFromLocalStorageWithSuffix(STORE_KEY) + deleteFromLocalStorageWithSuffix(COLS_ORDER_KEY()) handleDefaultHidden() }) diff --git a/src/config/config.js b/src/config/config.js index 60843d48..347a3e3f 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -189,4 +189,6 @@ export function FilterIsSelected(fieldName, value) { }; } -export const COLS_ORDER_KEY = (context) => `${context}.columnsOrder` \ No newline at end of file +export const STORAGE_KEY = (key = '') => `sn-portal.${key}` + +export const COLS_ORDER_KEY = (context = '') => `${context}.columnsOrder` \ No newline at end of file diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 1a24f14b..641e9cad 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -11,7 +11,7 @@ import { } from '../lib/services' import {deleteCookies} from "../lib/auth"; import {APP_ROUTES} from "../config/constants"; -import {getUIPassword} from "../config/config"; +import {getUIPassword, STORAGE_KEY} from "../config/config"; import Swal from 'sweetalert2' import AppModal from "../components/AppModal"; import Spinner from "../components/custom/Spinner"; @@ -33,7 +33,7 @@ export const AppProvider = ({ cache, children }) => { const [userWriteGroups, setUserWriteGroups] = useState([]) const router = useRouter() const authKey = 'isAuthenticated' - const pageKey = 'userPage' + const pageKey = STORAGE_KEY('userPage') useEffect(() => { // Should only include: '/', '/search', '/logout', '/login', '/404' diff --git a/src/example.env b/src/example.env index 1773fc73..cfb8438e 100644 --- a/src/example.env +++ b/src/example.env @@ -29,16 +29,5 @@ NEXT_PUBLIC_PROTOCOLS_TOKEN= NEXT_PUBLIC_COOKIE_DOMAIN=.sennetconsortium.org # Apply a banner to a page: -# dismissible (bool str) Add a close button to the banner -# theme (enum str) [info, danger, warning] -# title (html str) -# content (html str) -# className (str) a class name for the Alert / the actual banner -# innerWrapperClassName (str) a classname for the div that wraps the Alert -# beforeBanner (html str) set some content before the banner -# beforeBannerClassName (str) set a class name on wrapper of beforeBanner -# afterBanner (html str) set some content after the banner -# afterBannerClassName (str) set a class name on wrapper of afterBanner -# wrapperClassName (str) a class name for the entire div wrapping the SenNetBanner section NEXT_PUBLIC_BANNER_LOGIN = '{"theme": "danger", "title": "

Banner Title

", "content": "Hello world", "className": "mt-4"}' NEXT_PUBLIC_BANNER_SEARCH_ENTITIES = '{"content": "Search it", "className": "alert-hlf mb-4"}' diff --git a/src/lib/auth.js b/src/lib/auth.js index 7b634906..a8e7113c 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -1,6 +1,7 @@ import {deleteCookie, setCookie} from "cookies-next"; import {Sui} from "search-ui/lib/search-tools"; -import {getCookieDomain} from "../config/config"; +import {getCookieDomain, STORAGE_KEY} from "../config/config"; +import {deleteFromLocalStorage} from "../components/custom/js/functions"; export function deleteCookies() { setCookie('isAuthenticated', false, {sameSite: "Lax"}) @@ -8,6 +9,6 @@ export function deleteCookies() { deleteCookie('info', {path: '/', domain: getCookieDomain(), sameSite: "Lax"}) deleteCookie('user') deleteCookie('adminUIAuthorized') - localStorage.removeItem('userPage') + deleteFromLocalStorage(STORAGE_KEY()) Sui.clearFilters() }