Skip to content

Commit

Permalink
Merge branch 'develop' into r2-2608-prevent-multiple-cases
Browse files Browse the repository at this point in the history
  • Loading branch information
dhernandez-quoin committed Sep 20, 2023
2 parents 37c6f5e + 432a879 commit f60a3bd
Show file tree
Hide file tree
Showing 18 changed files with 1,708 additions and 892 deletions.
23 changes: 23 additions & 0 deletions .swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
// .swcrc should be treated as JSONC

"sourceMaps": true,

"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": true
},

"transform": {
"react": {
"runtime": "automatic"
}
},

"baseUrl": "./app/javascript",
"paths": {
"test-utils":[ "./test-utils/index.js"]
}
}
}
7 changes: 1 addition & 6 deletions app/javascript/app-init.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { getAppResources, saveNotificationSubscription } from "./components/user/action-creators";
import { getSubscriptionFromDb } from "./libs/service-worker-utils";
import { getAppResources } from "./components/user/action-creators";
import configureStore from "./store";

function appInit() {
const store = configureStore();

getSubscriptionFromDb().then(endpoint => {
store.dispatch(saveNotificationSubscription(endpoint));
});

store.dispatch(getAppResources);

return { store };
Expand Down
24 changes: 24 additions & 0 deletions app/javascript/components/conditional-tooltip/component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Tooltip } from "@material-ui/core";
import PropTypes from "prop-types";

function Component({ children, condition, title }) {
if (condition) {
return (
<Tooltip title={title} enterTouchDelay={20}>
<div>{children}</div>
</Tooltip>
);
}

return children;
}

Component.displayName = "ConditionalTooltip";

Component.propTypes = {
children: PropTypes.node.isRequired,
condition: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired
};

export default Component;
1 change: 1 addition & 0 deletions app/javascript/components/conditional-tooltip/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./component";
30 changes: 23 additions & 7 deletions app/javascript/components/push-notifications-toggle/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { useI18n } from "../i18n";
import ActionDialog, { useDialog } from "../action-dialog";
import { useMemoizedSelector } from "../../libs";
import { getWebpushConfig } from "../application/selectors";
import { getNotificationSubscription, removeNotificationSubscription, saveNotificationSubscription } from "../user";
import {
getNotificationSubscription,
getUserProperty,
removeNotificationSubscription,
saveNotificationSubscription
} from "../user";
import ConditionalTooltip from "../conditional-tooltip";

import css from "./styles.css";

Expand All @@ -21,17 +27,21 @@ function Component() {

const webpushConfig = useMemoizedSelector(state => getWebpushConfig(state));
const notificationEndpoint = useMemoizedSelector(state => getNotificationSubscription(state));

const [value, setValue] = useState(Boolean(notificationEndpoint));
const receiveWebpush = useMemoizedSelector(state => getUserProperty(state, "receive_webpush"));
const userLoaded = useMemoizedSelector(state => getUserProperty(state, "loaded"));
const [value, setValue] = useState(false);

const vapidID = webpushConfig.get("vapid_public");

const i18n = useI18n();
const { dialogOpen, setDialog } = useDialog(DIALOG);

const notificationsNotSupported = !("Notification" in window);
const notificationsNotSupported = !("Notification" in window) || !receiveWebpush;
const notificationsDenied = () => Notification.permission === NOTIFICATION_PERMISSIONS.DENIED;

useEffect(async () => {
setValue(await Boolean(notificationEndpoint));
}, []);

const handleSwitch = opened => event => {
const checked = event?.target?.checked;

Expand Down Expand Up @@ -97,14 +107,20 @@ function Component() {
window.vpubID = vapidID;
}, [vapidID]);

useEffect(() => {
if (!receiveWebpush && userLoaded) {
setValue(false);
}
}, [receiveWebpush]);

const pauseAfterDays = Math.floor(webpushConfig.get("pause_after") / 1440);

if (!webpushConfig.get("enabled", false)) {
return false;
}

return (
<>
<ConditionalTooltip condition={!receiveWebpush} title={i18n.t("user.receive_webpush.tooltip")}>
<FormControlLabel
disabled={notificationsNotSupported}
value="top"
Expand Down Expand Up @@ -139,7 +155,7 @@ function Component() {
i18n.t("push_notifications_dialog.body", { count: pauseAfterDays })
)}
</ActionDialog>
</>
</ConditionalTooltip>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useCallback, useEffect, useRef } from "react";
import { workerTimers } from "react-idle-timer";
import { useDispatch } from "react-redux";

import { PUSH_NOTIFICATION_SUBSCRIPTION_REFRESH_INTERVAL } from "../../config/constants";
import { POST_MESSAGES, PUSH_NOTIFICATION_SUBSCRIPTION_REFRESH_INTERVAL } from "../../config/constants";
import useMemoizedSelector from "../../libs/use-memoized-selector";
import { getNotificationSubscription } from "../user/selectors";
import { getNotificationSubscription, getUserProperty } from "../user/selectors";
import { toServerDateFormat } from "../../libs";

import { refreshNotificationSubscription } from "./action-creators";
Expand All @@ -15,6 +15,8 @@ function usePushNotifications() {
const dispatch = useDispatch();
const endpoint = useRef();
const notificationEndpoint = useMemoizedSelector(state => getNotificationSubscription(state));
const receiveWebpush = useMemoizedSelector(state => getUserProperty(state, "receive_webpush"));
const userLoaded = useMemoizedSelector(state => getUserProperty(state, "loaded"));

useEffect(() => {
endpoint.current = notificationEndpoint;
Expand All @@ -39,6 +41,15 @@ function usePushNotifications() {
}
});

useEffect(() => {
if (!receiveWebpush && userLoaded) {
stopRefreshNotificationTimer();
postMessage({
type: POST_MESSAGES.UNSUBSCRIBE_NOTIFICATIONS
});
}
}, [receiveWebpush]);

return {
startRefreshNotificationTimer,
stopRefreshNotificationTimer
Expand Down
14 changes: 7 additions & 7 deletions app/javascript/components/user/action-creators.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export const fetchAuthenticatedUserData = user => ({
}
});

export function saveNotificationSubscription() {
return {
type: actions.SAVE_USER_NOTIFICATION_SUBSCRIPTION
};
}

export const setAuthenticatedUser = user => async dispatch => {
dispatch(setUser(user));
dispatch(fetchAuthenticatedUserData(user)).then(
Expand All @@ -52,6 +58,7 @@ export const setAuthenticatedUser = user => async dispatch => {
console.error(error);
}
);
dispatch(saveNotificationSubscription());
};

export const attemptSignout = () => ({
Expand Down Expand Up @@ -115,13 +122,6 @@ export async function getAppResources(dispatch) {
dispatch(loginSystemSettings());
}

export function saveNotificationSubscription(payload) {
return {
type: actions.SAVE_USER_NOTIFICATION_SUBSCRIPTION,
payload
};
}

export function removeNotificationSubscription() {
return {
type: actions.REMOVE_USER_NOTIFICATION_SUBSCRIPTION
Expand Down
1 change: 1 addition & 0 deletions app/javascript/components/user/index.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("User - index", () => {
"getServerErrors",
"getUser",
"getUserSavingRecord",
"getUserProperty",
"hasPrimeroModule",
"hasUserPermissions",
"reducer",
Expand Down
6 changes: 4 additions & 2 deletions app/javascript/components/user/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Actions from "./actions";
import { ListHeaderRecord, FilterRecord } from "./records";

const DEFAULT_STATE = Map({
isAuthenticated: false
isAuthenticated: false,
loaded: false
});

export default (state = DEFAULT_STATE, { type, payload }) => {
Expand Down Expand Up @@ -58,7 +59,8 @@ export default (state = DEFAULT_STATE, { type, payload }) => {
codeOfConductId,
codeOfConductAcceptedOn,
permittedRoleUniqueIds,
managedReportScope
managedReportScope,
loaded: true
})
);
}
Expand Down
5 changes: 4 additions & 1 deletion app/javascript/components/user/reducer.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { FilterRecord, ListHeaderRecord } from "./records";

describe("User - Reducers", () => {
const initialState = fromJS({
isAuthenticated: false
isAuthenticated: false,
loaded: false
});

it("should handle SET_AUTHENTICATED_USER", () => {
const expected = fromJS({
isAuthenticated: true,
loaded: false,
id: 1,
username: "primero"
});
Expand Down Expand Up @@ -47,6 +49,7 @@ describe("User - Reducers", () => {
modules: ["primeromodule-cp", "primeromodule-gbv"],
permittedForms: { record_owner: "r", client_feedback: "rw" },
permittedRoleUniqueIds: ["role_1", "role_2"],
loaded: true,
locale: "en",
permissions: mapListToObject(
[
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/components/user/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const getUser = state => {
return state.get(NAMESPACE, fromJS({}));
};

export const getUserProperty = (state, property, defaultValue = false) => {
const path = Array.isArray(property) ? [NAMESPACE, ...property] : [NAMESPACE, property];

return state.getIn(path, defaultValue);
};

export const getUserSavingRecord = state => state.getIn([NAMESPACE, SAVING], false);

export const getServerErrors = state => {
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/middleware/auth-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import get from "lodash/get";
import { actions } from "../components/login/components/login-form";
import { Actions } from "../components/user";
import { ROUTES } from "../config";
import { getSubscriptionFromDb } from "../libs/service-worker-utils";

import {
LOGIN_PATTERN,
Expand Down Expand Up @@ -54,6 +55,12 @@ const authMiddleware = store => next => action => {
handleReturnUrl(store, location);
}

if (Actions.SAVE_USER_NOTIFICATION_SUBSCRIPTION === action.type) {
getSubscriptionFromDb().then(endpoint => {
next({ ...action, payload: endpoint });
});
}

next(action);
};

Expand Down
2 changes: 0 additions & 2 deletions app/javascript/test-utils/globals.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import "mutationobserver-shim";
import { MessageChannel } from "worker_threads";

import get from "lodash/get";
import { parseISO, format as formatDate } from "date-fns";
Expand Down Expand Up @@ -90,4 +89,3 @@ class Worker {
addEventListener() {}
}
global.Worker = Worker;
global.MessageChannel = MessageChannel;
13 changes: 12 additions & 1 deletion app/javascript/test-utils/setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import "react-16-node-hanging-test-fix"; // TODO: Remove when update to React 18
import "./globals";

import "@testing-library/jest-dom/extend-expect";
import { MessageChannel } from "worker_threads";

import { createMocks } from "react-idle-timer";
import { cleanup } from "@testing-library/react";

global.IS_REACT_ACT_ENVIRONMENT = true;

beforeAll(() => {
createMocks();
global.MessageChannel = MessageChannel;
});

afterEach(cleanup);
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4210,6 +4210,7 @@ en:
receive_webpush:
label: Receive push notifications?
help_text: Note that you will still need to allow push notifications on your device’s browser.
tooltip: You must first edit your account and enable push notifications for your user.
user_group_unique_ids: User Groups
user_name: Username
services: Services
Expand Down
13 changes: 8 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module.exports = {
// restoreMocks: false,

// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
rootDir: "./app/javascript",

// A list of paths to directories that Jest should use to search for files in
// roots: [
Expand All @@ -135,7 +135,7 @@ module.exports = {
setupFiles: ["fake-indexeddb/auto"],

// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ["./app/javascript/test-utils/setup.js"],
setupFilesAfterEnv: ["./test-utils/setup.js"],

// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
Expand All @@ -153,7 +153,7 @@ module.exports = {
// testLocationInResults: false,

// The glob patterns Jest uses to detect test files
testMatch: ["<rootDir>/app/javascript/components/**/*.spec.js"],
testMatch: ["<rootDir>/components/**/*.spec.js"],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: ["/node_modules/"],
Expand All @@ -168,10 +168,13 @@ module.exports = {
// testRunner: "jest-circus/runner",

// A map from regular expressions to paths to transformers
// transform: {}
transform: {
"^.+\\.(t|j)sx?$": "@swc/jest",
"^.+\\.(t|j)s?$": "@swc/jest"
},

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["node_modules/(?!uuid|(?!proxy-memoize)|proxy-memoize)"]
transformIgnorePatterns: []

// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
Expand Down
Loading

0 comments on commit f60a3bd

Please sign in to comment.