Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Libpitt/1160 banner update #1162

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
25 changes: 18 additions & 7 deletions src/components/SenNetBanner.jsx
Original file line number Diff line number Diff line change
@@ -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 && <div className={`c-SenNetBanner ${banner.wrapperClassName || ''}`} role='section' aria-label={banner.ariaLabel}>
{banner && !dismissed && <div className={`c-SenNetBanner ${banner.sectionClassName || ''}`} role='section' aria-label={banner.ariaLabel}>
{banner.beforeBanner && <div className={banner.beforeBannerClassName} dangerouslySetInnerHTML={{__html: banner.beforeBanner}}></div>}
<div className={banner.innerWrapperClassName}>
<div className={banner.outerWrapperClassName}>
<Alert variant={banner.theme || 'info'} show={showBanner} onClose={handleCloseBanner} dismissible={banner.dismissible} className={banner.className}>
{banner.title && <Alert.Heading><span dangerouslySetInnerHTML={{__html: banner.title}}></span></Alert.Heading>}
<div dangerouslySetInnerHTML={{__html: banner.content}}></div>
<div className={banner.innerClassName}>
{banner.title && <Alert.Heading><span dangerouslySetInnerHTML={{__html: banner.title}}></span></Alert.Heading>}
<div dangerouslySetInnerHTML={{__html: banner.content}}></div>
</div>
</Alert>
</div>
{banner.afterBanner && <div className={banner.afterBannerClassName} dangerouslySetInnerHTML={{__html: banner.afterBanner}}></div>}
Expand Down
4 changes: 3 additions & 1 deletion src/components/custom/js/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
6 changes: 3 additions & 3 deletions src/components/custom/search/ColumnsDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [] }) {
Expand Down Expand Up @@ -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()
})

Expand Down
4 changes: 3 additions & 1 deletion src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,6 @@ export function FilterIsSelected(fieldName, value) {
};
}

export const COLS_ORDER_KEY = (context) => `${context}.columnsOrder`
export const STORAGE_KEY = (key = '') => `sn-portal.${key}`

export const COLS_ORDER_KEY = (context = '') => `${context}.columnsOrder`
4 changes: 2 additions & 2 deletions src/context/AppContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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'
Expand Down
11 changes: 0 additions & 11 deletions src/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<h2>Banner Title</h2>", "content": "<a href='#'>Hello world</a>", "className": "mt-4"}'
NEXT_PUBLIC_BANNER_SEARCH_ENTITIES = '{"content": "Search it", "className": "alert-hlf mb-4"}'
5 changes: 3 additions & 2 deletions src/lib/auth.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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"})
deleteCookie('groups_token')
deleteCookie('info', {path: '/', domain: getCookieDomain(), sameSite: "Lax"})
deleteCookie('user')
deleteCookie('adminUIAuthorized')
localStorage.removeItem('userPage')
deleteFromLocalStorage(STORAGE_KEY())
Sui.clearFilters()
}