mirror of https://github.com/artifacthub/hub.git
Display usage tips and promote features available on UI (#1131)
Closes #1108 Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
parent
8c589661ec
commit
402de4ca2c
|
|
@ -7,6 +7,7 @@
|
||||||
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
"@types/bootstrap": "^4.5.0",
|
"@types/bootstrap": "^4.5.0",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
|
"@types/crypto-js": "^4.0.1",
|
||||||
"@types/json-schema": "^7.0.6",
|
"@types/json-schema": "^7.0.6",
|
||||||
"@types/json-schema-merge-allof": "^0.6.0",
|
"@types/json-schema-merge-allof": "^0.6.0",
|
||||||
"@types/lodash": "^4.14.161",
|
"@types/lodash": "^4.14.161",
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.57.0",
|
||||||
"codemirror-rego": "^1.1.0",
|
"codemirror-rego": "^1.1.0",
|
||||||
|
"crypto-js": "^4.0.0",
|
||||||
"json-schema-merge-allof": "^0.7.0",
|
"json-schema-merge-allof": "^0.7.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.27.0",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ type Action =
|
||||||
| { type: 'updateLimit'; limit: number }
|
| { type: 'updateLimit'; limit: number }
|
||||||
| { type: 'updateTheme'; theme: string }
|
| { type: 'updateTheme'; theme: string }
|
||||||
| { type: 'updateEfectiveTheme'; theme: string }
|
| { type: 'updateEfectiveTheme'; theme: string }
|
||||||
| { type: 'enableAutomaticTheme'; enabled: boolean };
|
| { type: 'enableAutomaticTheme'; enabled: boolean }
|
||||||
|
| { type: 'enabledDisplayedNotifications'; enabled: boolean }
|
||||||
|
| { type: 'addNewDisplayedNotification'; id: string };
|
||||||
|
|
||||||
export const AppCtx = createContext<{
|
export const AppCtx = createContext<{
|
||||||
ctx: AppState;
|
ctx: AppState;
|
||||||
|
|
@ -74,6 +76,14 @@ export function enableAutomaticTheme(enabled: boolean) {
|
||||||
return { type: 'enableAutomaticTheme', enabled };
|
return { type: 'enableAutomaticTheme', enabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enabledDisplayedNotifications(enabled: boolean) {
|
||||||
|
return { type: 'enabledDisplayedNotifications', enabled };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addNewDisplayedNotification(id: string) {
|
||||||
|
return { type: 'addNewDisplayedNotification', id };
|
||||||
|
}
|
||||||
|
|
||||||
export async function refreshUserProfile(dispatch: React.Dispatch<any>, redirectUrl?: string) {
|
export async function refreshUserProfile(dispatch: React.Dispatch<any>, redirectUrl?: string) {
|
||||||
try {
|
try {
|
||||||
const profile: Profile = await API.getUserProfile();
|
const profile: Profile = await API.getUserProfile();
|
||||||
|
|
@ -133,10 +143,11 @@ function getCurrentSystemActiveTheme(prefs: ThemePrefs): ThemePrefs {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appReducer(state: AppState, action: Action) {
|
export function appReducer(state: AppState, action: Action) {
|
||||||
|
let prefs;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'signIn':
|
case 'signIn':
|
||||||
const tmpUserPrefs = lsStorage.getPrefs(action.profile.alias);
|
prefs = lsStorage.getPrefs(action.profile.alias);
|
||||||
const userPrefs = { ...tmpUserPrefs, theme: getCurrentSystemActiveTheme(tmpUserPrefs.theme) };
|
const userPrefs = { ...prefs, theme: getCurrentSystemActiveTheme(prefs.theme) };
|
||||||
updateActiveStyleSheet(userPrefs.theme.efective || userPrefs.theme.configured);
|
updateActiveStyleSheet(userPrefs.theme.efective || userPrefs.theme.configured);
|
||||||
lsStorage.setPrefs(userPrefs, action.profile.alias);
|
lsStorage.setPrefs(userPrefs, action.profile.alias);
|
||||||
lsStorage.setActiveProfile(action.profile.alias);
|
lsStorage.setActiveProfile(action.profile.alias);
|
||||||
|
|
@ -146,49 +157,49 @@ export function appReducer(state: AppState, action: Action) {
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'unselectOrg':
|
case 'unselectOrg':
|
||||||
const unselectedOrgPrefs = updateSelectedOrg(state.prefs);
|
prefs = updateSelectedOrg(state.prefs);
|
||||||
lsStorage.setPrefs(unselectedOrgPrefs, state.user!.alias);
|
lsStorage.setPrefs(prefs, state.user!.alias);
|
||||||
redirectToControlPanel('user');
|
redirectToControlPanel('user');
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: unselectedOrgPrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'signOut':
|
case 'signOut':
|
||||||
const tmpGuestPrefs = lsStorage.getPrefs();
|
prefs = lsStorage.getPrefs();
|
||||||
const guestPrefs = { ...tmpGuestPrefs, theme: getCurrentSystemActiveTheme(tmpGuestPrefs.theme) };
|
const guestPrefs = { ...prefs, theme: getCurrentSystemActiveTheme(prefs.theme) };
|
||||||
lsStorage.setPrefs(guestPrefs);
|
lsStorage.setPrefs(guestPrefs);
|
||||||
lsStorage.setActiveProfile();
|
lsStorage.setActiveProfile();
|
||||||
updateActiveStyleSheet(guestPrefs.theme.efective || guestPrefs.theme.configured);
|
updateActiveStyleSheet(guestPrefs.theme.efective || guestPrefs.theme.configured);
|
||||||
return { user: null, prefs: guestPrefs };
|
return { user: null, prefs: guestPrefs };
|
||||||
|
|
||||||
case 'updateOrg':
|
case 'updateOrg':
|
||||||
const newPrefs = updateSelectedOrg(state.prefs, action.name);
|
prefs = updateSelectedOrg(state.prefs, action.name);
|
||||||
lsStorage.setPrefs(newPrefs, state.user!.alias);
|
lsStorage.setPrefs(prefs, state.user!.alias);
|
||||||
if (isUndefined(state.prefs.controlPanel.selectedOrg) || action.name !== state.prefs.controlPanel.selectedOrg) {
|
if (isUndefined(state.prefs.controlPanel.selectedOrg) || action.name !== state.prefs.controlPanel.selectedOrg) {
|
||||||
redirectToControlPanel('org');
|
redirectToControlPanel('org');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: newPrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'updateLimit':
|
case 'updateLimit':
|
||||||
const updatedPrefs = {
|
prefs = {
|
||||||
...state.prefs,
|
...state.prefs,
|
||||||
search: {
|
search: {
|
||||||
...state.prefs.search,
|
...state.prefs.search,
|
||||||
limit: action.limit,
|
limit: action.limit,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
lsStorage.setPrefs(updatedPrefs, state.user ? state.user.alias : undefined);
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: updatedPrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'updateTheme':
|
case 'updateTheme':
|
||||||
const updatedUserPrefs = {
|
prefs = {
|
||||||
...state.prefs,
|
...state.prefs,
|
||||||
theme: {
|
theme: {
|
||||||
configured: action.theme,
|
configured: action.theme,
|
||||||
|
|
@ -196,35 +207,35 @@ export function appReducer(state: AppState, action: Action) {
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
lsStorage.setPrefs(updatedUserPrefs, state.user ? state.user.alias : undefined);
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
updateActiveStyleSheet(action.theme);
|
updateActiveStyleSheet(action.theme);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: updatedUserPrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'updateEfectiveTheme':
|
case 'updateEfectiveTheme':
|
||||||
const updatedThemePrefs = {
|
prefs = {
|
||||||
...state.prefs,
|
...state.prefs,
|
||||||
theme: {
|
theme: {
|
||||||
...state.prefs.theme,
|
...state.prefs.theme,
|
||||||
efective: action.theme,
|
efective: action.theme,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
lsStorage.setPrefs(updatedThemePrefs, state.user ? state.user.alias : undefined);
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
updateActiveStyleSheet(action.theme);
|
updateActiveStyleSheet(action.theme);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: updatedThemePrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'enableAutomaticTheme':
|
case 'enableAutomaticTheme':
|
||||||
const updatedThemeUserPrefs = updateAutomaticTheme(state.prefs, action.enabled);
|
prefs = updateAutomaticTheme(state.prefs, action.enabled);
|
||||||
lsStorage.setPrefs(updatedThemeUserPrefs, state.user ? state.user.alias : undefined);
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
updateActiveStyleSheet(updatedThemeUserPrefs.theme.efective || updatedThemeUserPrefs.theme.configured);
|
updateActiveStyleSheet(prefs.theme.efective || prefs.theme.configured);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
prefs: updatedThemeUserPrefs,
|
prefs: prefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'updateUser':
|
case 'updateUser':
|
||||||
|
|
@ -237,6 +248,36 @@ export function appReducer(state: AppState, action: Action) {
|
||||||
...action.user,
|
...action.user,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case 'enabledDisplayedNotifications':
|
||||||
|
prefs = {
|
||||||
|
...state.prefs,
|
||||||
|
notifications: {
|
||||||
|
...state.prefs.notifications,
|
||||||
|
enabled: action.enabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
prefs: prefs,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'addNewDisplayedNotification':
|
||||||
|
prefs = {
|
||||||
|
...state.prefs,
|
||||||
|
notifications: {
|
||||||
|
...state.prefs.notifications,
|
||||||
|
displayed: [...state.prefs.notifications.displayed, action.id],
|
||||||
|
lastDisplayedTime: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
lsStorage.setPrefs(prefs, state.user ? state.user.alias : undefined);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
prefs: prefs,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import detectActiveThemeMode from '../utils/detectActiveThemeMode';
|
||||||
import history from '../utils/history';
|
import history from '../utils/history';
|
||||||
import lsPreferences from '../utils/localStoragePreferences';
|
import lsPreferences from '../utils/localStoragePreferences';
|
||||||
import AlertController from './common/AlertController';
|
import AlertController from './common/AlertController';
|
||||||
|
import UserNotificationsController from './common/userNotifications';
|
||||||
import ControlPanelView from './controlPanel';
|
import ControlPanelView from './controlPanel';
|
||||||
import HomeView from './home';
|
import HomeView from './home';
|
||||||
import BannerMOTD from './navigation/BannerMOTD';
|
import BannerMOTD from './navigation/BannerMOTD';
|
||||||
|
|
@ -58,6 +59,7 @@ export default function App() {
|
||||||
<ScrollMemory />
|
<ScrollMemory />
|
||||||
<AlertController />
|
<AlertController />
|
||||||
<BannerMOTD />
|
<BannerMOTD />
|
||||||
|
<UserNotificationsController />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={['/', '/verify-email', '/login', '/accept-invitation', '/oauth-failed', '/reset-password']}
|
path={['/', '/verify-email', '/login', '/accept-invitation', '/oauth-failed', '/reset-password']}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
.toast {
|
||||||
|
opacity: 0;
|
||||||
|
bottom: 20px;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
width: 700px;
|
||||||
|
max-width: 80%;
|
||||||
|
background-color: var(--white);
|
||||||
|
border: 3px solid var(--color-1-500);
|
||||||
|
border-radius: 15px;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isVisible {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0px 0px 15px 0 var(--color-black-75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn {
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content code {
|
||||||
|
color: var(--color-1-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content *:is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`UserNotificationsController creates snapshot 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="d-none d-md-flex justify-content-center align-items-center w-100"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="position-fixed toast fade toast show isVisible notificationCard"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="toast-body"
|
||||||
|
data-testid="notificationContent"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-row align-items-start"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-dark py-0 position-relative btn"
|
||||||
|
data-testid="disableNotificationsBtn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Don't show me more again
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="close position-relative closeBtn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="text-secondary"
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Using the
|
||||||
|
<strong>
|
||||||
|
filters
|
||||||
|
</strong>
|
||||||
|
on the left column, you can narrow down search results. You can filter by
|
||||||
|
<em>
|
||||||
|
kind
|
||||||
|
</em>
|
||||||
|
,
|
||||||
|
<em>
|
||||||
|
category
|
||||||
|
</em>
|
||||||
|
,
|
||||||
|
<em>
|
||||||
|
publisher
|
||||||
|
</em>
|
||||||
|
,
|
||||||
|
<em>
|
||||||
|
repository
|
||||||
|
</em>
|
||||||
|
and
|
||||||
|
<em>
|
||||||
|
license
|
||||||
|
</em>
|
||||||
|
, as well as only showing packages from
|
||||||
|
<strong>
|
||||||
|
official repositories
|
||||||
|
</strong>
|
||||||
|
or
|
||||||
|
<strong>
|
||||||
|
verified publishers
|
||||||
|
</strong>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { AppCtx } from '../../../context/AppCtx';
|
||||||
|
import UserNotificationsController from './index';
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
user: { alias: 'test', email: 'test@test.com' },
|
||||||
|
prefs: {
|
||||||
|
controlPanel: {},
|
||||||
|
search: { limit: 60 },
|
||||||
|
theme: {
|
||||||
|
configured: 'light',
|
||||||
|
automatic: false,
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('UserNotificationsController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const mockMath = Object.create(global.Math);
|
||||||
|
mockMath.random = () => 0;
|
||||||
|
global.Math = mockMath;
|
||||||
|
|
||||||
|
global.window = Object.create(window);
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: {
|
||||||
|
href: 'http://test.com',
|
||||||
|
pathname: '/packages/search',
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
jest.useRealTimers();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates snapshot', async () => {
|
||||||
|
const result = render(
|
||||||
|
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
|
||||||
|
<UserNotificationsController />
|
||||||
|
</AppCtx.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = result.getByRole('alert');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.queryByTestId('notificationContent')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).not.toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders component', async () => {
|
||||||
|
const { getByRole, getByTestId, getByText, queryByTestId } = render(
|
||||||
|
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
|
||||||
|
<UserNotificationsController />
|
||||||
|
</AppCtx.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getByRole('alert');
|
||||||
|
expect(component).toBeInTheDocument();
|
||||||
|
expect(component).not.toHaveClass('show');
|
||||||
|
expect(component).toHaveClass('toast');
|
||||||
|
expect(component).toBeEmptyDOMElement();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).toHaveClass('show isVisible');
|
||||||
|
expect(getByTestId('disableNotificationsBtn')).toBeInTheDocument();
|
||||||
|
expect(getByText("Don't show me more again")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId('notificationContent')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).not.toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disabled notifications', async () => {
|
||||||
|
const { getByRole, getByTestId, queryByTestId } = render(
|
||||||
|
<AppCtx.Provider value={{ ctx: mockCtx, dispatch: jest.fn() }}>
|
||||||
|
<UserNotificationsController />
|
||||||
|
</AppCtx.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getByRole('alert');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableBtn = getByTestId('disableNotificationsBtn');
|
||||||
|
fireEvent.click(disableBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId('notificationContent')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(component).not.toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call userNotificationsDispatcher.start when user is undefined', () => {
|
||||||
|
const { getByRole } = render(
|
||||||
|
<AppCtx.Provider value={{ ctx: { ...mockCtx, user: undefined }, dispatch: jest.fn() }}>
|
||||||
|
<UserNotificationsController />
|
||||||
|
</AppCtx.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getByRole('alert');
|
||||||
|
expect(component).not.toHaveClass('show isVisible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { isNull, isUndefined } from 'lodash';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
import { addNewDisplayedNotification, AppCtx, enabledDisplayedNotifications } from '../../../context/AppCtx';
|
||||||
|
import useBreakpointDetect from '../../../hooks/useBreakpointDetect';
|
||||||
|
import { UserNotification } from '../../../types';
|
||||||
|
import notificationsDispatcher from '../../../utils/userNotificationsDispatcher';
|
||||||
|
import styles from './UserNotifications.module.css';
|
||||||
|
|
||||||
|
interface HeadingProps {
|
||||||
|
level: number;
|
||||||
|
title?: string;
|
||||||
|
children?: JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Heading: React.ElementType = (data: HeadingProps) => {
|
||||||
|
const Tag = `h${data.level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
|
return <Tag className="text-secondary">{data.title || data.children}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ANIMATION_TIME = 300; //300ms
|
||||||
|
|
||||||
|
const UserNotificationsController: React.ElementType = () => {
|
||||||
|
const { dispatch, ctx } = useContext(AppCtx);
|
||||||
|
const [notification, setNotification] = useState<UserNotification | null>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||||
|
const [addNotificationTimeout, setAddNotificationTimeout] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const [dismissNotificationTimeout, setDismissNotificationTimeout] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const point = useBreakpointDetect();
|
||||||
|
|
||||||
|
notificationsDispatcher.subscribe({
|
||||||
|
updateUserNotificationsWrapper: (notif: UserNotification | null) => {
|
||||||
|
if (!isNull(notif)) {
|
||||||
|
setNotification(notif);
|
||||||
|
setIsVisible(true);
|
||||||
|
setAddNotificationTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(addNewDisplayedNotification(notif.id));
|
||||||
|
}, ANIMATION_TIME)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
setDismissNotificationTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isNull(notification)) {
|
||||||
|
setNotification(null);
|
||||||
|
}
|
||||||
|
}, ANIMATION_TIME)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
notificationsDispatcher.dismissNotification();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeNotificationsPrefs = () => {
|
||||||
|
notificationsDispatcher.dismissNotification();
|
||||||
|
// Change user prefs
|
||||||
|
dispatch(enabledDisplayedNotifications(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUndefined(ctx.user)) {
|
||||||
|
notificationsDispatcher.start(ctx.prefs.notifications, point);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
notificationsDispatcher.close();
|
||||||
|
};
|
||||||
|
}, [ctx.user]); /* eslint-disable-line react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
notificationsDispatcher.updateSettings(ctx.prefs.notifications);
|
||||||
|
}, [ctx.prefs.notifications]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (addNotificationTimeout) {
|
||||||
|
clearTimeout(addNotificationTimeout);
|
||||||
|
}
|
||||||
|
if (dismissNotificationTimeout) {
|
||||||
|
clearTimeout(dismissNotificationTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [addNotificationTimeout, dismissNotificationTimeout]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-none d-md-flex justify-content-center align-items-center w-100">
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
'position-fixed toast fade',
|
||||||
|
styles.toast,
|
||||||
|
{
|
||||||
|
[`show ${styles.isVisible}`]: !isNull(notification) && isVisible,
|
||||||
|
},
|
||||||
|
'notificationCard'
|
||||||
|
)}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{!isNull(notification) && (
|
||||||
|
<div className="toast-body" data-testid="notificationContent">
|
||||||
|
<div>
|
||||||
|
<div className="float-right">
|
||||||
|
<div className="d-flex flex-row align-items-start">
|
||||||
|
<button
|
||||||
|
data-testid="disableNotificationsBtn"
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-link text-dark py-0 position-relative ${styles.btn}`}
|
||||||
|
onClick={onChangeNotificationsPrefs}
|
||||||
|
>
|
||||||
|
Don't show me more again
|
||||||
|
</button>
|
||||||
|
<button type="button" className={`close position-relative ${styles.closeBtn}`} onClick={onClose}>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<ReactMarkdown
|
||||||
|
className={styles.content}
|
||||||
|
children={notification.body}
|
||||||
|
renderers={{
|
||||||
|
heading: Heading,
|
||||||
|
}}
|
||||||
|
linkTarget="_blank"
|
||||||
|
skipHtml
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserNotificationsController;
|
||||||
|
|
@ -31,6 +31,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -36,6 +41,11 @@ const mockOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,6 +60,11 @@ const mockOrgCtx1 = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -41,6 +46,11 @@ const mockCtxOrgSelected = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ const mockWithoutSelectedOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,6 +51,11 @@ const mockWithSelectedOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ const mockWithoutSelectedOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,6 +60,11 @@ const mockWithSelectedOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,6 +79,11 @@ const mockNotSelectedOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ const mockUserCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,6 +51,11 @@ const mockOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ const mockUserCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -49,6 +54,11 @@ const mockOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ const mockUserCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -40,6 +45,11 @@ const mockOrgCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ const mockCtxLoggedIn = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -35,6 +40,11 @@ const mockCtxNotLoggedIn = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,6 +57,11 @@ const mockUndefinedUser = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ const mockCtxLoggedIn = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -32,6 +37,11 @@ const mockCtxNotLoggedIn = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,6 +54,11 @@ const mockUndefinedUser = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ const mockCtxLoggedIn = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ const CustomResourceDefinition = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className="d-flex flex-wrap mt-4">
|
<div className="d-flex flex-wrap mt-4">
|
||||||
{props.resources.map((resourceDefinition: CustomResourcesDefinition) => {
|
{props.resources.map((resourceDefinition: CustomResourcesDefinition, index: number) => {
|
||||||
return (
|
return (
|
||||||
<div className="col-12 col-lg-6 col-xxl-4 mb-4" key={`resourceDef_${resourceDefinition.kind}`}>
|
<div className="col-12 col-lg-6 col-xxl-4 mb-4" key={`resourceDef_${resourceDefinition.kind}_${index}`}>
|
||||||
<div className="card h-100" data-testid="resourceDefinition">
|
<div className="card h-100" data-testid="resourceDefinition">
|
||||||
<div className="card-body d-flex flex-column">
|
<div className="card-body d-flex flex-column">
|
||||||
<h6 className="card-title mb-3">{resourceDefinition.displayName || resourceDefinition.name}</h6>
|
<h6 className="card-title mb-3">{resourceDefinition.displayName || resourceDefinition.name}</h6>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ const mockCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -35,6 +40,11 @@ const mockNotSignedInCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,6 +57,11 @@ const mockUndefinedUserCtx = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
--color-1-5: rgba(15, 14, 17, 0.95);
|
--color-1-5: rgba(15, 14, 17, 0.95);
|
||||||
--color-1-10: rgba(15, 14, 17, 0.9);
|
--color-1-10: rgba(15, 14, 17, 0.9);
|
||||||
--color-1-20: rgba(15, 14, 17, 0.8);
|
--color-1-20: rgba(15, 14, 17, 0.8);
|
||||||
|
--color-1-95: rgba(15, 14, 17, 0.95);
|
||||||
--color-2-500: #0f0e11;
|
--color-2-500: #0f0e11;
|
||||||
|
|
||||||
--color-2-10: rgba(var(--color-2-500), 0.1);
|
--color-2-10: rgba(var(--color-2-500), 0.1);
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
--color-black-90: rgba(255, 255, 255, 0.9);
|
--color-black-90: rgba(255, 255, 255, 0.9);
|
||||||
|
|
||||||
--color-white-50: rgba(0, 0, 0, 0.5);
|
--color-white-50: rgba(0, 0, 0, 0.5);
|
||||||
|
--color-white-95: rgba(0, 0, 0, 0.95);
|
||||||
|
|
||||||
--logo-font: 'Archivo Black', cursive;
|
--logo-font: 'Archivo Black', cursive;
|
||||||
|
|
||||||
|
|
@ -229,6 +230,10 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notificationCard {
|
||||||
|
box-shadow: 0px 0px 25px 5px var(--color-black-25);
|
||||||
|
}
|
||||||
|
|
||||||
.activeVersion {
|
.activeVersion {
|
||||||
& > span {
|
& > span {
|
||||||
color: var(--hightlighted);
|
color: var(--hightlighted);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
--color-1-5: rgba(28, 44, 53, 0.05);
|
--color-1-5: rgba(28, 44, 53, 0.05);
|
||||||
--color-1-10: rgba(28, 44, 53, 0.1);
|
--color-1-10: rgba(28, 44, 53, 0.1);
|
||||||
--color-1-20: rgba(28, 44, 53, 0.2);
|
--color-1-20: rgba(28, 44, 53, 0.2);
|
||||||
|
--color-1-95: rgba(28, 44, 53, 0.95);
|
||||||
--color-2-500: #b0cee0;
|
--color-2-500: #b0cee0;
|
||||||
|
|
||||||
--color-2-10: rgba(176, 206, 224, 0.1);
|
--color-2-10: rgba(176, 206, 224, 0.1);
|
||||||
|
|
@ -23,8 +24,11 @@
|
||||||
--color-black-5: rgba(0, 0, 0, 0.05);
|
--color-black-5: rgba(0, 0, 0, 0.05);
|
||||||
--color-black-15: rgba(0, 0, 0, 0.15);
|
--color-black-15: rgba(0, 0, 0, 0.15);
|
||||||
--color-black-25: rgba(0, 0, 0, 0.25);
|
--color-black-25: rgba(0, 0, 0, 0.25);
|
||||||
|
--color-black-75: rgba(0, 0, 0, 0.75);
|
||||||
|
--color-black-90: rgba(0, 0, 0, 0.9);
|
||||||
|
|
||||||
--color-white-50: rgba(256, 256, 256, 0.5);
|
--color-white-50: rgba(256, 256, 256, 0.5);
|
||||||
|
--color-white-95: rgba(256, 256, 256, 0.95);
|
||||||
|
|
||||||
--logo-font: 'Archivo Black', cursive;
|
--logo-font: 'Archivo Black', cursive;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,12 @@ export interface AvailabilityInfo {
|
||||||
excluded: string[];
|
excluded: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationsPrefs {
|
||||||
|
lastDisplayedTime: null | number;
|
||||||
|
enabled: boolean;
|
||||||
|
displayed: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Prefs {
|
export interface Prefs {
|
||||||
theme: ThemePrefs;
|
theme: ThemePrefs;
|
||||||
controlPanel: {
|
controlPanel: {
|
||||||
|
|
@ -325,6 +331,7 @@ export interface Prefs {
|
||||||
search: {
|
search: {
|
||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
notifications: NotificationsPrefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemePrefs {
|
export interface ThemePrefs {
|
||||||
|
|
@ -552,3 +559,16 @@ export interface ActiveJSONSchemaValue {
|
||||||
options: JSONSchema[];
|
options: JSONSchema[];
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserNotification {
|
||||||
|
id: string;
|
||||||
|
body: string;
|
||||||
|
linkTip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PathTips {
|
||||||
|
Home = 'home',
|
||||||
|
Package = 'package',
|
||||||
|
Search = 'search',
|
||||||
|
ControlPanel = 'control-panel',
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import analytics from '../analytics/analytics';
|
import analytics from '../analytics/analytics';
|
||||||
import updateMetaIndex from './updateMetaIndex';
|
import updateMetaIndex from './updateMetaIndex';
|
||||||
|
import notificationsDispatcher from './userNotificationsDispatcher';
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
const detailPath = /^\/packages\/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task)\//;
|
const detailPath = /^\/packages\/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task)\//;
|
||||||
|
|
@ -12,6 +13,8 @@ history.listen((location) => {
|
||||||
updateMetaIndex();
|
updateMetaIndex();
|
||||||
}
|
}
|
||||||
analytics.page();
|
analytics.page();
|
||||||
|
// Clean notifications to change location
|
||||||
|
notificationsDispatcher.dismissNotification();
|
||||||
});
|
});
|
||||||
|
|
||||||
export default history;
|
export default history;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@ const defaultPrefs = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialUserPrefs = {
|
const initialUserPrefs = {
|
||||||
controlPanel: {},
|
controlPanel: {},
|
||||||
search: { limit: 60 },
|
search: { limit: 60 },
|
||||||
|
|
@ -15,6 +21,11 @@ const initialUserPrefs = {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('localStoragePreferences', () => {
|
describe('localStoragePreferences', () => {
|
||||||
|
|
@ -40,6 +51,11 @@ describe('localStoragePreferences', () => {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
lsPreferences.setPrefs(userPrefs, 'user1');
|
lsPreferences.setPrefs(userPrefs, 'user1');
|
||||||
expect(lsPreferences.getPrefs('user1')).toStrictEqual(userPrefs);
|
expect(lsPreferences.getPrefs('user1')).toStrictEqual(userPrefs);
|
||||||
|
|
@ -57,6 +73,11 @@ describe('localStoragePreferences', () => {
|
||||||
configured: 'light',
|
configured: 'light',
|
||||||
automatic: false,
|
automatic: false,
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
lsPreferences.setPrefs(userPrefs, 'user2');
|
lsPreferences.setPrefs(userPrefs, 'user2');
|
||||||
expect(lsPreferences.getPrefs('user2')).toStrictEqual(userPrefs);
|
expect(lsPreferences.getPrefs('user2')).toStrictEqual(userPrefs);
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,16 @@ const LS_ITEM = 'prefs';
|
||||||
export const DEFAULT_SEARCH_LIMIT = 20;
|
export const DEFAULT_SEARCH_LIMIT = 20;
|
||||||
const DEFAULT_THEME = 'light';
|
const DEFAULT_THEME = 'light';
|
||||||
const LS_ACTIVE_PROFILE = 'activeProfile';
|
const LS_ACTIVE_PROFILE = 'activeProfile';
|
||||||
|
|
||||||
const DEFAULT_PREFS: Prefs = {
|
const DEFAULT_PREFS: Prefs = {
|
||||||
search: { limit: DEFAULT_SEARCH_LIMIT },
|
search: { limit: DEFAULT_SEARCH_LIMIT },
|
||||||
controlPanel: {},
|
controlPanel: {},
|
||||||
theme: { configured: DEFAULT_THEME, automatic: false },
|
theme: { configured: DEFAULT_THEME, automatic: false },
|
||||||
|
notifications: {
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LocalStoragePreferences {
|
export class LocalStoragePreferences {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"body": "## Light and dark themes\nYou can switch between the **light** and **dark** themes from the icon in the *top right in the navigation bar*. It is also possible to use the **automatic** mode, that will rely on your system setting when available.",
|
||||||
|
"linkTip": "home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Filters\nUsing the **filters** on the left column, you can narrow down search results. You can filter by *kind*, *category*, *publisher*, *repository* and *license*, as well as only showing packages from **official repositories** or **verified publishers**.",
|
||||||
|
"linkTip": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Search tip\nUse **multiple words** to refine your search. Example: *kafka operator*.",
|
||||||
|
"linkTip": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Search tip\nUse **-** to exclude words from your search. Example: *apache -solr -hadoop*.",
|
||||||
|
"linkTip": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Search tip\nPut a phrase inside **double quotes** for an exact match. Example: *\"monitoring system\"*.",
|
||||||
|
"linkTip": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Search tip\nUse **or** to combine multiple searches. Example: *postgresql or mysql*.",
|
||||||
|
"linkTip": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## New release notifications\nUsing the bell icon on the *top right* of this screen, you can subscribe to receive **email notifications** when a new version of the packages you are interested in is released. You can manage all your subscriptions from the control panel.",
|
||||||
|
"linkTip": "package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Webhooks notifications\nFrom the control panel, you can setup **webhooks** to notify *external services* when certain events happen. This could be, for example, posting a notification to Slack when a new version of certain packages is released.",
|
||||||
|
"linkTip": "package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Star your favorite packages\nBy starring your favorite packages, you help them **stand out in search results**, helping users when deciding among multiple alternatives.",
|
||||||
|
"linkTip": "package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "## Related packages\nOn the *right column*, below the package summary section, you will find some packages related to the one you are currently viewing.",
|
||||||
|
"linkTip": "package"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { UserNotification } from '../types';
|
||||||
|
import notificationsDispatcher from './userNotificationsDispatcher';
|
||||||
|
|
||||||
|
const updateUserNotificationMock = jest.fn();
|
||||||
|
|
||||||
|
const notificationSample = {
|
||||||
|
id: 'id',
|
||||||
|
body: 'Lorem ipsum',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('notificationsDispatcher', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const mockMath = Object.create(global.Math);
|
||||||
|
mockMath.random = () => 0;
|
||||||
|
global.Math = mockMath;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
jest.useRealTimers();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts dispatcher', async () => {
|
||||||
|
notificationsDispatcher.subscribe({
|
||||||
|
updateUserNotificationsWrapper: (notification: UserNotification | null) =>
|
||||||
|
updateUserNotificationMock(notification),
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsDispatcher.start({
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateUserNotificationMock).toHaveBeenCalledTimes(3);
|
||||||
|
expect(updateUserNotificationMock).toHaveBeenLastCalledWith(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dismiss notification', async () => {
|
||||||
|
notificationsDispatcher.subscribe({
|
||||||
|
updateUserNotificationsWrapper: (notification: UserNotification | null) =>
|
||||||
|
updateUserNotificationMock(notification),
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsDispatcher.updateSettings({
|
||||||
|
lastDisplayedTime: null,
|
||||||
|
enabled: true,
|
||||||
|
displayed: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsDispatcher.postNotification(notificationSample);
|
||||||
|
notificationsDispatcher.dismissNotification();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateUserNotificationMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(updateUserNotificationMock).toHaveBeenCalledWith(notificationSample);
|
||||||
|
expect(updateUserNotificationMock).toHaveBeenLastCalledWith(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import md5 from 'crypto-js/md5';
|
||||||
|
import { groupBy, isNull, isUndefined } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { NotificationsPrefs, PathTips, UserNotification } from '../types';
|
||||||
|
|
||||||
|
const DEFAULT_START_TIME = 3 * 1000; //3s
|
||||||
|
const DEFAULT_DISMISS_TIME = 20 * 1000; //20s
|
||||||
|
|
||||||
|
export interface UserNotificationsUpdatesHandler {
|
||||||
|
updateUserNotificationsWrapper(notification: UserNotification | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailPkgPath = /^\/packages\/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task)\//;
|
||||||
|
|
||||||
|
const getNotifications = (): UserNotification[] => {
|
||||||
|
const list = require('./notifications.json').notifications;
|
||||||
|
return list.map((notif: any) => {
|
||||||
|
const notification: UserNotification = {
|
||||||
|
...notif,
|
||||||
|
id: md5(notif.body).toString(),
|
||||||
|
};
|
||||||
|
return notification;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentLocationPath = (): PathTips | undefined => {
|
||||||
|
const currentLocation = window.location.pathname;
|
||||||
|
if (currentLocation.startsWith('/control-panel')) {
|
||||||
|
return PathTips.ControlPanel;
|
||||||
|
} else if (detailPkgPath.test(currentLocation)) {
|
||||||
|
return PathTips.Package;
|
||||||
|
} else {
|
||||||
|
switch (currentLocation) {
|
||||||
|
case '/':
|
||||||
|
return PathTips.Home;
|
||||||
|
case '/packages/search':
|
||||||
|
return PathTips.Search;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserNotificationsDispatcher {
|
||||||
|
private updatesHandler?: UserNotificationsUpdatesHandler;
|
||||||
|
private activeNotification: UserNotification | null = null;
|
||||||
|
private startTimeout?: NodeJS.Timeout;
|
||||||
|
private dismissTimeout?: NodeJS.Timeout;
|
||||||
|
private settings: NotificationsPrefs | null = null;
|
||||||
|
private notifications: UserNotification[] = getNotifications();
|
||||||
|
private breakpoint: string | undefined;
|
||||||
|
|
||||||
|
public start(prefs: NotificationsPrefs, breakpoint?: string) {
|
||||||
|
this.settings = prefs;
|
||||||
|
this.breakpoint = breakpoint;
|
||||||
|
this.startTimeout = setTimeout(() => {
|
||||||
|
// We don't display notifications for small devices
|
||||||
|
if (isUndefined(this.breakpoint) || !['xs', 'sm'].includes(this.breakpoint)) {
|
||||||
|
this.get(true);
|
||||||
|
}
|
||||||
|
}, DEFAULT_START_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(dateLimit: boolean) {
|
||||||
|
this.dismissNotification();
|
||||||
|
const notif = this.getRandomNotification(dateLimit);
|
||||||
|
if (!isNull(notif)) {
|
||||||
|
this.postNotification(notif);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(updatesHandler: UserNotificationsUpdatesHandler) {
|
||||||
|
this.updatesHandler = updatesHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public postNotification(notification: UserNotification | null) {
|
||||||
|
this.activeNotification = notification;
|
||||||
|
if (this.updatesHandler) {
|
||||||
|
this.updatesHandler.updateUserNotificationsWrapper(this.activeNotification);
|
||||||
|
}
|
||||||
|
if (!isNull(notification)) {
|
||||||
|
this.dismissTimeout = setTimeout(() => {
|
||||||
|
this.dismissNotification();
|
||||||
|
}, DEFAULT_DISMISS_TIME);
|
||||||
|
} else {
|
||||||
|
this.cleanTimeouts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dismissNotification() {
|
||||||
|
this.postNotification(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanTimeouts() {
|
||||||
|
if (this.startTimeout) {
|
||||||
|
clearTimeout(this.startTimeout);
|
||||||
|
}
|
||||||
|
if (this.dismissTimeout) {
|
||||||
|
clearTimeout(this.dismissTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
this.cleanTimeouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSettings(settings: NotificationsPrefs) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRandomNotification(dateLimit: boolean): UserNotification | null {
|
||||||
|
if (!isNull(this.settings)) {
|
||||||
|
if (
|
||||||
|
this.settings.enabled &&
|
||||||
|
this.settings.displayed.length < this.notifications.length &&
|
||||||
|
// Only display a maximun of 1 notification per day when dateLimit is true
|
||||||
|
(!dateLimit ||
|
||||||
|
isNull(this.settings.lastDisplayedTime) ||
|
||||||
|
moment(this.settings.lastDisplayedTime).diff(Date.now(), 'day') > 0)
|
||||||
|
) {
|
||||||
|
let notDisplayedNotifications = groupBy(
|
||||||
|
this.notifications.filter((notif: UserNotification) => !this.settings!.displayed.includes(notif.id)),
|
||||||
|
'linkTip'
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentLocationTip = getCurrentLocationPath();
|
||||||
|
const getCommonTip = (): UserNotification | null => {
|
||||||
|
return notDisplayedNotifications.hasOwnProperty('undefined')
|
||||||
|
? (notDisplayedNotifications.undefined[
|
||||||
|
Math.floor(Math.random() * notDisplayedNotifications.undefined.length)
|
||||||
|
] as UserNotification)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentLocationTip) {
|
||||||
|
if (
|
||||||
|
notDisplayedNotifications.hasOwnProperty(currentLocationTip) &&
|
||||||
|
notDisplayedNotifications[currentLocationTip].length > 0
|
||||||
|
) {
|
||||||
|
return notDisplayedNotifications[currentLocationTip][
|
||||||
|
Math.floor(Math.random() * notDisplayedNotifications[currentLocationTip].length)
|
||||||
|
] as UserNotification;
|
||||||
|
} else {
|
||||||
|
return getCommonTip();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return getCommonTip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationsDispatcher = new UserNotificationsDispatcher();
|
||||||
|
export default notificationsDispatcher;
|
||||||
|
|
@ -1677,6 +1677,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf"
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf"
|
||||||
integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==
|
integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==
|
||||||
|
|
||||||
|
"@types/crypto-js@^4.0.1":
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.0.1.tgz#3a4bd24518b0e6c5940da4e2659eeb2ef0806963"
|
||||||
|
integrity sha512-6+OPzqhKX/cx5xh+yO8Cqg3u3alrkhoxhE5ZOdSEv0DOzJ13lwJ6laqGU0Kv6+XDMFmlnGId04LtY22PsFLQUw==
|
||||||
|
|
||||||
"@types/eslint-visitor-keys@^1.0.0":
|
"@types/eslint-visitor-keys@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||||
|
|
@ -3783,6 +3788,11 @@ crypto-browserify@^3.11.0:
|
||||||
randombytes "^2.0.0"
|
randombytes "^2.0.0"
|
||||||
randomfill "^1.0.3"
|
randomfill "^1.0.3"
|
||||||
|
|
||||||
|
crypto-js@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.0.0.tgz#2904ab2677a9d042856a2ea2ef80de92e4a36dcc"
|
||||||
|
integrity sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==
|
||||||
|
|
||||||
css-blank-pseudo@^0.1.4:
|
css-blank-pseudo@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue