diff --git a/.pnp.cjs b/.pnp.cjs index fbede5ed0..8dc55b749 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -59,7 +59,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@mui/material", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ ["@mui/styles", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ ["@mui/system", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ - ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.17.4"],\ + ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A."],\ ["@mui/x-date-pickers", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:7.0.0-beta.7"],\ ["@next/bundle-analyzer", "npm:12.3.1"],\ ["@next/eslint-plugin-next", "npm:12.3.1"],\ @@ -83,7 +83,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/luxon", "npm:3.4.2"],\ ["@types/node", "npm:18.7.18"],\ ["@types/react", "npm:18.0.21"],\ - ["@types/seedrandom", "npm:3.0.5"],\ ["@types/testing-library__jest-dom", "npm:5.14.5"],\ ["@types/uuid", "npm:9.0.1"],\ ["@typescript-eslint/eslint-plugin", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:7.5.0"],\ @@ -149,7 +148,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-virtuoso", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:2.19.0"],\ ["recharts", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:2.3.2"],\ ["rollbar", "npm:2.25.2"],\ - ["seedrandom", "npm:3.0.5"],\ ["storybook", "npm:6.5.16"],\ ["storybook-addon-designs", "npm:6.3.1"],\ ["storybook-react-i18next", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:1.1.2"],\ @@ -10689,10 +10687,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["virtual:d0fddc76688844d0b7cd5c451944107aff44e2467232119db2a9d551c0a8c9e3841cd11dab9c2020c9df2a66918c318fbf7e81eea8e61c4d31b38f2166fd0e82#npm:5.10.3", {\ - "packageLocation": "./.yarn/__virtual__/@mui-utils-virtual-b021d13552/0/cache/@mui-utils-npm-5.10.3-28aa14e0ce-591f45a317.zip/node_modules/@mui/utils/",\ + ["virtual:76f2160c62fb95c7d23125f08593d68f943cd5f55f8822c553758636aff4fca188039da42371d467d252c4a9c12188a4ca59a59bde7e07563860c8005c8bdb77#npm:5.10.3", {\ + "packageLocation": "./.yarn/__virtual__/@mui-utils-virtual-b6ca8af581/0/cache/@mui-utils-npm-5.10.3-28aa14e0ce-591f45a317.zip/node_modules/@mui/utils/",\ "packageDependencies": [\ - ["@mui/utils", "virtual:d0fddc76688844d0b7cd5c451944107aff44e2467232119db2a9d551c0a8c9e3841cd11dab9c2020c9df2a66918c318fbf7e81eea8e61c4d31b38f2166fd0e82#npm:5.10.3"],\ + ["@mui/utils", "virtual:76f2160c62fb95c7d23125f08593d68f943cd5f55f8822c553758636aff4fca188039da42371d467d252c4a9c12188a4ca59a59bde7e07563860c8005c8bdb77#npm:5.10.3"],\ ["@babel/runtime", "npm:7.18.9"],\ ["@types/prop-types", "npm:15.7.5"],\ ["@types/react", "npm:18.0.21"],\ @@ -10709,21 +10707,21 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@mui/x-data-grid", [\ - ["npm:5.17.4", {\ - "packageLocation": "./.yarn/cache/@mui-x-data-grid-npm-5.17.4-542730a19e-9f88883fd9.zip/node_modules/@mui/x-data-grid/",\ + ["patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A.", {\ + "packageLocation": "./.yarn/cache/@mui-x-data-grid-patch-8cb0676cbf-0c31923597.zip/node_modules/@mui/x-data-grid/",\ "packageDependencies": [\ - ["@mui/x-data-grid", "npm:5.17.4"]\ + ["@mui/x-data-grid", "patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A."]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.17.4", {\ - "packageLocation": "./.yarn/__virtual__/@mui-x-data-grid-virtual-d0fddc7668/0/cache/@mui-x-data-grid-npm-5.17.4-542730a19e-9f88883fd9.zip/node_modules/@mui/x-data-grid/",\ + ["virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A.", {\ + "packageLocation": "./.yarn/__virtual__/@mui-x-data-grid-virtual-76f2160c62/0/cache/@mui-x-data-grid-patch-8cb0676cbf-0c31923597.zip/node_modules/@mui/x-data-grid/",\ "packageDependencies": [\ - ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.17.4"],\ + ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A."],\ ["@babel/runtime", "npm:7.18.9"],\ ["@mui/material", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ ["@mui/system", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ - ["@mui/utils", "virtual:d0fddc76688844d0b7cd5c451944107aff44e2467232119db2a9d551c0a8c9e3841cd11dab9c2020c9df2a66918c318fbf7e81eea8e61c4d31b38f2166fd0e82#npm:5.10.3"],\ + ["@mui/utils", "virtual:76f2160c62fb95c7d23125f08593d68f943cd5f55f8822c553758636aff4fca188039da42371d467d252c4a9c12188a4ca59a59bde7e07563860c8005c8bdb77#npm:5.10.3"],\ ["@types/mui__material", null],\ ["@types/mui__system", null],\ ["@types/react", "npm:18.0.21"],\ @@ -14097,15 +14095,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ - ["@types/seedrandom", [\ - ["npm:3.0.5", {\ - "packageLocation": "./.yarn/cache/@types-seedrandom-npm-3.0.5-b6a276228d-d63d56ebc6.zip/node_modules/@types/seedrandom/",\ - "packageDependencies": [\ - ["@types/seedrandom", "npm:3.0.5"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@types/semver", [\ ["npm:7.5.8", {\ "packageLocation": "./.yarn/cache/@types-semver-npm-7.5.8-26073743d7-ea6f5276f5.zip/node_modules/@types/semver/",\ @@ -27194,7 +27183,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@mui/material", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ ["@mui/styles", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ ["@mui/system", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.15.14"],\ - ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:5.17.4"],\ + ["@mui/x-data-grid", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A."],\ ["@mui/x-date-pickers", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:7.0.0-beta.7"],\ ["@next/bundle-analyzer", "npm:12.3.1"],\ ["@next/eslint-plugin-next", "npm:12.3.1"],\ @@ -27218,7 +27207,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/luxon", "npm:3.4.2"],\ ["@types/node", "npm:18.7.18"],\ ["@types/react", "npm:18.0.21"],\ - ["@types/seedrandom", "npm:3.0.5"],\ ["@types/testing-library__jest-dom", "npm:5.14.5"],\ ["@types/uuid", "npm:9.0.1"],\ ["@typescript-eslint/eslint-plugin", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:7.5.0"],\ @@ -27284,7 +27272,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-virtuoso", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:2.19.0"],\ ["recharts", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:2.3.2"],\ ["rollbar", "npm:2.25.2"],\ - ["seedrandom", "npm:3.0.5"],\ ["storybook", "npm:6.5.16"],\ ["storybook-addon-designs", "npm:6.3.1"],\ ["storybook-react-i18next", "virtual:9909ff5388c6b6a3a46f12eb37c0afb449fcd1eedb9f02d871bde711a076c929583f48ecc4b85fa6d71478b076104a25f83dee45bc69687a22f551c576d7595d#npm:1.1.2"],\ diff --git a/.yarn/cache/@mui-x-data-grid-patch-8cb0676cbf-0c31923597.zip b/.yarn/cache/@mui-x-data-grid-patch-8cb0676cbf-0c31923597.zip new file mode 100644 index 000000000..5118efaa4 Binary files /dev/null and b/.yarn/cache/@mui-x-data-grid-patch-8cb0676cbf-0c31923597.zip differ diff --git a/.yarn/cache/@types-seedrandom-npm-3.0.5-b6a276228d-d63d56ebc6.zip b/.yarn/cache/@types-seedrandom-npm-3.0.5-b6a276228d-d63d56ebc6.zip deleted file mode 100644 index 6197c0704..000000000 Binary files a/.yarn/cache/@types-seedrandom-npm-3.0.5-b6a276228d-d63d56ebc6.zip and /dev/null differ diff --git a/.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch b/.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch new file mode 100644 index 000000000..933eecfa8 --- /dev/null +++ b/.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch @@ -0,0 +1,13 @@ +diff --git a/components/panel/GridPanel.js b/components/panel/GridPanel.js +index e832b6b806fa0e0a40b79ba299917c81198daaf1..5c0551f98dfe7dc57bd7b168f8d1eb4d9f7e34d2 100644 +--- a/components/panel/GridPanel.js ++++ b/components/panel/GridPanel.js +@@ -76,7 +76,7 @@ const GridPanel = /*#__PURE__*/React.forwardRef((props, ref) => { + + return /*#__PURE__*/_jsx(GridPanelRoot, _extends({ + ref: ref, +- placement: "bottom-start", ++ placement: "top-start", + className: clsx(className, classes.panel), + anchorEl: anchorEl, + modifiers: modifiers diff --git a/__tests__/util/graphqlMocking.tsx b/__tests__/util/graphqlMocking.tsx index b3f4bd3d0..7bf8fc319 100644 --- a/__tests__/util/graphqlMocking.tsx +++ b/__tests__/util/graphqlMocking.tsx @@ -17,28 +17,35 @@ import { ergonomock, } from 'graphql-ergonomock'; import { DefaultMockResolvers } from 'graphql-ergonomock/dist/mock'; +import random from 'graphql-ergonomock/dist/utils/random'; import { gql } from 'graphql-tag'; -import seedrandom from 'seedrandom'; import { DeepPartial } from 'ts-essentials'; import schema from 'src/graphql/schema.graphql'; import { createCache } from 'src/lib/apollo/cache'; const seed = 'seed'; -const rng = seedrandom(seed); const resolvers: DefaultMockResolvers = { ISO8601DateTime: () => // Time in 2022 new Date( - 1641016800000 /* Jan 1, 2022 */ + Math.floor(rng() * 365 * 86400) * 1000, + 1641016800000 /* Jan 1, 2022 */ + random.integer(365 * 86400 * 1000), ).toISOString(), ISO8601Date: () => // Date in 2022 new Date( - 1641016800000 /* Jan 1, 2022 */ + Math.floor(rng() * 365) * 86400 * 1000, + 1641016800000 /* Jan 1, 2022 */ + random.integer(365 * 86400 * 1000), ) .toISOString() .slice(0, 10), + String: (_root, _args, _context, info) => { + if (info.fieldName.toLowerCase().endsWith('currency')) { + // Return a random valid currency + const currencies = ['USD', 'CAD', 'EUR']; + return currencies[random.integer(currencies.length)]; + } + return random.words(); + }, }; export const GqlMockedProvider = ({ diff --git a/package.json b/package.json index 34bcc6ab6..5c29e4886 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@mui/x-date-pickers": "^7.0.0-beta.7", "@react-google-maps/api": "^2.13.1", "@rollbar/react": "^0.11.1", - "@types/seedrandom": "^3.0.5", "apollo-datasource-rest": "^3.7.0", "apollo-server-micro": "^3.11.0", "apollo3-cache-persist": "^0.14.1", @@ -84,7 +83,6 @@ "react-virtuoso": "2.19.0", "recharts": "2.3.2", "rollbar": "^2.25.2", - "seedrandom": "^3.0.5", "storybook": "^6.5.16", "tslib": "^2.4.0", "tss-react": "^4.1.3", @@ -180,7 +178,8 @@ "package-json/got": "^11.8.5", "http-cache-semantics": "~4.1.1", "cacheable-request": "^10.2.7", - "@mui/base@5.0.0-alpha.98": "patch:@mui/base@npm%3A5.0.0-alpha.98#./.yarn/patches/@mui-base-npm-5.0.0-alpha.98-f4d605d753.patch" + "@mui/base@5.0.0-alpha.98": "patch:@mui/base@npm%3A5.0.0-alpha.98#./.yarn/patches/@mui-base-npm-5.0.0-alpha.98-f4d605d753.patch", + "@mui/x-data-grid@^5.17.4": "patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch" }, "lint-staged": { "*.{js,ts,tsx}": "eslint --cache --fix" diff --git a/src/components/Coaching/LoadCoachingList.test.tsx b/src/components/Coaching/LoadCoachingList.test.tsx index 466afd4e8..41b5d591a 100644 --- a/src/components/Coaching/LoadCoachingList.test.tsx +++ b/src/components/Coaching/LoadCoachingList.test.tsx @@ -23,77 +23,77 @@ describe('LoadCoaching', () => { "nodes": Array [ Object { "__typename": "CoachingAccountList", - "balance": 26.507845354363234, - "currency": "Kitchen Star Restaurant", + "balance": 64.55430394702351, + "currency": "CAD", "id": "7671408", - "monthlyGoal": 32, + "monthlyGoal": 12, "name": "Diamond Sword Restaurant", "primaryAppeal": Object { "__typename": "CoachingAppeal", "active": false, "amount": 71.24003484887974, - "amountCurrency": "Gloves Tennis racquet Pebble", - "id": "8018947", - "name": "Rifle Feather Videotape", - "pledgesAmountNotReceivedNotProcessed": 18.46232645104087, - "pledgesAmountProcessed": 12.197856859203839, - "pledgesAmountTotal": 64.55430394702351, + "amountCurrency": "EUR", + "id": "4377364", + "name": "Pebble Solid Triangle", + "pledgesAmountNotReceivedNotProcessed": 70.20389802275804, + "pledgesAmountProcessed": 36.11248127960118, + "pledgesAmountTotal": 96.17510597010335, }, - "receivedPledges": 71.71677620866463, - "totalPledges": 88.38895264618175, + "receivedPledges": 79.3262861684276, + "totalPledges": 50.50755229029304, }, Object { "__typename": "CoachingAccountList", - "balance": 7.701867972597047, - "currency": "Onion Pocket", - "id": "5702668", - "monthlyGoal": 67, - "name": "Circle", + "balance": 20.84220615982889, + "currency": "EUR", + "id": "8465485", + "monthlyGoal": 73, + "name": "Egg Compass Rocket", "primaryAppeal": Object { "__typename": "CoachingAppeal", "active": true, - "amount": 19.745889235377575, - "amountCurrency": "Bird Typewriter", - "id": "1270780", - "name": "Saddle Chisel Circle", - "pledgesAmountNotReceivedNotProcessed": 66.2952785316502, - "pledgesAmountProcessed": 18.199816964540847, - "pledgesAmountTotal": 72.72060834776661, + "amount": 57.02668130320204, + "amountCurrency": "CAD", + "id": "2268928", + "name": "Chess Board Mosquito", + "pledgesAmountNotReceivedNotProcessed": 11.708288466314501, + "pledgesAmountProcessed": 94.3774669075171, + "pledgesAmountTotal": 12.70780215832432, }, - "receivedPledges": 49.79264398185141, - "totalPledges": 54.25736018912712, + "receivedPledges": 22.600969051965148, + "totalPledges": 66.2952785316502, }, Object { "__typename": "CoachingAccountList", - "balance": 2.9079667684670696, - "currency": "Bible", - "id": "1688878", - "monthlyGoal": 23, - "name": "Onion Church Car", + "balance": 66.76403183825539, + "currency": "EUR", + "id": "1819981", + "monthlyGoal": 42, + "name": "Leather jacket Onion Pocket", "primaryAppeal": Object { "__typename": "CoachingAppeal", "active": true, - "amount": 42.290111917719415, - "amountCurrency": "Necklace Prison", - "id": "761729", - "name": "Software Drum", - "pledgesAmountNotReceivedNotProcessed": 5.751591941256687, - "pledgesAmountProcessed": 71.27233959202866, - "pledgesAmountTotal": 88.88260916797685, + "amount": 7.701867972597047, + "amountCurrency": "CAD", + "id": "5425736", + "name": "Snail", + "pledgesAmountNotReceivedNotProcessed": 60.6603194388847, + "pledgesAmountProcessed": 22.219772841760978, + "pledgesAmountTotal": 16.959686277380897, }, - "receivedPledges": 90.38398155210061, - "totalPledges": 54.133712713605604, + "receivedPledges": 59.9470658594177, + "totalPledges": 67.6666019807398, }, ], "pageInfo": Object { "__typename": "PageInfo", - "endCursor": "Circus Map", + "endCursor": "Arm Bible Circus", "hasNextPage": false, - "hasPreviousPage": false, - "startCursor": "Boy Wheelchair Data Base", + "hasPreviousPage": true, + "startCursor": "Drum Baby Rock", }, - "totalCount": 95, - "totalPageCount": 23, + "totalCount": 7, + "totalPageCount": 59, } `); }); diff --git a/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderPartnerSection.tsx b/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderPartnerSection.tsx index 54595f47b..3a2f6d7bc 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderPartnerSection.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsHeader/ContactHeaderSection/ContactHeaderPartnerSection.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { Skeleton, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; @@ -28,30 +28,20 @@ export const ContactHeaderPartnerSection: React.FC = ({ ); - } else { - if ( - contact !== null && - (contact?.contactDonorAccounts.nodes.length ?? 0) > 0 - ) { - return ( - - - {t('Partner Account')} + } else if (contact && contact.contactDonorAccounts.nodes.length) { + return ( + + + {t('Partner Account')} + + {contact?.contactDonorAccounts.nodes.map((donorAccount) => ( + + {donorAccount.donorAccount.displayName} - {contact?.contactDonorAccounts.nodes.map((donorAccount) => { - return ( - - - - {donorAccount.donorAccount.displayName} - - - ); - })} - - ); - } else { - return null; - } + ))} + + ); + } else { + return null; } }; diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.graphql b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.graphql deleted file mode 100644 index 1e7c2a6b2..000000000 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.graphql +++ /dev/null @@ -1,21 +0,0 @@ -query ContactDonationsList( - $accountListId: ID! - $contactId: ID! - $after: String -) { - contact(accountListId: $accountListId, id: $contactId) { - id - donations(first: 13, after: $after) { - nodes { - ...ContactDonation - ...EditDonationModalDonation - } - pageInfo { - endCursor - hasNextPage - hasPreviousPage - startCursor - } - } - } -} diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.stories.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.stories.tsx deleted file mode 100644 index c65561334..000000000 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { ReactElement } from 'react'; -import { DateTime } from 'luxon'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { ContactDonationsList } from './ContactDonationsList'; -import { ContactDonationsListQuery } from './ContactDonationsList.generated'; - -export default { - title: 'Contacts/Tab/ContactDonationsTab/ContactDonationsList', - component: ContactDonationsList, -}; - -const accountList = 'account-list-id'; -const contactId = 'contact-id'; - -export const Default = (): ReactElement => { - return ( - - mocks={{ - ContactDonationsList: { - donations: { - totalCount: 125, - nodes: [...Array(25)].map((x, i) => { - return { - donationDate: DateTime.local().minus({ month: i }).toISO, - amount: { - currency: 'USD', - convertedCurrency: 'EUR', - }, - }; - }), - }, - }, - }} - > - - - ); -}; diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.test.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.test.tsx deleted file mode 100644 index fcb83db67..000000000 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; -import { ThemeProvider } from '@mui/material/styles'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; -import userEvent from '@testing-library/user-event'; -import { SnackbarProvider } from 'notistack'; -import TestRouter from '__tests__/util/TestRouter'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { - render, - waitFor, - within, -} from '__tests__/util/testingLibraryReactMock'; -import { GetAccountListCurrencyQuery } from 'src/components/Reports/DonationsReport/GetDonationsTable.generated'; -import theme from 'src/theme'; -import { ContactDonationsList } from './ContactDonationsList'; -import { ContactDonationsListQuery } from './ContactDonationsList.generated'; - -const accountListId = 'account-list-1'; -const contactId = 'contact-id-1'; - -const router = { - query: { accountListId }, - isReady: true, -}; - -interface TestComponentProps { - foreignCurrencies?: boolean; -} - -const TestComponent: React.FC = ({ - foreignCurrencies = true, -}) => ( - - - - - mocks={{ - ContactDonationsList: { - contact: { - id: contactId, - donations: { - nodes: [...Array(5)].map(() => ({ - amount: { - currency: foreignCurrencies ? 'EUR' : 'USD', - convertedCurrency: 'USD', - amount: 10, - convertedAmount: 9.9, - }, - appeal: { - name: 'EOY Ask', - }, - })), - pageInfo: { - hasNextPage: true, - }, - }, - }, - }, - GetAccountListCurrency: { - accountList: { - currency: 'USD', - }, - }, - }} - > - - - - - - - -); - -describe('ContactDonationsList', () => { - it('renders donations', async () => { - const { findAllByRole, getByRole } = render(); - - const rows = await findAllByRole('row'); - expect(rows).toHaveLength(6); - expect( - getByRole('columnheader', { name: 'Converted Amount' }), - ).toBeInTheDocument(); - - const donationRow = rows[1]; - expect(donationRow.children[1]).toHaveTextContent('€10'); - expect(donationRow.children[2]).toHaveTextContent('$9.90'); - expect( - within(donationRow).getByRole('cell', { name: 'EOY Ask' }), - ).toBeInTheDocument(); - }); - - it('hides converted currency column when it is redundant', async () => { - const { queryByRole, findAllByRole } = render( - , - ); - - const rows = await findAllByRole('row'); - expect(rows).toHaveLength(6); - - expect( - queryByRole('columnheader', { name: 'Converted Amount' }), - ).not.toBeInTheDocument(); - - const donationRow = rows[1]; - expect(donationRow.children).toHaveLength(5); - }); - - it('loads more donations', async () => { - const { findByRole, getAllByRole } = render(); - - userEvent.click(await findByRole('button', { name: 'Load More' })); - await waitFor(() => expect(getAllByRole('row')).toHaveLength(11)); - }); - - it('edits donations', async () => { - const { findAllByRole, findByText } = render(); - - const rows = await findAllByRole('row'); - expect(rows).toHaveLength(6); - - const donationRow = rows[1]; - userEvent.click(within(donationRow).getByRole('button')); - expect(await findByText('Edit Donation')).toBeInTheDocument(); - }); -}); diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.tsx deleted file mode 100644 index 0b9cb85bc..000000000 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsList/ContactDonationsList.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useState } from 'react'; -import EditIcon from '@mui/icons-material/Edit'; -import { - Button, - IconButton, - Skeleton, - Table, - TableBody, - TableCell, - TableHead, - TableRow, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { DateTime } from 'luxon'; -import { useTranslation } from 'react-i18next'; -import { - DynamicEditDonationModal, - preloadEditDonationModal, -} from 'src/components/EditDonationModal/DynamicEditDonationModal'; -import { EditDonationModalDonationFragment } from 'src/components/EditDonationModal/EditDonationModal.generated'; -import { useGetAccountListCurrencyQuery } from 'src/components/Reports/DonationsReport/GetDonationsTable.generated'; -import { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; -import { useContactDonationsListQuery } from './ContactDonationsList.generated'; - -interface ContactDonationsListProp { - accountListId: string; - contactId: string; -} - -const LoadMoreButton = styled(Button)(({ theme }) => ({ - margin: theme.spacing(1), -})); - -const DonationLoadingPlaceHolder = styled(Skeleton)(({ theme }) => ({ - width: '100%', - height: '24px', - margin: theme.spacing(2, 0), -})); - -export const ContactDonationsList: React.FC = ({ - accountListId, - contactId, -}) => { - const { data, loading, fetchMore } = useContactDonationsListQuery({ - variables: { - accountListId: accountListId, - contactId: contactId, - }, - }); - const [editingDonation, setEditingDonation] = - useState(null); - - const { data: accountListData } = useGetAccountListCurrencyQuery({ - variables: { - accountListId, - }, - }); - - const hasForeignCurrencies = - accountListData && - data?.contact.donations.nodes.some( - (donation) => - donation.amount.currency !== accountListData.accountList.currency, - ); - - const { t } = useTranslation(); - const locale = useLocale(); - - return ( - <> - {loading && !data ? ( - <> - - - - - ) : ( - <> - - - - {t('Date')} - {t('Amount')} - {hasForeignCurrencies && ( - {t('Converted Amount')} - )} - {t('Method')} - {t('Appeal')} - - - - - {data?.contact.donations.nodes.map((donation) => ( - - - {dateFormat( - DateTime.fromISO(donation.donationDate), - locale, - )} - - - {currencyFormat( - donation.amount.amount, - donation.amount.currency, - locale, - )} - - {hasForeignCurrencies && ( - - {currencyFormat( - donation.amount.convertedAmount, - donation.amount.convertedCurrency, - locale, - )} - - )} - {donation.paymentMethod} - {donation.appeal?.name} - - { - setEditingDonation(donation); - }} - onMouseEnter={preloadEditDonationModal} - > - - - - - ))} - -
- {!loading && data?.contact.donations.pageInfo.hasNextPage ? ( - { - fetchMore({ - variables: { - after: data.contact.donations.pageInfo.endCursor, - }, - }); - }} - > - {t('Load More')} - - ) : null} - - )} - {editingDonation && ( - setEditingDonation(null)} - /> - )} - - ); -}; diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.test.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.test.tsx index d7f6c006f..433167dca 100644 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.test.tsx +++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.test.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; import { DateTime } from 'luxon'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render } from '__tests__/util/testingLibraryReactMock'; +import theme from 'src/theme'; import { ContactDetailProvider } from '../ContactDetailContext'; import { ContactDonationsTab } from './ContactDonationsTab'; import { GetContactDonationsQuery } from './ContactDonationsTab.generated'; @@ -12,27 +14,29 @@ const contactId = 'contact-id-1'; describe('ContactDonationsTab', () => { it('test renderer', async () => { const { findByRole } = render( - - mocks={{ - GetContactDonations: { - contact: { - nextAsk: DateTime.now().plus({ month: 5 }).toISO(), - pledgeStartDate: DateTime.now().minus({ month: 5 }).toISO(), - pledgeCurrency: 'USD', - lastDonation: { - donationDate: DateTime.now().toISO(), + + + mocks={{ + GetContactDonations: { + contact: { + nextAsk: DateTime.now().plus({ month: 5 }).toISO(), + pledgeStartDate: DateTime.now().minus({ month: 5 }).toISO(), + pledgeCurrency: 'USD', + lastDonation: { + donationDate: DateTime.now().toISO(), + }, }, }, - }, - }} - > - - - - , + }} + > + + + + + , ); expect(await findByRole('region')).toBeVisible(); }); diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.tsx index 78ef0b8c9..7c8b7d3a6 100644 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.tsx +++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/ContactDonationsTab.tsx @@ -5,11 +5,12 @@ import TabPanel from '@mui/lab/TabPanel'; import { Box, Skeleton, Tab } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next/'; +import { DonationTable } from 'src/components/DonationTable/DonationTable'; +import { EmptyDonationsTable } from 'src/components/common/EmptyDonationsTable/EmptyDonationsTable'; import { ContactDetailContext, ContactDetailsType, } from '../ContactDetailContext'; -import { ContactDonationsList } from './ContactDonationsList/ContactDonationsList'; import { GetContactDonationsQueryVariables, useGetContactDonationsQuery, @@ -23,12 +24,9 @@ const ContactDonationsContainer = styled(Box)(({ theme }) => ({ })); const DonationsTabContainer = styled(Box)(({ theme }) => ({ - margin: theme.spacing(1), + marginBottom: theme.spacing(2), background: theme.palette.background.paper, -})); - -const DonationsGraphContainer = styled(Box)(({ theme }) => ({ - margin: theme.spacing(1), + borderBottom: '1px solid #DCDCDC', })); const DonationsTabList = styled(TabList)(({}) => ({ @@ -45,12 +43,16 @@ const DonationsTab = styled(Tab)(({ theme }) => ({ '&:hover': { opacity: 1 }, })); -const ContactDonationsLoadingPlaceHolder = styled(Skeleton)(({ theme }) => ({ - width: '100%', +const PartnershipInfoLoadingPlaceHolder = styled(Skeleton)(({ theme }) => ({ + width: '20em', height: '24px', margin: theme.spacing(2, 0), })); +const StyledTabPanel = styled(TabPanel)({ + padding: 0, +}); + interface ContactDonationsProp { accountListId: string; contactId: string; @@ -71,6 +73,9 @@ export const ContactDonationsTab: React.FC = ({ contactId: contactId, }, }); + const donorAccountIds = data?.contact.contactDonorAccounts.nodes.map( + (donor) => donor.donorAccount.id, + ); const { t } = useTranslation(); @@ -85,27 +90,11 @@ export const ContactDonationsTab: React.FC = ({ }; return ( - - {loading ? ( - <> - - - - - ) : ( - { - return donor.donorAccount.id; - }) ?? [] - } - convertedCurrency={ - data?.contact.lastDonation?.amount.convertedCurrency ?? '' - } - /> - )} - + = ({ /> - - {loading ? ( - <> - - - - - ) : ( - - )} - - + + + } + visibleColumnsStorageKey="contact-donations" + /> + + {loading ? ( - <> - - - - + new Array(10) + .fill(null) + .map((_, index) => ( + + )) ) : ( )} - + ); diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx index 8938b5b70..69f608603 100644 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx +++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.test.tsx @@ -1,21 +1,45 @@ import React from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react-hooks'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render } from '__tests__/util/testingLibraryReactMock'; +import { + afterTestResizeObserver, + beforeTestResizeObserver, +} from '__tests__/util/windowResizeObserver'; import { DonationsGraph } from './DonationsGraph'; import { - GetDonationsGraphDocument, GetDonationsGraphQuery, useGetDonationsGraphQuery, } from './DonationsGraph.generated'; +// ResponsiveContainer isn't rendering its children in tests, so override it to render its children with static dimensions +jest.mock('recharts', () => ({ + ...jest.requireActual('recharts'), + ResponsiveContainer: ({ children }) => ({ + ...children, + props: { + ...children.props, + height: 600, + width: 300, + }, + }), +})); + const accountListId = 'account-list-id'; const donorAccountIds = ['donor-Account-Id']; const currency = 'USD'; + describe('Donations Graph', () => { + beforeEach(() => { + beforeTestResizeObserver(); + }); + + afterEach(() => { + afterTestResizeObserver(); + }); + it('test renderer', async () => { - const { findByRole } = render( + const { findByText } = render( mocks={{ GetDonationsGraph: { @@ -43,35 +67,33 @@ describe('Donations Graph', () => { /> , ); - expect(await findByRole('textbox')).toBeVisible(); + expect(await findByText('Amount (USD)')).toBeInTheDocument(); }); - it('test loading renderer', async () => { - const { findByRole } = render( - + it('renders loading placeholders while loading data', () => { + const { getByLabelText } = render( + - , + , + ); + expect(getByLabelText('Loading donations graph')).toBeInTheDocument(); + }); + + it('renders loading placeholders while waiting for donorAccountIds', async () => { + const { getByLabelText } = render( + + + , ); - expect(await findByRole('alert')).toBeVisible(); + expect(getByLabelText('Loading donations graph')).toBeInTheDocument(); }); it('test query', async () => { diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx index 70b09f60e..2c26ee3b8 100644 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx +++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.tsx @@ -8,36 +8,28 @@ import { BarChart, CartesianGrid, Legend, + ResponsiveContainer, + Text, Tooltip, XAxis, YAxis, } from 'recharts'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat } from 'src/lib/intlFormat'; -import theme from '../../../../../theme'; +import theme from 'src/theme'; import { useGetDonationsGraphQuery } from './DonationsGraph.generated'; -const LegendText = styled(Typography)(({ theme }) => ({ - margin: theme.spacing(3, 0), - writingMode: 'vertical-rl', - textOrientation: 'mixed', -})); - const GraphContainer = styled(Box)(({ theme }) => ({ - display: 'flex', - margin: theme.spacing(0, 2, 0, 0), -})); - -const GraphLoadingPlaceHolder = styled(Skeleton)(({ theme }) => ({ - width: '400', - height: '24px', - margin: theme.spacing(2, 0), + height: 300, + marginBottom: theme.spacing(2), + padding: theme.spacing(1), + overflowX: 'scroll', })); interface DonationsGraphProps { accountListId: string; - donorAccountIds: string[]; - convertedCurrency: string; + donorAccountIds: string[] | undefined; + convertedCurrency: string | undefined; } export const DonationsGraph: React.FC = ({ @@ -47,11 +39,13 @@ export const DonationsGraph: React.FC = ({ }) => { const { t } = useTranslation(); const locale = useLocale(); + const skipped = !donorAccountIds; const { data, loading } = useGetDonationsGraphQuery({ variables: { accountListId: accountListId, donorAccountIds: donorAccountIds, }, + skip: skipped, }); const monthFormatter = useMemo( @@ -106,22 +100,26 @@ export const DonationsGraph: React.FC = ({ })} )} - - {loading ? ( - - - - - + + {loading || skipped ? ( + ) : ( - <> - - {t('Amount ({{amount}})', { amount: convertedCurrency })} - - + + - + + {t('Amount ({{amount}})', { amount: convertedCurrency })} + + } + /> = ({ fill={theme.palette.secondary.main} /> - + )} diff --git a/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx b/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx index 0b3700b56..62b195220 100644 --- a/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx +++ b/src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/PartnershipInfo.tsx @@ -41,10 +41,9 @@ const IconContainer = styled(Box)(({ theme }) => ({ margin: theme.spacing(0), })); -const PartnershipInfoContainer = styled(Box)(({ theme }) => ({ +const PartnershipInfoContainer = styled(Box)({ width: '100%', - margin: theme.spacing(1), -})); +}); const PartnershipTitle = styled(Typography)(({ theme }) => ({ margin: theme.spacing(1), diff --git a/src/components/Reports/DonationsReport/GetDonationsTable.graphql b/src/components/DonationTable/DonationTable.graphql similarity index 87% rename from src/components/Reports/DonationsReport/GetDonationsTable.graphql rename to src/components/DonationTable/DonationTable.graphql index 41019a0e6..eea5fb623 100644 --- a/src/components/Reports/DonationsReport/GetDonationsTable.graphql +++ b/src/components/DonationTable/DonationTable.graphql @@ -1,14 +1,16 @@ -query GetDonationsTable( +query DonationTable( $accountListId: ID! $pageSize: Int! $after: String $startDate: ISO8601Date $endDate: ISO8601Date + $donorAccountIds: [ID!] $designationAccountIds: [ID!] ) { donations( accountListId: $accountListId donationDate: { max: $endDate, min: $startDate } + donorAccountId: $donorAccountIds designationAccountId: $designationAccountIds first: $pageSize after: $after @@ -54,7 +56,7 @@ fragment DonationTableRow on Donation { paymentMethod } -query GetAccountListCurrency($accountListId: ID!) { +query AccountListCurrency($accountListId: ID!) { accountList(id: $accountListId) { id currency diff --git a/src/components/DonationTable/DonationTable.test.tsx b/src/components/DonationTable/DonationTable.test.tsx new file mode 100644 index 000000000..4dc70fdb1 --- /dev/null +++ b/src/components/DonationTable/DonationTable.test.tsx @@ -0,0 +1,285 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { render, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SnackbarProvider } from 'notistack'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import theme from 'src/theme'; +import { DonationTable, DonationTableProps } from './DonationTable'; +import { + AccountListCurrencyQuery, + DonationTableQuery, +} from './DonationTable.generated'; + +const onSelectContact = jest.fn(); +const mutationSpy = jest.fn(); + +const router = { + query: { accountListId: 'account-list-1' }, + isReady: true, +}; + +interface TestComponentProps { + isEmpty?: boolean; + hasForeignCurrency?: boolean; + hasMultiplePages?: boolean; + tableProps?: Partial; +} + +const TestComponent: React.FC = ({ + isEmpty = false, + hasForeignCurrency = false, + hasMultiplePages = false, + tableProps, +}) => ( + + + + + + mocks={{ + AccountListCurrency: { + accountList: { + currency: 'CAD', + }, + }, + DonationTable: { + currency: 'CAD', + donations: { + nodes: isEmpty + ? [] + : [ + { + id: 'donation-1', + amount: { + amount: 10, + convertedAmount: 10, + convertedCurrency: 'CAD', + currency: 'CAD', + }, + appeal: { + name: 'Appeal 1', + }, + donationDate: '2023-03-01', + donorAccount: { + contacts: { + nodes: [{ id: 'contact-1' }], + }, + displayName: 'Donor 1', + }, + paymentMethod: 'Check', + }, + { + id: 'donation-2', + amount: { + amount: hasForeignCurrency ? 200 : 100, + convertedAmount: 100, + convertedCurrency: 'CAD', + currency: hasForeignCurrency ? 'USD' : 'CAD', + }, + appeal: null, + donationDate: '2023-03-02', + donorAccount: { + contacts: { + nodes: [], + }, + displayName: 'Donor 2', + }, + paymentMethod: 'Credit Card', + }, + ], + pageInfo: { + endCursor: 'cursor', + hasNextPage: hasMultiplePages, + }, + }, + }, + }} + onCall={mutationSpy} + > + Empty Table} + {...tableProps} + /> + + + + + +); + +describe('DonationTable', () => { + it('renders with data', async () => { + const { getByRole, findByRole } = render(); + + expect(await findByRole('cell', { name: 'Donor 1' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'Donor 2' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'CA$10' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'CA$100' })).toBeInTheDocument(); + expect(getByRole('cell', { name: '3/1/2023' })).toBeInTheDocument(); + expect(getByRole('cell', { name: '3/2/2023' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'Check' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'Credit Card' })).toBeInTheDocument(); + expect(getByRole('cell', { name: 'Appeal 1' })).toBeInTheDocument(); + }); + + it('opens and closes the edit donation modal', async () => { + const { findByRole, findByText, queryByText, getByTestId, getByRole } = + render(); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('DonationTable', { + designationAccountIds: ['designation-1'], + }), + ); + + expect(await findByRole('cell', { name: 'Donor 1' })).toBeInTheDocument(); + expect(queryByText('Edit Donation')).not.toBeInTheDocument(); + + userEvent.click(getByTestId('edit-donation-1')); + expect(await findByText('Edit Donation')).toBeInTheDocument(); + + userEvent.click(getByRole('button', { name: 'Cancel' })); + expect(queryByText('Edit Donation')).not.toBeInTheDocument(); + }); + + it('renders loading spinner when loading prop is true', async () => { + const { findByTestId } = render( + , + ); + + expect(await findByTestId('LoadingBox')).toBeInTheDocument(); + await waitFor(() => + expect(mutationSpy).not.toHaveGraphqlOperation('DonationTable'), + ); + }); + + it('renders empty', async () => { + const { findByText } = render(); + + expect(await findByText('Empty Table')).toBeInTheDocument(); + }); + + it('is clickable', async () => { + const { findByText } = render(); + + const link = await findByText('Donor 1'); + expect(link).toHaveClass('MuiLink-root'); + userEvent.click(link); + expect(onSelectContact).toHaveBeenCalledWith('contact-1'); + }); + + it('is not clickable', async () => { + const { findByText } = render(); + + userEvent.click(await findByText('Donor 1')); + expect(onSelectContact).toHaveBeenCalledWith('contact-1'); + }); + + it('is not a link when onSelectContact is not provided', async () => { + const { findByText } = render( + , + ); + + expect(await findByText('Donor 1')).not.toHaveClass('MuiLink-root'); + expect(onSelectContact).not.toHaveBeenCalled(); + }); + + it('hides currency column when all currencies match the account currency', async () => { + const { queryByRole, findByRole } = render(); + + expect(await findByRole('cell', { name: 'Donor 1' })).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: 'Foreign Amount' }), + ).not.toBeInTheDocument(); + + const totalRow = within( + await findByRole('table', { name: 'Donation Totals' }), + ).getByRole('row'); + expect(totalRow.children[0]).toHaveTextContent('Total Donations:'); + expect(totalRow.children[1]).toHaveTextContent('CA$110'); + }); + + it('shows currency column and additional total rows when a currency does not match the account currency', async () => { + const { findByRole } = render(); + + expect( + await findByRole('columnheader', { name: 'Foreign Amount' }), + ).toBeInTheDocument(); + + const totalsRows = within( + await findByRole('table', { name: 'Donation Totals' }), + ).getAllByRole('row'); + expect(totalsRows).toHaveLength(4); + expect(totalsRows[1].children[0]).toHaveTextContent('Total CAD Donations:'); + expect(totalsRows[1].children[1]).toHaveTextContent('CA$10'); + expect(totalsRows[1].children[2]).toHaveTextContent('CA$10'); + expect(totalsRows[2].children[0]).toHaveTextContent('Total USD Donations:'); + expect(totalsRows[2].children[1]).toHaveTextContent('CA$100'); + expect(totalsRows[2].children[2]).toHaveTextContent('$200'); + expect(totalsRows[3].children[0]).toHaveTextContent('Total Donations:'); + expect(totalsRows[3].children[1]).toHaveTextContent('CA$110'); + }); + + it('updates the sort order', async () => { + const { findByRole, getAllByRole } = render(); + + const dateHeader = await findByRole('columnheader', { name: 'Date' }); + expect( + within(dateHeader).getByTestId('ArrowDownwardIcon'), + ).toBeInTheDocument(); + + userEvent.click(await findByRole('columnheader', { name: 'Amount' })); + const cellsAsc = getAllByRole('cell', { name: /CA/ }); + expect(cellsAsc[0]).toHaveTextContent('CA$10'); + expect(cellsAsc[1]).toHaveTextContent('CA$100'); + + userEvent.click(await findByRole('columnheader', { name: 'Amount' })); + const cellsDesc = getAllByRole('cell', { name: /CA/ }); + expect(cellsDesc[0]).toHaveTextContent('CA$100'); + expect(cellsDesc[1]).toHaveTextContent('CA$10'); + }); + + it('loads multiple pages and shows the progress bar', async () => { + const { findByRole, queryByRole } = render( + , + ); + + expect(await findByRole('progressbar')).toBeInTheDocument(); + expect( + queryByRole('table', { name: 'Donation Totals' }), + ).not.toBeInTheDocument(); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('DonationTable', { + designationAccountIds: ['designation-1'], + after: 'cursor', + }), + ); + }); + + it('updates the page size without reloading the donations', async () => { + const { findByRole, getByRole } = render(); + + userEvent.click(await findByRole('combobox', { name: 'Rows per page:' })); + userEvent.click(getByRole('option', { name: '50' })); + + await waitFor(() => + expect(mutationSpy).not.toHaveGraphqlOperation('DonationTable', { + pageSize: 50, + }), + ); + }); +}); diff --git a/src/components/DonationTable/DonationTable.tsx b/src/components/DonationTable/DonationTable.tsx new file mode 100644 index 000000000..c8064210b --- /dev/null +++ b/src/components/DonationTable/DonationTable.tsx @@ -0,0 +1,418 @@ +import React, { useMemo, useState } from 'react'; +import EditIcon from '@mui/icons-material/Edit'; +import { + Box, + CircularProgress, + LinearProgress, + Link, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, +} from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import { styled } from '@mui/material/styles'; +import { + DataGrid, + GridColDef, + GridColumnVisibilityModel, + GridSortModel, +} from '@mui/x-data-grid'; +import { DateTime } from 'luxon'; +import { useTranslation } from 'react-i18next'; +import { useFetchAllPages } from 'src/hooks/useFetchAllPages'; +import { useLocalStorage } from 'src/hooks/useLocalStorage'; +import { useLocale } from 'src/hooks/useLocale'; +import { currencyFormat, dateFormatShort } from 'src/lib/intlFormat'; +import { + DynamicEditDonationModal, + preloadEditDonationModal, +} from '../EditDonationModal/DynamicEditDonationModal'; +import { + DonationTableQueryVariables, + DonationTableRowFragment, + useAccountListCurrencyQuery, + useDonationTableQuery, +} from './DonationTable.generated'; + +type RenderCell = GridColDef['renderCell']; + +export interface DonationTableProps { + accountListId: string; + filter: Partial; + loading?: boolean; + onSelectContact?: (contactId: string) => void; + visibleColumnsStorageKey: string; + emptyPlaceholder: React.ReactElement; +} + +const StyledGrid = styled(DataGrid)(({ theme }) => ({ + '.MuiDataGrid-row:nth-of-type(2n + 1):not(:hover)': { + backgroundColor: theme.palette.cruGrayLight.main, + }, + '.MuiDataGrid-cell': { + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }, +})); + +const TotalsTable = styled(Table)({ + '.MuiTableCell-root': { + fontWeight: 'bold', + }, + + '.MuiTableRow-root .MuiTableCell-root:nth-of-type(1)': { + textAlign: 'right', + }, +}); + +const LoadingProgressBar = styled(LinearProgress)(({ theme }) => ({ + height: '0.5rem', + borderRadius: theme.shape.borderRadius, + ['& .MuiLinearProgress-bar']: { + borderRadius: theme.shape.borderRadius, + }, +})); + +const LoadingBox = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.cruGrayLight.main, + height: 300, + minWidth: 700, + margin: 'auto', + padding: 4, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +})); + +const LoadingIndicator = styled(CircularProgress)(({ theme }) => ({ + margin: theme.spacing(0, 1, 0, 0), +})); + +interface DonationRow { + id: string; + date: DateTime; + contactId: string | null; + donorAccountName: string; + currency: string; + foreignCurrency: string; + convertedAmount: number; + foreignAmount: number; + designationAccount: string; + paymentMethod: string | null; + appealName: string | null; + rawDonation: DonationTableRowFragment; +} + +const createDonationRow = (data: DonationTableRowFragment): DonationRow => ({ + id: data.id, + date: DateTime.fromISO(data.donationDate), + contactId: data.donorAccount.contacts.nodes[0]?.id ?? null, + donorAccountName: data.donorAccount.displayName, + convertedAmount: data.amount.convertedAmount, + currency: data.amount.convertedCurrency, + foreignAmount: data.amount.amount, + foreignCurrency: data.amount.currency, + designationAccount: data.designationAccount.name, + paymentMethod: data.paymentMethod ?? null, + appealName: data.appeal?.name ?? null, + rawDonation: data, +}); + +export const DonationTable: React.FC = ({ + accountListId, + filter, + loading: skipped = false, + onSelectContact, + visibleColumnsStorageKey, + emptyPlaceholder, +}) => { + const { t } = useTranslation(); + const locale = useLocale(); + const [editingDonation, setEditingDonation] = useState( + null, + ); + + const handleClose = () => { + setEditingDonation(null); + }; + + const [pageSize, setPageSize] = useState(25); + + // pageSize is intentionally omitted from the dependencies array so that the query isn't rerun when the page size changes + // If all the pages have loaded and the user changes the page size, there's no reason to reload all the pages + // TODO: sort donations on the server after https://jira.cru.org/browse/MPDX-7634 is implemented + const variables: DonationTableQueryVariables = useMemo( + () => ({ + accountListId, + pageSize, + ...filter, + }), + [accountListId, filter], + ); + const { data, error, loading, fetchMore } = useDonationTableQuery({ + variables, + skip: skipped, + }); + // Load the rest of the pages asynchronously so that we can calculate the total donations + useFetchAllPages({ + fetchMore, + error, + pageInfo: data?.donations.pageInfo, + }); + + const { data: accountListData, loading: loadingAccountListData } = + useAccountListCurrencyQuery({ + variables: { accountListId }, + }); + + const nodes = data?.donations.nodes || []; + + const accountCurrency = accountListData?.accountList.currency || 'USD'; + + const donations = useMemo(() => nodes.map(createDonationRow), [nodes]); + + const date: RenderCell = ({ row }) => dateFormatShort(row.date, locale); + + const donor: RenderCell = ({ row }) => ( + + {onSelectContact ? ( + row.contactId && onSelectContact(row.contactId)} + > + {row.donorAccountName} + + ) : ( + {row.donorAccountName} + )} + + ); + + const amount: RenderCell = ({ row }) => + currencyFormat(row.convertedAmount, row.currency, locale); + + const foreignAmount: RenderCell = ({ row }) => + currencyFormat(row.foreignAmount, row.foreignCurrency, locale); + + const designationAccount: RenderCell = ({ row }) => ( + + {row.designationAccount} + + ); + + const appeal: RenderCell = ({ row: donation }) => donation.appealName; + + const edit: RenderCell = ({ row: donation }) => ( + { + setEditingDonation(donation); + }} + onMouseEnter={preloadEditDonationModal} + > + + + ); + + const columns: GridColDef[] = [ + { + field: 'date', + headerName: t('Date'), + flex: 1, + minWidth: 80, + renderCell: date, + }, + { + field: 'donorAccountName', + headerName: t('Partner'), + flex: 3, + minWidth: 200, + renderCell: donor, + }, + { + field: 'convertedAmount', + headerName: t('Amount'), + flex: 1, + minWidth: 120, + renderCell: amount, + }, + { + field: 'foreignAmount', + headerName: t('Foreign Amount'), + flex: 1.5, + minWidth: 120, + renderCell: foreignAmount, + hideable: false, + }, + { + field: 'designationAccount', + headerName: t('Designation'), + flex: 3, + minWidth: 200, + renderCell: designationAccount, + }, + { + field: 'paymentMethod', + headerName: t('Method'), + flex: 1, + minWidth: 100, + }, + { + field: 'appealName', + headerName: t('Appeal'), + flex: 1, + minWidth: 100, + renderCell: appeal, + }, + { + field: 'Edit', + headerName: t('Edit'), + width: 40, + renderCell: edit, + hideable: false, + }, + ]; + + const [sortModel, setSortModel] = useState([ + { field: 'date', sort: 'desc' }, + ]); + + const hasForeignDonations = donations.some( + (donation) => donation.foreignCurrency !== accountCurrency, + ); + + const totalDonations = donations.reduce( + (total, current) => total + current.convertedAmount, + 0, + ); + + const totalForeignDonations = donations.reduce( + ( + acc: Record, + donation, + ) => { + const { foreignCurrency, foreignAmount, convertedAmount } = donation; + acc[foreignCurrency] = { + convertedTotal: + convertedAmount + (acc[foreignCurrency]?.convertedTotal ?? 0), + foreignTotal: + foreignAmount + (acc[foreignCurrency]?.convertedTotal ?? 0), + }; + return acc; + }, + {}, + ); + + const loadedPercentage = data + ? (data.donations.nodes.length / (data.donations.totalCount || 1)) * 100 + : 0; + const [columnVisibility, setColumnVisibility] = + useLocalStorage( + `donation-table-visible-columns-${visibleColumnsStorageKey}`, + { + partner: typeof filter.donorAccountIds === 'undefined', + }, + ); + + return ( + <> + {data?.donations.nodes.length ? ( + <> + setPageSize(pageSize)} + rowsPerPageOptions={[25, 50, 100]} + pagination + sortModel={sortModel} + onSortModelChange={(sortModel) => setSortModel(sortModel)} + autoHeight + disableSelectionOnClick + disableVirtualization + /> + {data.donations.pageInfo.hasNextPage ? ( + + + + ) : ( + + {hasForeignDonations && ( + + + + {t('Amount')} + {t('Foreign Amount')} + + + )} + + {hasForeignDonations && + Object.entries(totalForeignDonations).map( + ([currency, total]) => ( + + + {t('Total {{currency}} Donations:', { currency })} + + + {currencyFormat( + total.convertedTotal, + accountCurrency, + locale, + )} + + + {currencyFormat(total.foreignTotal, currency, locale)} + + + ), + )} + + {/* Best-effort attempt to line up the columns with the DataGrid when the column visibility isn't customized */} + + {t('Total Donations: ')} + + + {currencyFormat(totalDonations, accountCurrency, locale)} + + + + + + )} + + ) : loadingAccountListData || loading || skipped ? ( + + + + ) : ( + emptyPlaceholder + )} + {editingDonation && ( + handleClose()} + /> + )} + + ); +}; diff --git a/src/components/Layouts/SidePanelsLayout.tsx b/src/components/Layouts/SidePanelsLayout.tsx index 90edde9ad..6ae7716b2 100644 --- a/src/components/Layouts/SidePanelsLayout.tsx +++ b/src/components/Layouts/SidePanelsLayout.tsx @@ -49,8 +49,6 @@ const ExpandingContent = styled(Box)(({ open }: { open: boolean }) => ({ flexBasis: open ? 0 : '100%', transition: 'flex-basis ease-in-out 225ms', overflowX: 'hidden', - position: 'relative', - zIndex: 10, })); const LeftPanelWrapper = styled(FullHeightBox)(({ theme }) => ({ @@ -62,7 +60,7 @@ const LeftPanelWrapper = styled(FullHeightBox)(({ theme }) => ({ transition: 'transform ease-in-out 225ms', background: theme.palette.common.white, position: 'absolute', - zIndex: 20, + zIndex: 720, // Must be higher than RightPanelWrapper }, })); @@ -70,7 +68,7 @@ const RightPanelWrapper = styled(FullHeightBox)(({ theme, headerHeight }) => { const toolbar = theme.mixins.toolbar as ToolbarMixin; return { position: 'fixed', - zIndex: 20, + zIndex: 710, // Must be higher than MultiPageHeader's StickyHeader right: 0, transition: 'transform ease-in-out 225ms', overflowY: 'scroll', @@ -115,11 +113,22 @@ export const SidePanelsLayout: FC = ({ headerHeight = '0px', }) => { const isMobile = useMediaQuery((theme: Theme) => - theme.breakpoints.down('sm'), + theme.breakpoints.down('md'), ); return ( + + {rightOpen && rightPanel} + = ({ {mainContent} - - {rightOpen && rightPanel} - ); }; diff --git a/src/components/Reports/DonationsReport/DonationsReport.test.tsx b/src/components/Reports/DonationsReport/DonationsReport.test.tsx index 6ea148a50..dd2bcccc3 100644 --- a/src/components/Reports/DonationsReport/DonationsReport.test.tsx +++ b/src/components/Reports/DonationsReport/DonationsReport.test.tsx @@ -9,10 +9,10 @@ import { afterTestResizeObserver, beforeTestResizeObserver, } from '__tests__/util/windowResizeObserver'; -import theme from '../../../theme'; +import { DonationTableQuery } from 'src/components/DonationTable/DonationTable.generated'; +import theme from 'src/theme'; import { GetDonationsGraphQuery } from '../../Contacts/ContactDetails/ContactDonationsTab/DonationsGraph/DonationsGraph.generated'; import { DonationsReport } from './DonationsReport'; -import { GetDonationsTableQuery } from './GetDonationsTable.generated'; const title = 'test title'; const onNavListToggle = jest.fn(); @@ -59,27 +59,17 @@ const mocks = { ], }, }, - GetDonationsTable: { + DonationTable: { donations: { nodes: [ { amount: { - amount: 10, - convertedAmount: 10, convertedCurrency: 'CAD', currency: 'CAD', }, - appeal: { - id: 'abc', - name: 'John', - }, - donationDate: DateTime.now().minus({ minutes: 4 }).toISO(), donorAccount: { displayName: 'John', - id: 'abc', }, - id: 'abc', - paymentMethod: 'pay', }, ], pageInfo: { @@ -90,7 +80,7 @@ const mocks = { }; interface Mocks { GetDonationsGraph: GetDonationsGraphQuery; - GetDonationsTable: GetDonationsTableQuery; + DonationTable: DonationTableQuery; } describe('DonationsReport', () => { @@ -148,7 +138,7 @@ describe('DonationsReport', () => { }); it('renders with data', async () => { - const { getByTestId, queryByRole, queryByTestId } = render( + const { getByTestId, findByRole, queryByRole, queryByTestId } = render( mocks={mocks}> @@ -173,7 +163,7 @@ describe('DonationsReport', () => { expect( queryByTestId('DonationHistoriesGridLoading'), ).not.toBeInTheDocument(); - expect(getByTestId('donationRow')).toBeInTheDocument(); + expect(await findByRole('cell', { name: 'John' })).toBeInTheDocument(); }); it('initializes with month from query', () => { @@ -261,6 +251,7 @@ describe('DonationsReport', () => { }), ); }); + it('renders nav list icon and onclick triggers onNavListToggle', async () => { onNavListToggle.mockClear(); const { getByTestId } = render( diff --git a/src/components/Reports/DonationsReport/Table/DonationsReportTable.stories.tsx b/src/components/Reports/DonationsReport/Table/DonationsReportTable.stories.tsx deleted file mode 100644 index 79107704c..000000000 --- a/src/components/Reports/DonationsReport/Table/DonationsReportTable.stories.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { ReactElement } from 'react'; -import { DateTime } from 'luxon'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { GetDonationsTableQuery } from '../GetDonationsTable.generated'; -import { DonationsReportTable } from './DonationsReportTable'; - -export default { - title: 'Reports/DonationsReport/Table', -}; - -const onSelectContact = () => {}; -const time = DateTime.now(); - -export const Default = (): ReactElement => { - return ( - - mocks={{ - GetDonationsTable: { - donations: { - nodes: [ - { - amount: { - amount: 10, - convertedAmount: 10, - convertedCurrency: 'CAD', - currency: 'CAD', - }, - appeal: { - amount: 10, - amountCurrency: 'CAD', - createdAt: DateTime.now().minus({ month: 3 }).toISO(), - id: 'abc', - name: 'John', - }, - donationDate: DateTime.now().plus({ minutes: 4 }).toISO(), - donorAccount: { - displayName: 'John', - id: 'abc', - }, - id: 'abc', - paymentMethod: 'pay', - }, - { - amount: { - amount: 10, - convertedAmount: 10, - convertedCurrency: 'CAD', - currency: 'CAD', - }, - appeal: { - amount: 10, - amountCurrency: 'CAD', - createdAt: DateTime.now().minus({ month: 3 }).toISO(), - id: 'abc', - name: 'John', - }, - donationDate: DateTime.now().plus({ days: 5 }).toISO(), - donorAccount: { - displayName: 'Bob', - id: '123', - }, - id: '123', - paymentMethod: 'pay', - }, - ], - }, - }, - }} - > - {}} - /> - - ); -}; - -export const Empty = (): ReactElement => { - return ( - - mocks={{ - GetDonationsTable: { - donations: { - nodes: [], - }, - }, - }} - > - {}} - /> - - ); -}; diff --git a/src/components/Reports/DonationsReport/Table/DonationsReportTable.test.tsx b/src/components/Reports/DonationsReport/Table/DonationsReportTable.test.tsx index f56672228..7996a461c 100644 --- a/src/components/Reports/DonationsReport/Table/DonationsReportTable.test.tsx +++ b/src/components/Reports/DonationsReport/Table/DonationsReportTable.test.tsx @@ -1,511 +1,29 @@ -import React, { useState } from 'react'; +import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; -import { render, waitFor, within } from '@testing-library/react'; +import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { cloneDeep } from 'lodash'; import { DateTime } from 'luxon'; -import { SnackbarProvider } from 'notistack'; -import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import theme from '../../../../theme'; -import { GetDonationsTableQuery } from '../GetDonationsTable.generated'; +import theme from 'src/theme'; import { DonationsReportTable } from './DonationsReportTable'; -const time = DateTime.now(); -const setTime = jest.fn(); -const onSelectContact = jest.fn(); - -const router = { - query: { accountListId: 'aaa' }, - isReady: true, -}; - -const mocks = { - GetAccountListCurrency: { - accountList: { - id: 'abc', - currency: 'CAD', - }, - }, - GetDonationsTable: { - currency: 'CAD', - donations: { - nodes: [ - { - amount: { - amount: 10, - convertedAmount: 10, - convertedCurrency: 'CAD', - currency: 'CAD', - }, - appeal: { - id: 'abc', - name: 'Appeal Test 1', - }, - donationDate: '2023-03-02', - donorAccount: { - contacts: { - nodes: [{ id: 'contact1' }], - }, - displayName: 'John', - id: 'abc', - }, - id: 'abc', - paymentMethod: 'pay', - }, - { - amount: { - amount: 100, - convertedAmount: 100, - convertedCurrency: 'CAD', - currency: 'CAD', - }, - appeal: null, - donationDate: '2023-03-01', - donorAccount: { - contacts: { - nodes: [{ id: 'contact2' }], - }, - displayName: 'John', - id: 'def', - }, - id: 'def', - paymentMethod: 'pay', - }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, -}; - describe('DonationsReportTable', () => { - it('renders with data', async () => { - const { - getAllByTestId, - queryAllByRole, - queryByRole, - getByText, - queryAllByText, - } = render( - - - mocks={mocks} - > - - - , - ); - - await waitFor(() => - expect(queryByRole('progressbar')).not.toBeInTheDocument(), - ); - - expect(queryAllByRole('button')[1]).toBeInTheDocument(); - - expect(getAllByTestId('donationRow')[0]).toBeInTheDocument(); - - await waitFor(() => - expect(queryAllByText('Appeal Test 1')).toHaveLength(1), - ); - - expect(getAllByTestId('appeal-name')).toHaveLength(2); - - expect(getAllByTestId('appeal-name')[1]).toBeEmptyDOMElement(); - - expect(getByText('3/1/2023')).toBeInTheDocument(); - }); - - it('opens and closes the edit donation modal', async () => { - const { queryByRole, queryByText, findByText, getByTestId, getByRole } = - render( - - - - - - mocks={mocks} - > - - - - - - , - ); - - await waitFor(() => - expect(queryByRole('progressbar')).not.toBeInTheDocument(), - ); - - expect(queryByText('Edit Donation')).not.toBeInTheDocument(); - - await waitFor(() => expect(getByTestId('edit-abc')).toBeInTheDocument()); - - userEvent.click(getByTestId('edit-abc')); - - expect(await findByText('Edit Donation')).toBeInTheDocument(); - - userEvent.click(getByRole('button', { name: 'Cancel' })); - - expect(queryByText('Edit Donation')).not.toBeInTheDocument(); - }); - - it('renders empty', async () => { - const mocks = { - GetDonationsTable: { - donations: { - nodes: [], - pageInfo: { - hasNextPage: false, - }, - }, - }, - }; - - const { queryByTestId, queryAllByRole, queryByRole } = render( - - - - mocks={mocks} - > - - - - , - ); - - await waitFor(() => - expect(queryByRole('progressbar')).not.toBeInTheDocument(), - ); - - expect(queryAllByRole('button')[1]).toBeInTheDocument(); - - expect(queryByTestId('donationRow')).not.toBeInTheDocument(); - }); - - it('is clickable', async () => { - const { queryAllByText } = render( - - - mocks={mocks} - > - - - , - ); - - await waitFor(() => expect(queryAllByText('John')).toHaveLength(2)); - - userEvent.click(queryAllByText('John')[0]); - expect(onSelectContact).toHaveBeenCalledWith('contact1'); - }); - - it('filters report by designation account', async () => { - const mutationSpy = jest.fn(); - render( - - - mocks={mocks} - onCall={mutationSpy} - > - - - , - ); - - await waitFor(() => - expect(mutationSpy.mock.calls[0][0]).toMatchObject({ - operation: { - operationName: 'GetDonationsTable', - variables: { - designationAccountIds: ['account-1'], - }, - }, - }), - ); - }); - - it('does not filter report by designation account', async () => { - const mutationSpy = jest.fn(); - render( - - - mocks={mocks} - onCall={mutationSpy} - > - - - , - ); - - await waitFor(() => - expect(mutationSpy.mock.calls[0][0]).toMatchObject({ - operation: { - operationName: 'GetDonationsTable', - variables: { - designationAccountIds: null, - }, - }, - }), - ); - }); - - it('is not clickable when contact is missing', async () => { - const mocks = { - GetAccountListCurrency: { - accountList: { - currency: 'CAD', - }, - }, - GetDonationsTable: { - donations: { - nodes: [ - { - amount: { - amount: 10, - convertedAmount: 10, - convertedCurrency: 'CAD', - currency: 'CAD', - }, - appeal: { - id: 'abc', - name: 'Appeal Test 1', - }, - donationDate: DateTime.now().minus({ minutes: 4 }).toISO(), - donorAccount: { - contacts: { - nodes: [], - }, - displayName: 'John', - id: 'abc', - }, - id: 'abc', - paymentMethod: 'pay', - }, - ], - }, - }, - }; - const { queryAllByText, getByText } = render( - - - mocks={mocks} - > - - - , - ); - - await waitFor(() => expect(queryAllByText('John')).toHaveLength(1)); - - userEvent.click(getByText('John')); - expect(onSelectContact).not.toHaveBeenCalled(); - }); - - it('hides currency column if all currencies match the account currency', async () => { - const { getByLabelText, queryByLabelText } = render( - - - mocks={mocks} - > - - - , - ); - - await waitFor(() => expect(getByLabelText('Partner')).toBeInTheDocument()); - expect(queryByLabelText('Foreign Amount')).not.toBeInTheDocument(); - }); - - it('shows currency column if a currency does not match the account currency', async () => { - const mocksWithMultipleCurrencies = cloneDeep(mocks); - mocksWithMultipleCurrencies.GetDonationsTable.donations.nodes[0].amount.currency = - 'EUR'; - const { getByLabelText } = render( - - - mocks={mocksWithMultipleCurrencies} - > - - - , - ); - - await waitFor(() => expect(getByLabelText('Partner')).toBeInTheDocument()); - expect(getByLabelText('Foreign Amount')).toBeInTheDocument(); - }); - - it('updates the sort order', async () => { - const mutationSpy = jest.fn(); - const { findByRole, getAllByRole } = render( + it('updates the time filter', () => { + const setTime = jest.fn(); + const { getByRole } = render( - - mocks={mocks} - onCall={mutationSpy} - > + , ); - const dateHeader = await findByRole('columnheader', { name: 'Date' }); - expect( - within(dateHeader).getByTestId('ArrowDownwardIcon'), - ).toBeInTheDocument(); - - userEvent.click(await findByRole('columnheader', { name: 'Amount' })); - const cellsAsc = getAllByRole('cell', { name: /CA/ }); - expect(cellsAsc[0]).toHaveTextContent('CA$10'); - expect(cellsAsc[1]).toHaveTextContent('CA$100'); - - userEvent.click(await findByRole('columnheader', { name: 'Amount' })); - const cellsDesc = getAllByRole('cell', { name: /CA/ }); - expect(cellsDesc[0]).toHaveTextContent('CA$100'); - expect(cellsDesc[1]).toHaveTextContent('CA$10'); - }); - - it('updates the page size without rerendering until the month changes', async () => { - const DonationsReportTableWrapper: React.FC = () => { - const [time, setTime] = useState(DateTime.now()); - - return ( - - ); - }; - - const mutationSpy = jest.fn(); - const { findByRole, getByRole } = render( - - - mocks={mocks} - onCall={mutationSpy} - > - - - , - ); - - userEvent.click(await findByRole('combobox', { name: 'Rows per page:' })); - mutationSpy.mockClear(); - userEvent.click(getByRole('option', { name: '50' })); - - expect(mutationSpy).not.toHaveBeenCalled(); - userEvent.click(getByRole('button', { name: 'Previous Month' })); - - await waitFor(() => { - expect(mutationSpy).toHaveBeenCalledTimes(1); - expect(mutationSpy.mock.calls[0][0]).toMatchObject({ - operation: { - operationName: 'GetDonationsTable', - variables: { - pageSize: 50, - }, - }, - }); - }); - }); - - it('Ensure process bar loads when loading next page', async () => { - const DonationsReportTableWrapper: React.FC = () => { - const [time, setTime] = useState(DateTime.now()); - - return ( - - ); - }; - - const mutationSpy = jest.fn(); - const { getByTestId } = render( - - - mocks={{ - GetAccountListCurrency: mocks.GetAccountListCurrency, - GetDonationsTable: { - donations: { - nodes: mocks.GetDonationsTable.donations.nodes, - pageInfo: { - hasNextPage: true, - }, - }, - }, - }} - onCall={mutationSpy} - > - - - , - ); - - await waitFor(() => - expect(getByTestId('nextPageProcessBar')).toBeInTheDocument(), - ); + expect(setTime.mock.lastCall[0].toISODate()).toBe('2019-12-01'); }); }); diff --git a/src/components/Reports/DonationsReport/Table/DonationsReportTable.tsx b/src/components/Reports/DonationsReport/Table/DonationsReportTable.tsx index b741665c3..df8fc3c11 100644 --- a/src/components/Reports/DonationsReport/Table/DonationsReportTable.tsx +++ b/src/components/Reports/DonationsReport/Table/DonationsReportTable.tsx @@ -1,43 +1,12 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import EditIcon from '@mui/icons-material/Edit'; -import { - Box, - Button, - CircularProgress, - Divider, - IconButton, - LinearProgress, - Link, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { DataGrid, GridColDef, GridSortModel } from '@mui/x-data-grid'; +import { Box, Button, Divider, Typography } from '@mui/material'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; -import { preloadContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/DynamicContactsRightPanel'; -import { - DynamicEditDonationModal, - preloadEditDonationModal, -} from 'src/components/EditDonationModal/DynamicEditDonationModal'; -import { useFetchAllPages } from 'src/hooks/useFetchAllPages'; +import { DonationTable } from 'src/components/DonationTable/DonationTable'; import { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat, dateFormatShort } from 'src/lib/intlFormat'; import { EmptyDonationsTable } from '../../../common/EmptyDonationsTable/EmptyDonationsTable'; -import { - DonationTableRowFragment, - GetDonationsTableQueryVariables, - useGetAccountListCurrencyQuery, - useGetDonationsTableQuery, -} from '../GetDonationsTable.generated'; - -type RenderCell = GridColDef['renderCell']; interface DonationReportTableProps { accountListId: string; @@ -47,79 +16,6 @@ interface DonationReportTableProps { setTime: (time: DateTime) => void; } -const DataTable = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - '& .MuiDataGrid-row.Mui-even:not(:hover)': { - backgroundColor: - theme.palette.mode === 'light' - ? theme.palette.common.white - : theme.palette.cruGrayLight.main, - }, - '& .MuiDataGrid-cell': { - '& .MuiTypography-root': { - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }, - }, -})); - -const LoadingBox = styled(Box)(({ theme }) => ({ - backgroundColor: theme.palette.cruGrayLight.main, - height: 300, - minWidth: 700, - margin: 'auto', - padding: 4, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -})); - -const LoadingIndicator = styled(CircularProgress)(({ theme }) => ({ - margin: theme.spacing(0, 1, 0, 0), -})); - -const LoadingProgressBar = styled(LinearProgress)(({ theme }) => ({ - flex: 0.25, - height: '0.5rem', - borderRadius: theme.shape.borderRadius, - ['& .MuiLinearProgress-bar']: { - borderRadius: theme.shape.borderRadius, - }, - alignSelf: 'center', -})); - -interface DonationRow { - id: string; - date: DateTime; - contactId: string | null; - donorAccountName: string; - currency: string; - foreignCurrency: string; - convertedAmount: number; - foreignAmount: number; - designationAccount: string; - paymentMethod: string | null; - appealName: string | null; - rawDonation: DonationTableRowFragment; -} - -const createDonationRow = (data: DonationTableRowFragment): DonationRow => ({ - id: data.id, - date: DateTime.fromISO(data.donationDate), - contactId: data.donorAccount.contacts.nodes[0]?.id ?? null, - donorAccountName: data.donorAccount.displayName, - convertedAmount: data.amount.convertedAmount, - currency: data.amount.convertedCurrency, - foreignAmount: data.amount.amount, - foreignCurrency: data.amount.currency, - designationAccount: data.designationAccount.name, - paymentMethod: data.paymentMethod ?? null, - appealName: data.appeal?.name ?? null, - rawDonation: data, -}); - export const DonationsReportTable: React.FC = ({ accountListId, designationAccounts, @@ -129,181 +25,9 @@ export const DonationsReportTable: React.FC = ({ }) => { const { t } = useTranslation(); const locale = useLocale(); - const [selectedDonation, setSelectedDonation] = useState( - null, - ); - - const handleClose = () => { - setSelectedDonation(null); - }; - - const startDate = time.toString().slice(0, 10); - const endDate = time - .plus({ months: 1 }) - .minus({ days: 1 }) - .toString() - .slice(0, 10); - - const [pageSize, setPageSize] = useState(25); - - // pageSize is intentionally omitted from the dependencies array so that the query isn't rerun when the page size changes - // If all the pages have loaded and the user changes the page size, there's no reason to reload all the pages - // TODO: sort donations on the server after https://jira.cru.org/browse/MPDX-7634 is implemented - const variables: GetDonationsTableQueryVariables = useMemo( - () => ({ - accountListId, - pageSize, - startDate, - endDate, - designationAccountIds: designationAccounts?.length - ? designationAccounts - : null, - }), - [accountListId, startDate, endDate, designationAccounts], - ); - const { data, error, loading, fetchMore } = useGetDonationsTableQuery({ - variables, - }); - // Load the rest of the pages asynchronously so that we can calculate the total donations - useFetchAllPages({ - fetchMore, - error, - pageInfo: data?.donations.pageInfo, - }); - - const { data: accountListData, loading: loadingAccountListData } = - useGetAccountListCurrencyQuery({ - variables: { accountListId }, - }); - - const nodes = data?.donations.nodes || []; - - const accountCurrency = accountListData?.accountList.currency || 'USD'; - - const donations = useMemo(() => nodes.map(createDonationRow), [nodes]); - - const date: RenderCell = ({ row }) => ( - {dateFormatShort(row.date, locale)} - ); - - const link: RenderCell = ({ row }) => ( - - row.contactId && onSelectContact(row.contactId)} - onMouseEnter={preloadContactsRightPanel} - > - {row.donorAccountName} - - - ); - - const amount: RenderCell = ({ row }) => ( - - {currencyFormat(row.convertedAmount, row.currency, locale)} - - ); - - const foreignAmount: RenderCell = ({ row }) => ( - - {currencyFormat(row.foreignAmount, row.foreignCurrency, locale)} - - ); - - const designation: RenderCell = ({ row }) => ( - {row.designationAccount} - ); - - const method: RenderCell = ({ row: donation }) => ( - {donation.paymentMethod} - ); - - const button: RenderCell = ({ row: donation }) => { - return ( - - {donation.appealName} - { - setSelectedDonation(donation); - }} - onMouseEnter={preloadEditDonationModal} - > - - - - ); - }; - const columns: GridColDef[] = [ - { - field: 'date', - headerName: t('Date'), - width: 100, - renderCell: date, - }, - { - field: 'donorAccountName', - headerName: t('Partner'), - width: 360, - renderCell: link, - }, - { - field: 'convertedAmount', - headerName: t('Amount'), - width: 120, - renderCell: amount, - }, - { - field: 'foreignAmount', - headerName: t('Foreign Amount'), - width: 120, - renderCell: foreignAmount, - }, - { - field: 'designation', - headerName: t('Designation'), - width: 220, - renderCell: designation, - }, - { - field: 'method', - headerName: t('Method'), - width: 100, - renderCell: method, - }, - { - field: 'appeal', - headerName: t('Appeal'), - width: 125, - renderCell: button, - }, - ]; - - const [sortModel, setSortModel] = useState([ - { field: 'date', sort: 'desc' }, - ]); - - // Remove foreign amount column if both of these conditions are met: - // 1.) There only one type of currency. - // 2.) The type of currency is the same as the account currency. - const currencyList = new Set( - donations.map((donation) => donation.foreignCurrency), - ); - - if (currencyList.size === 1 && currencyList.has(accountCurrency)) { - columns.splice(3, 1); - columns.forEach( - (column) => (column.width = column.width ? column.width + 20 : 0), - ); - } - - const isEmpty = nodes.length === 0; + const startDate = time.toISODate(); + const endDate = time.plus({ months: 1 }).minus({ days: 1 }).toISODate(); const title = time.toJSDate().toLocaleDateString(locale, { month: 'long', @@ -320,34 +44,17 @@ export const DonationsReportTable: React.FC = ({ setTime(time.plus({ months: 1 })); }; - const totalDonations = donations.reduce((total, current) => { - return total + current.convertedAmount; - }, 0); - - const totalForeignDonations = donations.reduce( - ( - acc: Record, - donation, - ) => { - const { foreignCurrency, foreignAmount, convertedAmount } = donation; - if (acc[foreignCurrency] !== undefined) { - acc[foreignCurrency].foreignTotal += foreignAmount; - acc[foreignCurrency].convertedTotal += convertedAmount; - } else { - acc[foreignCurrency] = { - convertedTotal: convertedAmount, - foreignTotal: foreignAmount, - }; - } - return acc; - }, - {}, + const query = useMemo( + () => ({ + startDate, + endDate, + designationAccountIds: designationAccounts?.length + ? designationAccounts + : null, + }), + [startDate, endDate, designationAccounts], ); - const loadingProgress = data - ? (data.donations.nodes.length / (data?.donations.totalCount || 1)) * 100 - : 0; - return ( <> = ({ }} > {title} - {data?.donations.pageInfo.hasNextPage && ( - - )} - - {!isEmpty ? ( - - setPageSize(pageSize)} - rowsPerPageOptions={[25, 50, 100]} - pagination - sortModel={sortModel} - onSortModelChange={(sortModel) => setSortModel(sortModel)} - autoHeight - disableSelectionOnClick - disableVirtualization + + - {!data?.donations.pageInfo.hasNextPage && ( - - - {Object.entries(totalForeignDonations).map( - ([currency, total]) => ( - - - - {t('Total {{currency}} Donations:', { currency })} - - - - - {currencyFormat( - total.convertedTotal, - accountCurrency, - locale, - )} - - - - - {currencyFormat(total.foreignTotal, currency, locale)} - - - - ), - )} - - - - - - {t('Total Donations: ')} - - - - - {currencyFormat(totalDonations, accountCurrency, locale)} - - - - - -
- )} -
- ) : loading || loadingAccountListData ? ( - - - - ) : ( - - )} - {selectedDonation && ( - handleClose()} - /> - )} + } + /> ); }; diff --git a/src/components/Task/TaskRow/TaskRow.test.tsx b/src/components/Task/TaskRow/TaskRow.test.tsx index fee703d89..2dd19c175 100644 --- a/src/components/Task/TaskRow/TaskRow.test.tsx +++ b/src/components/Task/TaskRow/TaskRow.test.tsx @@ -45,6 +45,9 @@ describe('TaskRow', () => { mocks: { startAt, result: ResultEnum.None, + contacts: { + nodes: [{}], + }, }, }); @@ -94,11 +97,15 @@ describe('TaskRow', () => { expect(await findByText(task.contacts.nodes[0].name)).toBeVisible(); }); + it('should render late', async () => { const task = gqlMock(TaskRowFragmentDoc, { mocks: { startAt: lateStartAt, result: ResultEnum.None, + contacts: { + nodes: [{}], + }, }, }); @@ -133,6 +140,9 @@ describe('TaskRow', () => { startAt, result: ResultEnum.None, user: assignee, + contacts: { + nodes: [{}], + }, }, }); @@ -185,6 +195,7 @@ describe('TaskRow', () => { userEvent.click(getByRole('checkbox', { hidden: true })); expect(onTaskCheckSelected).toHaveBeenCalledWith(task.id); }); + it('handles task row click', async () => { const task = gqlMock(TaskRowFragmentDoc, { mocks: { @@ -210,6 +221,7 @@ describe('TaskRow', () => { userEvent.click(getByTestId('task-row')); expect(onTaskCheckSelected).toHaveBeenCalledWith(task.id); }); + it('handles complete button click', async () => { const task = gqlMock(TaskRowFragmentDoc, { mocks: { @@ -245,6 +257,9 @@ describe('TaskRow', () => { mocks: { startAt, result: ResultEnum.None, + contacts: { + nodes: [{}], + }, }, }); diff --git a/src/hooks/useLocalStorage.test.ts b/src/hooks/useLocalStorage.test.ts new file mode 100644 index 000000000..0fd30164b --- /dev/null +++ b/src/hooks/useLocalStorage.test.ts @@ -0,0 +1,46 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { useLocalStorage } from './useLocalStorage'; + +describe('useLocalStorage', () => { + const initialValue = { key: 'initial' }; + const storageKey = 'storage-key'; + const getItem = jest.fn(); + const setItem = jest.fn(); + + beforeEach(() => { + window.Storage.prototype.getItem = getItem; + window.Storage.prototype.setItem = setItem; + }); + + it('returns the initial value', () => { + const { result } = renderHook(() => + useLocalStorage(storageKey, initialValue), + ); + + expect(result.current[0]).toBe(initialValue); + }); + + it('reads the value from local storage', () => { + getItem.mockReturnValue('{"key":"value"}'); + + const { result } = renderHook(() => + useLocalStorage(storageKey, initialValue), + ); + + expect(getItem).toHaveBeenCalledWith(storageKey); + expect(result.current[0]).toEqual({ key: 'value' }); + }); + + it('writes the value to local storage', () => { + const { result } = renderHook(() => + useLocalStorage(storageKey, initialValue), + ); + + act(() => { + result.current[1]({ key: 'value' }); + }); + + expect(setItem).toHaveBeenCalledWith(storageKey, '{"key":"value"}'); + expect(result.current[0]).toEqual({ key: 'value' }); + }); +}); diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..fef0e7fc1 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,74 @@ +// Adapted from https://usehooks-ts.com/react-hook/use-local-storage +import { useCallback, useEffect, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; + +const IS_SERVER = typeof window === 'undefined'; + +export const useLocalStorage = ( + key: string, + initialValue: T, +): [T, Dispatch>] => { + const deserializer = useCallback<(value: string) => T>( + (value) => { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch (error) { + return initialValue; // Return initialValue if parsing fails + } + + return parsed as T; + }, + [initialValue], + ); + + // Get from local storage then + // parse stored json or return initialValue + const readValue = useCallback((): T => { + // Prevent build error "window is undefined" but keeps working + if (IS_SERVER) { + return initialValue; + } + + try { + const raw = window.localStorage.getItem(key); + return raw ? deserializer(raw) : initialValue; + } catch (error) { + return initialValue; + } + }, [initialValue, key, deserializer]); + + const [storedValue, setStoredValue] = useState(initialValue); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue: Dispatch> = useCallback((value) => { + // Prevent build error "window is undefined" but keeps working + if (IS_SERVER) { + // eslint-disable-next-line no-console + console.warn( + `Tried setting localStorage key “${key}” even though environment is not a client`, + ); + } + + try { + // Allow value to be a function so we have the same API as useState + const newValue = value instanceof Function ? value(readValue()) : value; + + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(newValue)); + + // Save state + setStoredValue(newValue); + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`Error setting localStorage key “${key}”:`, error); + } + }, []); + + useEffect(() => { + setStoredValue(readValue()); + }, [key]); + + return [storedValue, setValue]; +}; diff --git a/src/theme.ts b/src/theme.ts index bad7e4824..35b520b90 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -172,6 +172,13 @@ const theme = createTheme({ }, }, }, + MuiLink: { + styleOverrides: { + root: { + cursor: 'pointer', + }, + }, + }, MuiTableCell: { styleOverrides: { head: { diff --git a/yarn.lock b/yarn.lock index f1ef864c6..c0d40db5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5214,7 +5214,7 @@ __metadata: languageName: node linkType: hard -"@mui/x-data-grid@npm:^5.17.4": +"@mui/x-data-grid@npm:5.17.4": version: 5.17.4 resolution: "@mui/x-data-grid@npm:5.17.4" dependencies: @@ -5232,6 +5232,24 @@ __metadata: languageName: node linkType: hard +"@mui/x-data-grid@patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::locator=mpdx-react%40workspace%3A.": + version: 5.17.4 + resolution: "@mui/x-data-grid@patch:@mui/x-data-grid@npm%3A5.17.4#./.yarn/patches/@mui-x-data-grid-npm-5.17.4-542730a19e.patch::version=5.17.4&hash=362dcd&locator=mpdx-react%40workspace%3A." + dependencies: + "@babel/runtime": ^7.18.9 + "@mui/utils": ^5.10.3 + clsx: ^1.2.1 + prop-types: ^15.8.1 + reselect: ^4.1.6 + peerDependencies: + "@mui/material": ^5.4.1 + "@mui/system": ^5.4.1 + react: ^17.0.2 || ^18.0.0 + react-dom: ^17.0.2 || ^18.0.0 + checksum: 0c31923597e605528d3ce17e40eca4f97519af7174b768f5530d85fb7764194c75e22eb63252ddff5e455a236afc497a826de9b4eb722965519f2b1efaaef928 + languageName: node + linkType: hard + "@mui/x-date-pickers@npm:^7.0.0-beta.7": version: 7.0.0-beta.7 resolution: "@mui/x-date-pickers@npm:7.0.0-beta.7" @@ -7585,13 +7603,6 @@ __metadata: languageName: node linkType: hard -"@types/seedrandom@npm:^3.0.5": - version: 3.0.5 - resolution: "@types/seedrandom@npm:3.0.5" - checksum: d63d56ebc609203cf8cbe2ab7e7934237446bc2463ade664be130dcd4a3d74915d529909de2e1b18141cfabc9e42784998b7e5ff30c016919ac5b83911a700f0 - languageName: node - linkType: hard - "@types/semver@npm:^7.5.0": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -18598,7 +18609,6 @@ __metadata: "@types/luxon": ^3.4.2 "@types/node": ^18.7.18 "@types/react": ^18.0.21 - "@types/seedrandom": ^3.0.5 "@types/testing-library__jest-dom": ^5.14.5 "@types/uuid": ^9.0.1 "@typescript-eslint/eslint-plugin": ^7.5.0 @@ -18664,7 +18674,6 @@ __metadata: react-virtuoso: 2.19.0 recharts: 2.3.2 rollbar: ^2.25.2 - seedrandom: ^3.0.5 storybook: ^6.5.16 storybook-addon-designs: ^6.3.1 storybook-react-i18next: ^1.1.2