From f0768b1e3c3c2ba2e11728e8e47adb937db139e8 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Wed, 28 Feb 2024 14:50:58 -0500 Subject: [PATCH 01/68] fix: Renamed state fields and service function names to avoid confusion betwen fetching shared resources and fetching data entries --- src/group/groupSlice.ts | 4 +- src/landscape/landscapeSlice.js | 4 +- src/sharedData/components/SharedDataCard.js | 2 +- src/sharedData/sharedDataService.js | 4 +- src/sharedData/sharedDataSlice.js | 44 ++++++++++--------- .../SelectDataFileStep.js | 8 ++-- .../visualization/visualizationUtils.js | 4 +- 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/group/groupSlice.ts b/src/group/groupSlice.ts index 3917d4e95..1125cfc58 100644 --- a/src/group/groupSlice.ts +++ b/src/group/groupSlice.ts @@ -25,7 +25,7 @@ import type { Message } from 'terraso-client-shared/notifications/notificationsS import { createAsyncThunk } from 'terraso-client-shared/store/utils'; import * as groupService from 'group/groupService'; -import { setList } from 'sharedData/sharedDataSlice'; +import { setSharedResourcesList } from 'sharedData/sharedDataSlice'; export type Group = { slug: string; @@ -46,7 +46,7 @@ export const fetchGroupView = createAsyncThunk( 'group/fetchGroupView', async (slug: string, user, { dispatch }) => { const group = await groupService.fetchGroupToView(slug, user); - dispatch(setList(group.sharedResources)); + dispatch(setSharedResourcesList(group.sharedResources)); return group; } ); diff --git a/src/landscape/landscapeSlice.js b/src/landscape/landscapeSlice.js index d7757341b..42e28c1b6 100644 --- a/src/landscape/landscapeSlice.js +++ b/src/landscape/landscapeSlice.js @@ -19,7 +19,7 @@ import _ from 'lodash/fp'; import { createAsyncThunk } from 'terraso-client-shared/store/utils'; import * as landscapeService from 'landscape/landscapeService'; -import { setList } from 'sharedData/sharedDataSlice'; +import { setSharedResourcesList } from 'sharedData/sharedDataSlice'; const initialState = { list: { @@ -73,7 +73,7 @@ export const fetchLandscapeView = createAsyncThunk( params, currentUser ); - dispatch(setList(landscape.sharedResources)); + dispatch(setSharedResourcesList(landscape.sharedResources)); return landscape; } ); diff --git a/src/sharedData/components/SharedDataCard.js b/src/sharedData/components/SharedDataCard.js index 4026ba21d..bbc741682 100644 --- a/src/sharedData/components/SharedDataCard.js +++ b/src/sharedData/components/SharedDataCard.js @@ -42,7 +42,7 @@ const SharedDataCard = props => { const { owner, entityTypeLocalized } = useCollaborationContext(); const { allowed } = usePermission(`sharedData.viewFiles`, owner); const { data: sharedResources, fetching } = useSelector( - _.get('sharedData.list') + _.get('sharedData.sharedResources') ); const hasSharedData = !_.isEmpty(sharedResources); diff --git a/src/sharedData/sharedDataService.js b/src/sharedData/sharedDataService.js index 2eb99852a..ec964ebde 100644 --- a/src/sharedData/sharedDataService.js +++ b/src/sharedData/sharedDataService.js @@ -118,7 +118,7 @@ export const updateSharedData = ({ dataEntry }) => { .then(extractDataEntry); }; -export const fetchSharedData = ({ +export const fetchDataEntries = ({ targetSlug, targetType, resourceTypes = ALL_RESOURCE_TYPES, @@ -153,7 +153,7 @@ export const fetchSharedData = ({ .then(edges => edges.map(edge => extractDataEntry(edge.node))); }; -export const fetchSharedDataWithGeojson = ({ id }) => { +export const fetchDataEntriesWithGeojson = ({ id }) => { const query = graphql(` query dataEntryWithGeojson($id: ID!) { dataEntry(id: $id) { diff --git a/src/sharedData/sharedDataSlice.js b/src/sharedData/sharedDataSlice.js index 4bc76a353..74d81f0b3 100644 --- a/src/sharedData/sharedDataSlice.js +++ b/src/sharedData/sharedDataSlice.js @@ -30,7 +30,11 @@ const initialState = { links: {}, }, processing: {}, - list: { + sharedResources: { + fetching: true, + data: null, + }, + dataEntries: { fetching: true, data: null, }, @@ -78,9 +82,9 @@ export const updateSharedData = createAsyncThunk( params: { name: dataEntry.name }, }) ); -export const fetchSharedData = createAsyncThunk( - 'sharedData/fetchSharedData', - sharedDataService.fetchSharedData +export const fetchDataEntries = createAsyncThunk( + 'sharedData/fetchDataEntries', + sharedDataService.fetchDataEntries ); export const addVisualizationConfig = createAsyncThunk( @@ -134,9 +138,9 @@ const sharedDataSlice = createSlice({ ...state, processing: _.omit(action.payload, state.processing), }), - setList: (state, action) => ({ + setSharedResourcesList: (state, action) => ({ ...state, - list: { + sharedResources: { data: action.payload, fetching: false, }, @@ -160,9 +164,9 @@ const sharedDataSlice = createSlice({ ); builder.addCase(updateSharedData.fulfilled, (state, action) => ({ ...state, - list: { - ...state.list, - data: state.list.data.map(item => + sharedResources: { + ...state.sharedResources, + data: state.sharedResources.data.map(item => item.id === action.meta.arg.sharedResource.id ? { ...item, dataEntry: action.payload } : item @@ -190,9 +194,9 @@ const sharedDataSlice = createSlice({ processing: _.omit(action.meta.arg.sharedResource.id, { ...state.processing, }), - list: { - ...state.list, - data: state.list.data.map(item => + sharedResources: { + ...state.sharedResources, + data: state.sharedResources.data.map(item => item.id === action.meta.arg.sharedResource.id ? action.payload : item @@ -218,9 +222,9 @@ const sharedDataSlice = createSlice({ builder.addCase(deleteSharedData.fulfilled, (state, action) => ({ ...state, - list: { - ...state.list, - data: state.list.data.filter( + sharedResources: { + ...state.sharedResources, + data: state.sharedResources.data.filter( item => item.id !== action.meta.arg.sharedResource.id ), }, @@ -292,17 +296,17 @@ const sharedDataSlice = createSlice({ ) ); - builder.addCase(fetchSharedData.pending, (state, action) => ({ + builder.addCase(fetchDataEntries.pending, (state, action) => ({ ...state, - list: { + dataEntries: { fetching: true, data: null, }, })); - builder.addCase(fetchSharedData.fulfilled, (state, action) => ({ + builder.addCase(fetchDataEntries.fulfilled, (state, action) => ({ ...state, - list: { + dataEntries: { fetching: false, data: action.payload, }, @@ -368,7 +372,7 @@ const sharedDataSlice = createSlice({ }, }); -export const { resetUploads, resetProcessing, setList } = +export const { resetUploads, resetProcessing, setSharedResourcesList } = sharedDataSlice.actions; export default sharedDataSlice.reducer; diff --git a/src/sharedData/visualization/components/VisualizationConfigForm/SelectDataFileStep.js b/src/sharedData/visualization/components/VisualizationConfigForm/SelectDataFileStep.js index 081ddbb7c..da40a8b64 100644 --- a/src/sharedData/visualization/components/VisualizationConfigForm/SelectDataFileStep.js +++ b/src/sharedData/visualization/components/VisualizationConfigForm/SelectDataFileStep.js @@ -37,7 +37,7 @@ import StepperStep from 'common/components/StepperStep'; import PageLoader from 'layout/PageLoader'; import { formatDate } from 'localization/utils'; import SharedFileIcon from 'sharedData/components/SharedFileIcon'; -import { fetchSharedData } from 'sharedData/sharedDataSlice'; +import { fetchDataEntries } from 'sharedData/sharedDataSlice'; import { useVisualizationContext } from 'sharedData/visualization/visualizationContext'; import { @@ -77,7 +77,9 @@ const SelectDataFileStep = props => { const { visualizationConfig } = useVisualizationContext(); const { onNext, onBack } = props; const { owner, entityType } = useCollaborationContext(); - const { data: sharedFiles, fetching } = useSelector(_.get('sharedData.list')); + const { data: sharedFiles, fetching } = useSelector( + _.get('sharedData.dataEntries') + ); const [selected, setSelected] = useState(); useEffect(() => { @@ -88,7 +90,7 @@ const SelectDataFileStep = props => { useEffect(() => { dispatch( - fetchSharedData({ + fetchDataEntries({ targetSlug: owner.slug, targetType: entityType, resourceTypes: ACCEPTED_RESOURCE_TYPES, diff --git a/src/sharedData/visualization/visualizationUtils.js b/src/sharedData/visualization/visualizationUtils.js index f8f086afa..2b3ec2bec 100644 --- a/src/sharedData/visualization/visualizationUtils.js +++ b/src/sharedData/visualization/visualizationUtils.js @@ -20,7 +20,7 @@ import * as yup from 'yup'; import { normalizeLongitude } from 'gis/gisUtils'; import mapboxgl from 'gis/mapbox'; -import { fetchSharedDataWithGeojson } from 'sharedData/sharedDataService'; +import { fetchDataEntriesWithGeojson } from 'sharedData/sharedDataService'; export const readFile = async file => { const response = await fetch(file.url); @@ -61,7 +61,7 @@ export const readDataSetFile = async file => { }; export const readMapFile = async dataEntry => { - const response = await fetchSharedDataWithGeojson({ id: dataEntry.id }); + const response = await fetchDataEntriesWithGeojson({ id: dataEntry.id }); const geojson = JSON.parse(response.geojson); return { geojson }; }; From a2cefa877032bb45940aa559ab13eb27766d7562 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Wed, 28 Feb 2024 18:56:20 -0500 Subject: [PATCH 02/68] Squashed commit of the following: fix: rename SENTRY_DSN environment variable to REACT_APP_SENTRY_DSN fix: update sentry config chore: gitignore sentry CLI config chore: update sentry CLI --- .gitignore | 3 +++ local.env.sample | 2 +- package-lock.json | 65 ++++++++++++++++++++++++----------------------- package.json | 2 +- src/config.ts | 2 +- src/index.js | 26 ++++++++++++------- 6 files changed, 56 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index bfba0d1b7..f5b5944ee 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ src/terrasoApi/shared/graphqlSchema # Sentry Config File .sentryclirc + +# Sentry Config File +.sentryclirc diff --git a/local.env.sample b/local.env.sample index 02aa171d0..2a52fd1bd 100644 --- a/local.env.sample +++ b/local.env.sample @@ -4,5 +4,5 @@ REACT_APP_PLAUSIBLE_DOMAIN=app.local.terraso.org REACT_APP_SHARED_DATA_ACCEPTED_EXTENSIONS=csv,doc,docx,pdf,ppt,pptx,xlsx,xls REACT_APP_IMAGE_ACCEPTED_EXTENSIONS=jpeg,jpg REACT_APP_MAPBOX_ACCESS_TOKEN= +REACT_APP_SENTRY_DSN=https://xyz@o4506299756642304.ingest.sentry.io/abcd SENTRY_AUTH_TOKEN=sntrys_eyxxxxxxxxxxx -SENTRY_DSN=https://xyz@o4506299756642304.ingest.sentry.io/abcd diff --git a/package-lock.json b/package-lock.json index a773f7ae5..acd535f27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@mui/material": "^5.15.3", "@mui/x-data-grid": "^6.19.1", "@reduxjs/toolkit": "^1.9.7", - "@sentry/cli": "^2.22.3", + "@sentry/cli": "^2.28.6", "@sentry/react": "^7.102.1", "@turf/bbox": "^6.5.0", "@turf/center": "^6.5.0", @@ -6713,9 +6713,10 @@ } }, "node_modules/@sentry/cli": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.22.3.tgz", - "integrity": "sha512-VFHdtrHsMyTRSZhDLeMyXvit7xB4e81KugIEwMve95c7h5HO672bfmCcM/403CAugj4NzvQ+IR2NKF/2SsEPlg==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.28.6.tgz", + "integrity": "sha512-o2Ngz7xXuhwHxMi+4BFgZ4qjkX0tdZeOSIZkFAGnTbRhQe5T8bxq6CcQRLdPhqMgqvDn7XuJ3YlFtD3ZjHvD7g==", + "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", @@ -6730,19 +6731,19 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.22.3", - "@sentry/cli-linux-arm": "2.22.3", - "@sentry/cli-linux-arm64": "2.22.3", - "@sentry/cli-linux-i686": "2.22.3", - "@sentry/cli-linux-x64": "2.22.3", - "@sentry/cli-win32-i686": "2.22.3", - "@sentry/cli-win32-x64": "2.22.3" + "@sentry/cli-darwin": "2.28.6", + "@sentry/cli-linux-arm": "2.28.6", + "@sentry/cli-linux-arm64": "2.28.6", + "@sentry/cli-linux-i686": "2.28.6", + "@sentry/cli-linux-x64": "2.28.6", + "@sentry/cli-win32-i686": "2.28.6", + "@sentry/cli-win32-x64": "2.28.6" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.22.3.tgz", - "integrity": "sha512-A1DwFTffg3+fF68qujaJI07dk/1H1pRuihlvS5WQ9sD7nQLnXZGoLUht4eULixhDzZYinWHKkcWzQ6k40UTvNA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.28.6.tgz", + "integrity": "sha512-KRf0VvTltHQ5gA7CdbUkaIp222LAk/f1+KqpDzO6nB/jC/tL4sfiy6YyM4uiH6IbVEudB8WpHCECiatmyAqMBA==", "optional": true, "os": [ "darwin" @@ -6752,9 +6753,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.22.3.tgz", - "integrity": "sha512-mDtLVbqbCu/5b/v2quTAMzY/atGlJVvrqO2Wvpro0Jb/LYhn7Y1pVBdoXEDcnOX82/pseFkLT8PFfq/OcezPhA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.28.6.tgz", + "integrity": "sha512-ANG7U47yEHD1g3JrfhpT4/MclEvmDZhctWgSP5gVw5X4AlcI87E6dTqccnLgvZjiIAQTaJJAZuSHVVF3Jk403w==", "cpu": [ "arm" ], @@ -6768,9 +6769,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.22.3.tgz", - "integrity": "sha512-PnBPb4LJ+A2LlqLjtVFn4mEizcVdxBSLZvB85pEGzq9DRXjZ6ZEuGWFHTVnWvjd79TB/s0me29QnLc3n4B6lgA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.28.6.tgz", + "integrity": "sha512-caMDt37FI752n4/3pVltDjlrRlPFCOxK4PHvoZGQ3KFMsai0ZhE/0CLBUMQqfZf0M0r8KB2x7wqLm7xSELjefQ==", "cpu": [ "arm64" ], @@ -6784,9 +6785,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.22.3.tgz", - "integrity": "sha512-wxvbpQ2hiw4hwJWfJMp7K45BV40nXL62f91jLuftFXIbieKX1Li57NNKNu2JUVn7W1bJxkwz/PKGGTXSgeJlRw==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.28.6.tgz", + "integrity": "sha512-Tj1+GMc6lFsDRquOqaGKXFpW9QbmNK4TSfynkWKiJxdTEn5jSMlXXfr0r9OQrxu3dCCqEHkhEyU63NYVpgxIPw==", "cpu": [ "x86", "ia32" @@ -6801,9 +6802,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.22.3.tgz", - "integrity": "sha512-0GxsYNO5GyRWifeOpng+MmdUFZRA64bgA1n1prsEsXnoeLcm3Zj4Q63hBZmiwz9Qbhf5ibohkpf94a7dI7pv3A==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.28.6.tgz", + "integrity": "sha512-Dt/Xz784w/z3tEObfyJEMmRIzn0D5qoK53H9kZ6e0yNvJOSKNCSOq5cQk4n1/qeG0K/6SU9dirmvHwFUiVNyYg==", "cpu": [ "x64" ], @@ -6817,9 +6818,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.22.3.tgz", - "integrity": "sha512-YERPsd7ClBrxKcmCUw+ZrAvQfbyIZFrqh269hgDuXFodpsB7LPGnI33ilo0uzmKdq2vGppTb6Z3gf1Rbq0Hadg==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.28.6.tgz", + "integrity": "sha512-zkpWtvY3kt+ogVaAbfFr2MEkgMMHJNJUnNMO8Ixce9gh38sybIkDkZNFnVPBXMClJV0APa4QH0EwumYBFZUMuQ==", "cpu": [ "x86", "ia32" @@ -6833,9 +6834,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.22.3.tgz", - "integrity": "sha512-NUh56xWvgJo2KuC9lI6o6nTPXdzbpQUB4qGwJ73L9NP3HT2P1I27jtHyrC2zlXTVlYE23gQZGrL3wgW4Jy80QA==", + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.28.6.tgz", + "integrity": "sha512-TG2YzZ9JMeNFzbicdr5fbtsusVGACbrEfHmPgzWGDeLUP90mZxiMTjkXsE1X/5jQEQjB2+fyfXloba/Ugo51hA==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index f9e06fc0c..7ff302c76 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@mui/material": "^5.15.3", "@mui/x-data-grid": "^6.19.1", "@reduxjs/toolkit": "^1.9.7", - "@sentry/cli": "^2.22.3", + "@sentry/cli": "^2.28.6", "@sentry/react": "^7.102.1", "@turf/bbox": "^6.5.0", "@turf/center": "^6.5.0", diff --git a/src/config.ts b/src/config.ts index f63ece3ad..1eb383cc9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,7 +34,7 @@ export const COOKIES_DOMAIN = const COOKIES_PARAMS = { path: '/', domain: COOKIES_DOMAIN }; -export const SENTRY_DSN = process.env.SENTRY_DSN; +export const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN || ''; setAPIConfig({ terrasoAPIURL: TERRASO_API_URL, diff --git a/src/index.js b/src/index.js index dc1cf1956..ec9e35e9c 100644 --- a/src/index.js +++ b/src/index.js @@ -38,17 +38,25 @@ Sentry.init({ dsn: SENTRY_DSN, environment: TERRASO_ENV, integrations: [ - new Sentry.BrowserTracing({ - // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled - tracePropagationTargets: ['localhost', /^https:\/\/yourserver\.io\/api/], + // See docs for support of different versions of variation of react router + // https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, }), - new Sentry.Replay(), + Sentry.replayIntegration(), ], - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - // Session Replay - replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. - replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + tracesSampleRate: 1.0, + + // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ['127.0.0.1', /^https:\/\/terraso\.org/], + + // Capture Replay for 10% of all sessions, + // plus for 100% of sessions with an error + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, }); createRoot(document.getElementById('root')).render( From caf1f7fdf13cd1d9dc8149ff2a2d29f02c488f4b Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Wed, 28 Feb 2024 22:07:37 -0500 Subject: [PATCH 03/68] build: update @mapbox/mapbox-gl-geocoder@5.0.2 mapbox-gl@3.1.2 --- package-lock.json | 14 ++++++++++---- package.json | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d311aa62..03464fa51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", - "mapbox-gl": "^2.15.0", + "mapbox-gl": "^3.1.2", "notistack": "^3.0.1", "path-browserify": "^1.0.1", "query-string": "^7.1.3", @@ -10642,6 +10642,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/cheap-ruler": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-3.0.2.tgz", + "integrity": "sha512-02T332h1/HTN6cDSufLP8x4JzDs2+VC+8qZ/N0kWIVPyc2xUkWwWh3B2fJxR7raXkL4Mq7k554mfuM9ofv/vGg==" + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -21765,9 +21770,9 @@ } }, "node_modules/mapbox-gl": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz", - "integrity": "sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.1.2.tgz", + "integrity": "sha512-+KoLEqZM8GxO/ViPz9tKgGURteKne+Y0pXiVCNsowymiZFH3FiL6dt9oZE95owMg5XqD3Kygz5mfchR1ZgmWlA==", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -21777,6 +21782,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", + "cheap-ruler": "^3.0.1", "csscolorparser": "~1.0.3", "earcut": "^2.2.4", "geojson-vt": "^3.2.1", diff --git a/package.json b/package.json index 5701c34bc..39e254e95 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", - "mapbox-gl": "^2.15.0", + "mapbox-gl": "^3.1.2", "notistack": "^3.0.1", "path-browserify": "^1.0.1", "query-string": "^7.1.3", From bf36eb1a4fbfa0ac6e7ac885e29c8c323378a4d7 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Wed, 28 Feb 2024 22:48:19 -0500 Subject: [PATCH 04/68] fix: define TextDecoder so tests pass with mapbox-gl 3 --- .../components/VisualizationConfigForm.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sharedData/visualization/components/VisualizationConfigForm.test.js b/src/sharedData/visualization/components/VisualizationConfigForm.test.js index 8b97993f7..12149f50f 100644 --- a/src/sharedData/visualization/components/VisualizationConfigForm.test.js +++ b/src/sharedData/visualization/components/VisualizationConfigForm.test.js @@ -14,6 +14,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import * as util from 'util'; import { act, fireEvent, render, screen, waitFor, within } from 'tests/utils'; import { useParams } from 'react-router-dom'; import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; @@ -29,6 +30,13 @@ import { MAP_DATA_ACCEPTED_EXTENSIONS, } from 'config'; +// mapbox-gl 3.x no longer has TextDecoder defined +// https://github.com/mapbox/mapbox-gl-js/issues/13027 +Object.defineProperty(window, 'TextDecoder', { + writable: true, + value: util.TextDecoder, +}); + jest.mock('terraso-client-shared/terrasoApi/api'); jest.mock('sharedData/visualization/visualizationMarkers'); jest.mock('react-router-dom', () => ({ From 8f97c82bb669393feb061c0288e969f8aeaf9809 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Mon, 11 Mar 2024 13:15:36 -0500 Subject: [PATCH 05/68] fix: Redirect user to complete profile after first login --- src/account/accountProfileUtils.js | 75 ++++++++++++++++++++++ src/account/components/AccountProfile.js | 23 ++++++- src/account/components/RequireAuth.js | 3 + src/account/components/RequireAuth.test.js | 63 +++++++++++++++++- src/localization/locales/en-US.json | 1 + src/navigation/components/Routes.js | 1 + 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/account/accountProfileUtils.js diff --git a/src/account/accountProfileUtils.js b/src/account/accountProfileUtils.js new file mode 100644 index 000000000..3b72d6230 --- /dev/null +++ b/src/account/accountProfileUtils.js @@ -0,0 +1,75 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { useEffect, useState } from 'react'; +import { jwtDecode } from 'jwt-decode'; +import { useNavigate } from 'react-router-dom'; +import { getToken } from 'terraso-client-shared/account/auth'; +import { useSelector } from 'terrasoApi/store'; + +const getCreatedWithService = async () => { + const token = await getToken(); + return token === undefined ? undefined : jwtDecode(token).createdWithService; +}; + +const getStoredCompletedProfile = email => { + const storedCompletedProfile = localStorage.getItem( + 'completedProfileDisplayed' + ); + try { + const parsed = JSON.parse(storedCompletedProfile); + return parsed[email]; + } catch (error) { + return false; + } +}; + +export const profileCompleted = email => { + if (!email) { + return; + } + localStorage.setItem( + 'completedProfileDisplayed', + JSON.stringify({ + [email]: true, + }) + ); +}; + +export const useCompleteProfile = () => { + const navigate = useNavigate(); + const { data: user } = useSelector(state => state.account.currentUser); + const [createdWithService, setCreatedWithService] = useState(); + + useEffect(() => { + getCreatedWithService().then(createdWithService => { + setCreatedWithService(createdWithService); + }); + }, []); + + useEffect(() => { + if (!createdWithService || !user?.email) { + return; + } + + const completedProfile = getStoredCompletedProfile(user?.email); + if (completedProfile) { + return; + } + + navigate('/account/profile/completeProfile'); + }, [createdWithService, user?.email, navigate]); +}; diff --git a/src/account/components/AccountProfile.js b/src/account/components/AccountProfile.js index cfdd805b2..3029a1b59 100644 --- a/src/account/components/AccountProfile.js +++ b/src/account/components/AccountProfile.js @@ -14,10 +14,11 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import _ from 'lodash/fp'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import { fetchProfile, savePreference, @@ -25,7 +26,13 @@ import { } from 'terraso-client-shared/account/accountSlice'; import { useFetchData } from 'terraso-client-shared/store/utils'; import * as yup from 'yup'; -import { Checkbox, FormControlLabel, Grid, Typography } from '@mui/material'; +import { + Alert, + Checkbox, + FormControlLabel, + Grid, + Typography, +} from '@mui/material'; import { withProps } from 'react-hoc'; @@ -36,6 +43,7 @@ import PageHeader from 'layout/PageHeader'; import PageLoader from 'layout/PageLoader'; import LocalePickerSelect from 'localization/components/LocalePickerSelect'; import { useAnalytics } from 'monitoring/analytics'; +import { profileCompleted } from 'account/accountProfileUtils'; import AccountAvatar from './AccountAvatar'; @@ -170,6 +178,7 @@ const AccountProfile = () => { const dispatch = useDispatch(); const { trackEvent } = useAnalytics(); const { t } = useTranslation(); + const { completeProfile } = useParams(); const { data: user, fetching } = useSelector(_.get('account.profile')); useFetchData(fetchProfile); @@ -177,6 +186,13 @@ const AccountProfile = () => { useDocumentTitle(t('account.profile_document_title')); useDocumentDescription(t('account.profile_document_description')); + useEffect( + () => () => { + profileCompleted(user?.email); + }, + [user?.email] + ); + const onSave = updatedProfile => { // Save user data dispatch( @@ -223,6 +239,9 @@ const AccountProfile = () => { return ( + {completeProfile && ( + {t('account.profile_complete_message')} + )}
{ const location = useLocation(); @@ -31,6 +32,8 @@ const RequireAuth = ({ children }) => { ); const hasToken = useSelector(state => state.account.hasToken); + useCompleteProfile(); + useFetchData( useCallback( () => (hasToken && !user ? fetchUser() : null), diff --git a/src/account/components/RequireAuth.test.js b/src/account/components/RequireAuth.test.js index 0feae1e54..3cf8a0a33 100644 --- a/src/account/components/RequireAuth.test.js +++ b/src/account/components/RequireAuth.test.js @@ -17,8 +17,8 @@ import { render, screen } from 'tests/utils'; import React from 'react'; import _ from 'lodash/fp'; -import { useLocation, useParams } from 'react-router-dom'; -import { getUserEmail } from 'terraso-client-shared/account/auth'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { getToken, getUserEmail } from 'terraso-client-shared/account/auth'; import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; import RequireAuth from 'account/components/RequireAuth'; @@ -29,17 +29,24 @@ jest.mock('terraso-client-shared/terrasoApi/api'); jest.mock('terraso-client-shared/account/auth', () => ({ ...jest.requireActual('terraso-client-shared/account/auth'), getUserEmail: jest.fn(), + getToken: jest.fn(), })); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn(), useLocation: jest.fn(), + useNavigate: jest.fn(), Navigate: props =>
To: {props.to}
, })); +// Payload: { "createdWithService": "google" } +const CREATED_WITH_SERVICE_TOKEN = + 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkV2l0aFNlcnZpY2UiOiJnb29nbGUifQ.aznynzRP1qRh-GVKPM_Xhi7ZhG7XuM7R6SIXNd7rfCo2bgnXJen3btm4VnpcVDalnCQPpp8e-1f7t8qlTLZu0Q'; + beforeEach(() => { global.fetch = jest.fn(); + useNavigate.mockReturnValue(jest.fn()); }); test('Auth: test redirect', async () => { @@ -186,3 +193,55 @@ test('Auth: test fetch user', async () => { expect(terrasoApi.requestGraphQL).toHaveBeenCalledTimes(2); }); + +test('Auth: Test redirect complete profile', async () => { + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + await render( + +
+ , + { + account: { + currentUser: { + data: { + email: 'test@test.com', + }, + }, + }, + } + ); + + expect(navigate).toHaveBeenCalledWith('/account/profile/completeProfile'); +}); + +test('Auth: Avoid redirect if profile complete already displayed for user', async () => { + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + + localStorage.setItem( + 'completedProfileDisplayed', + JSON.stringify({ + 'test@test.com': true, + }) + ); + + await render( + +
+ , + { + account: { + currentUser: { + data: { + email: 'test@test.com', + }, + }, + }, + } + ); + + expect(navigate).not.toHaveBeenCalled(); +}); diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 579dfb8e5..313415132 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -219,6 +219,7 @@ "profile_document_title": "Account", "profile_document_description": "View and edit your Terraso profile and preferences", "profile_form_label": "Manage profile", + "profile_complete_message": "We just need a few details about you before you can get started.", "form_language_label": "Language", "form_notifications_section_label": "Email notifications", "form_notifications_section_when_label": "Notify me when:", diff --git a/src/navigation/components/Routes.js b/src/navigation/components/Routes.js index 780af7a09..d91916154 100644 --- a/src/navigation/components/Routes.js +++ b/src/navigation/components/Routes.js @@ -185,6 +185,7 @@ const paths = [ breadcrumbsLabel: 'tools.breadcrumbs_list', }), path('/account', AccountLogin, { auth: false }), + path('/account/profile/:completeProfile', AccountProfile), path('/account/profile', AccountProfile), path('/contact', ContactForm), path('/tools/story-maps', StoryMapsToolsHome, { From 48924eed35272c09d5976072a001c17da69c42a9 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 10:10:29 -0500 Subject: [PATCH 06/68] fix: Changed createdWithService to isFirstLogin --- src/account/accountProfileUtils.js | 14 +++++++------- src/account/components/RequireAuth.test.js | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/account/accountProfileUtils.js b/src/account/accountProfileUtils.js index 3b72d6230..16bfc97be 100644 --- a/src/account/accountProfileUtils.js +++ b/src/account/accountProfileUtils.js @@ -20,9 +20,9 @@ import { useNavigate } from 'react-router-dom'; import { getToken } from 'terraso-client-shared/account/auth'; import { useSelector } from 'terrasoApi/store'; -const getCreatedWithService = async () => { +const getIsFirstLogin = async () => { const token = await getToken(); - return token === undefined ? undefined : jwtDecode(token).createdWithService; + return token === undefined ? undefined : jwtDecode(token).isFirstLogin; }; const getStoredCompletedProfile = email => { @@ -52,16 +52,16 @@ export const profileCompleted = email => { export const useCompleteProfile = () => { const navigate = useNavigate(); const { data: user } = useSelector(state => state.account.currentUser); - const [createdWithService, setCreatedWithService] = useState(); + const [isFirstLogin, setIsFirstLogin] = useState(); useEffect(() => { - getCreatedWithService().then(createdWithService => { - setCreatedWithService(createdWithService); + getIsFirstLogin().then(isFirstLogin => { + setIsFirstLogin(isFirstLogin); }); }, []); useEffect(() => { - if (!createdWithService || !user?.email) { + if (!isFirstLogin || !user?.email) { return; } @@ -71,5 +71,5 @@ export const useCompleteProfile = () => { } navigate('/account/profile/completeProfile'); - }, [createdWithService, user?.email, navigate]); + }, [isFirstLogin, user?.email, navigate]); }; diff --git a/src/account/components/RequireAuth.test.js b/src/account/components/RequireAuth.test.js index 3cf8a0a33..ab2856eb8 100644 --- a/src/account/components/RequireAuth.test.js +++ b/src/account/components/RequireAuth.test.js @@ -40,9 +40,9 @@ jest.mock('react-router-dom', () => ({ Navigate: props =>
To: {props.to}
, })); -// Payload: { "createdWithService": "google" } -const CREATED_WITH_SERVICE_TOKEN = - 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkV2l0aFNlcnZpY2UiOiJnb29nbGUifQ.aznynzRP1qRh-GVKPM_Xhi7ZhG7XuM7R6SIXNd7rfCo2bgnXJen3btm4VnpcVDalnCQPpp8e-1f7t8qlTLZu0Q'; +// Payload: { "isFirstLogin": true } +const IS_FIRST_LOGIN_TOKEN = + 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc0ZpcnN0TG9naW4iOnRydWV9.Z5WctUFTDZuFDAr0QiczFKIIx8qWzWJ38kiIHnGSiUQ29z7VQqGz9F5mfFfrt48sRob-fyw5sWxIxm3qbcxrEQ'; beforeEach(() => { global.fetch = jest.fn(); @@ -197,7 +197,7 @@ test('Auth: test fetch user', async () => { test('Auth: Test redirect complete profile', async () => { const navigate = jest.fn(); useNavigate.mockReturnValue(navigate); - getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + getToken.mockResolvedValue(IS_FIRST_LOGIN_TOKEN); await render(
@@ -219,7 +219,7 @@ test('Auth: Test redirect complete profile', async () => { test('Auth: Avoid redirect if profile complete already displayed for user', async () => { const navigate = jest.fn(); useNavigate.mockReturnValue(navigate); - getToken.mockResolvedValue(CREATED_WITH_SERVICE_TOKEN); + getToken.mockResolvedValue(IS_FIRST_LOGIN_TOKEN); localStorage.setItem( 'completedProfileDisplayed', From 7a1fd09ad49b8bfa43162586625f28e6f2c03ebd Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 10:12:06 -0500 Subject: [PATCH 07/68] chore: Added ES translation --- src/localization/locales/es-ES.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index 0b3e2020f..ccf2f11d0 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -219,6 +219,7 @@ "profile_document_title": "Cuenta", "profile_document_description": "Administrar las preferencias de su perfil de Terraso", "profile_form_label": "Administrar perfil", + "profile_complete_message": "Sólo necesitamos algunos detalles sobre ti antes de que poder comenzar.", "form_language_label": "Idioma", "form_notifications_section_label": "Notificaciónes de Correo Electrónico", "form_notifications_section_when_label": "Notifícame cuando:", From 0a58bf7e2bb1c25810e30462b9a23342d298d446 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 10:40:04 -0500 Subject: [PATCH 08/68] fix: Redirect user after save profile --- src/account/components/AccountProfile.js | 30 +++++--- src/account/components/AccountProfile.test.js | 68 +++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/account/components/AccountProfile.js b/src/account/components/AccountProfile.js index 3029a1b59..44702a2a3 100644 --- a/src/account/components/AccountProfile.js +++ b/src/account/components/AccountProfile.js @@ -18,7 +18,7 @@ import React, { useCallback, useEffect } from 'react'; import _ from 'lodash/fp'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { fetchProfile, savePreference, @@ -175,6 +175,7 @@ const ProfilePicture = () => { }; const AccountProfile = () => { + const navigate = useNavigate(); const dispatch = useDispatch(); const { trackEvent } = useAnalytics(); const { t } = useTranslation(); @@ -195,7 +196,7 @@ const AccountProfile = () => { const onSave = updatedProfile => { // Save user data - dispatch( + const saveUserPromise = dispatch( saveUser( _.omit( ['profilePicture', 'notifications', 'email'].concat( @@ -207,7 +208,7 @@ const AccountProfile = () => { ); // Save language and notifications preferences - PREFERENCE_KEYS.forEach(preferenceKey => { + const savePreferencesPromises = PREFERENCE_KEYS.map(preferenceKey => { const currentValue = _.get(['preferences', preferenceKey], user); const newValue = _.get(['preferences', preferenceKey], updatedProfile); @@ -215,19 +216,32 @@ const AccountProfile = () => { // database. newValue coments from user data and will be a string, // so the strict equality check below is not enough if (newValue === '' && typeof currentValue === 'undefined') { - return; + return null; } if (newValue !== currentValue) { - dispatch( - savePreference({ key: preferenceKey, value: newValue.toString() }) - ); - if (_.endsWith(preferenceKey, 'notifications')) { trackEvent('preference.update', { props: { emailNotifications: newValue }, }); } + + return dispatch( + savePreference({ key: preferenceKey, value: newValue.toString() }) + ); + } + return null; + }); + + const allPromises = [saveUserPromise, ...savePreferencesPromises].filter( + promise => !!promise + ); + Promise.all(allPromises).then(responses => { + const allSuccess = responses.every( + response => _.get('meta.requestStatus', response) === 'fulfilled' + ); + if (allSuccess) { + navigate('/account/profile'); } }); }; diff --git a/src/account/components/AccountProfile.test.js b/src/account/components/AccountProfile.test.js index 5eae2dc26..ddc1327a1 100644 --- a/src/account/components/AccountProfile.test.js +++ b/src/account/components/AccountProfile.test.js @@ -16,14 +16,22 @@ */ import { fireEvent, render, screen, within } from 'tests/utils'; import React from 'react'; +import { when } from 'jest-when'; import _ from 'lodash/fp'; import { act } from 'react-dom/test-utils'; +import { useNavigate, useParams } from 'react-router-dom'; import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; import AccountProfile from 'account/components/AccountProfile'; jest.mock('terraso-client-shared/terrasoApi/api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), +})); + const setup = async ( initialState = { account: { @@ -52,6 +60,11 @@ const setup = async ( }; }; +beforeEach(() => { + useNavigate.mockReturnValue(jest.fn()); + useParams.mockReturnValue({}); +}); + test('AccountProfile: Display Avatar', async () => { terrasoApi.requestGraphQL.mockReturnValue( Promise.resolve( @@ -368,3 +381,58 @@ test('AccountProfile: Save error', async () => { // Test error display expect(screen.getByText(/Save Error/i)).toBeInTheDocument(); }); + +test('AccountProfile: Complete profile', async () => { + useParams.mockReturnValue({ + completeProfile: 'completeProfile', + }); + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + when(terrasoApi.requestGraphQL) + .calledWith(expect.stringContaining('query userProfile'), expect.anything()) + .mockReturnValue( + Promise.resolve( + _.set( + 'users.edges[0].node', + { + id: 'user-id', + firstName: 'John', + lastName: 'Doe', + email: 'group@group.org', + profileImage: '', + preferences: { edges: [] }, + }, + {} + ) + ) + ); + + when(terrasoApi.requestGraphQL) + .calledWith( + expect.stringContaining('mutation updateUser'), + expect.anything() + ) + .mockResolvedValue( + _.set( + 'updateUser.user', + { + id: '1', + firstName: 'Pablo', + lastName: 'Perez', + email: 'group@group.org', + profileImage: 'https://www.group.org/image.jpg', + preferences: { + edges: [{ node: { key: 'language', value: 'es-ES' } }], + }, + }, + {} + ) + ); + + const { inputs } = await setup(); + + await act(async () => + fireEvent.click(screen.getByRole('button', { name: 'Save Profile' })) + ); + expect(navigate).toHaveBeenCalledWith('/account/profile'); +}); From bb6db2ef0a449652ee9cd5cb68e480431d400e94 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 10:45:52 -0500 Subject: [PATCH 09/68] fix: Added text validation, and linter fix --- src/account/components/AccountProfile.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/account/components/AccountProfile.test.js b/src/account/components/AccountProfile.test.js index ddc1327a1..33f13b060 100644 --- a/src/account/components/AccountProfile.test.js +++ b/src/account/components/AccountProfile.test.js @@ -429,8 +429,10 @@ test('AccountProfile: Complete profile', async () => { ) ); - const { inputs } = await setup(); + await setup(); + expect(screen.getByRole('alert')).toHaveTextContent('We just need a few details about you before you can get started.') + await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Save Profile' })) ); From f01a0bd23031532466f03967a22c48f472282e00 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 11:03:31 -0500 Subject: [PATCH 10/68] fix: added form outlined property --- src/account/components/AccountProfile.js | 33 +++-- src/forms/components/Form.js | 170 ++++++++++++----------- 2 files changed, 108 insertions(+), 95 deletions(-) diff --git a/src/account/components/AccountProfile.js b/src/account/components/AccountProfile.js index 44702a2a3..8563757ae 100644 --- a/src/account/components/AccountProfile.js +++ b/src/account/components/AccountProfile.js @@ -31,6 +31,7 @@ import { Checkbox, FormControlLabel, Grid, + Paper, Typography, } from '@mui/material'; @@ -253,19 +254,27 @@ const AccountProfile = () => { return ( - {completeProfile && ( - {t('account.profile_complete_message')} - )} - + + {completeProfile && ( + ({ m: spacing(3, 3, 0, 3) })} + > + {t('account.profile_complete_message')} + + )} + + ); }; diff --git a/src/forms/components/Form.js b/src/forms/components/Form.js index 06d1f9977..dca6b0381 100644 --- a/src/forms/components/Form.js +++ b/src/forms/components/Form.js @@ -24,7 +24,9 @@ import { useFormState, } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { Button, Grid } from '@mui/material'; +import { Button, Grid, Paper } from '@mui/material'; + +import { withProps } from 'react-hoc'; import FormField from 'forms/components/FormField'; import { useFormSetContext } from 'forms/formContext'; @@ -52,6 +54,7 @@ const Form = props => { onChange, filterField, gridContainerProps = {}, + outlined = true, } = props; const setFormContext = useFormSetContext(); @@ -150,91 +153,92 @@ const Form = props => { ]); }, [saveLabel, t, onCancel, cancelLabel, isMultiStep]); + const Container = outlined + ? withProps(Paper, { variant: 'outlined' }) + : React.Fragment; + return ( - - - {fields - .filter(field => - filterField ? filterField(field, { getValues }) : true - ) - .map(field => - field.renderStaticElement ? ( - - {field.renderStaticElement({ t })} - - ) : ( - - ( - - )} - /> - + + + + {fields + .filter(field => + filterField ? filterField(field, { getValues }) : true ) + .map(field => + field.renderStaticElement ? ( + + {field.renderStaticElement({ t })} + + ) : ( + + ( + + )} + /> + + ) + )} + {children} + {!_.isEmpty(actions) && ( + + {isMultiStep ? actions.reverse() : actions} + )} - {children} - {!_.isEmpty(actions) && ( - - {isMultiStep ? actions.reverse() : actions} - - )} - - + + + ); }; From 81a969dd90662d4cf5939321f8db746091a1dfb1 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 11:03:46 -0500 Subject: [PATCH 11/68] fix: Linter fix --- src/account/components/AccountProfile.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/account/components/AccountProfile.test.js b/src/account/components/AccountProfile.test.js index 33f13b060..99a3ad427 100644 --- a/src/account/components/AccountProfile.test.js +++ b/src/account/components/AccountProfile.test.js @@ -431,8 +431,10 @@ test('AccountProfile: Complete profile', async () => { await setup(); - expect(screen.getByRole('alert')).toHaveTextContent('We just need a few details about you before you can get started.') - + expect(screen.getByRole('alert')).toHaveTextContent( + 'We just need a few details about you before you can get started.' + ); + await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Save Profile' })) ); From 99147af9438379ee9bd7e754adbff4232e18e62d Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 11:27:07 -0500 Subject: [PATCH 12/68] fix: Reverted form changes, just set border --- src/forms/components/Form.js | 173 ++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/src/forms/components/Form.js b/src/forms/components/Form.js index dca6b0381..48522bb46 100644 --- a/src/forms/components/Form.js +++ b/src/forms/components/Form.js @@ -24,9 +24,7 @@ import { useFormState, } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { Button, Grid, Paper } from '@mui/material'; - -import { withProps } from 'react-hoc'; +import { Button, Grid } from '@mui/material'; import FormField from 'forms/components/FormField'; import { useFormSetContext } from 'forms/formContext'; @@ -153,92 +151,95 @@ const Form = props => { ]); }, [saveLabel, t, onCancel, cancelLabel, isMultiStep]); - const Container = outlined - ? withProps(Paper, { variant: 'outlined' }) - : React.Fragment; - return ( - - - - {fields - .filter(field => - filterField ? filterField(field, { getValues }) : true + + + {fields + .filter(field => + filterField ? filterField(field, { getValues }) : true + ) + .map(field => + field.renderStaticElement ? ( + + {field.renderStaticElement({ t })} + + ) : ( + + ( + + )} + /> + ) - .map(field => - field.renderStaticElement ? ( - - {field.renderStaticElement({ t })} - - ) : ( - - ( - - )} - /> - - ) - )} - {children} - {!_.isEmpty(actions) && ( - - {isMultiStep ? actions.reverse() : actions} - )} - - - + {children} + {!_.isEmpty(actions) && ( + + {isMultiStep ? actions.reverse() : actions} + + )} + + ); }; From 808b98570a7b51e9ff0aec26fc51cbbeb7de191a Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Tue, 12 Mar 2024 13:00:16 -0400 Subject: [PATCH 13/68] fix: remove name helper text --- src/account/components/AccountProfile.js | 1 - src/localization/locales/en-US.json | 1 - src/localization/locales/es-ES.json | 1 - 3 files changed, 3 deletions(-) diff --git a/src/account/components/AccountProfile.js b/src/account/components/AccountProfile.js index 8563757ae..2a9727002 100644 --- a/src/account/components/AccountProfile.js +++ b/src/account/components/AccountProfile.js @@ -62,7 +62,6 @@ const FIELDS = [ { name: 'firstName', label: 'account.form_first_name_label', - info: 'account.form_first_name_info', props: { gridItemProps: { xs: 12, diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 313415132..642328886 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -208,7 +208,6 @@ "form_save_label": "Save Profile", "save_success": "Your profile has been saved.", "not_found": "Account not found", - "form_first_name_info": "This name will be used to greet you in Terraso.", "welcome_to": "Welcome to", "profile_picture": "Profile Picture", "apple_login": "Sign in with Apple", diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index ccf2f11d0..6402a384c 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -209,7 +209,6 @@ "save_success": "Su perfil se ha guardado.", "not_found": "Cuenta no encontrada", "welcome_to": "Bienvenido a", - "form_first_name_info": "Este nombre se utilizará para saludarlo en Terraso.", "profile_picture": "Fotografía de perfil", "apple_login": "Acceder con Apple", "google_login": "Acceder con Google", From bfd234d2791805e8e00f81c2b7a019bd51356da1 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Tue, 12 Mar 2024 13:00:53 -0400 Subject: [PATCH 14/68] fix: updated profile completion prompt --- src/localization/locales/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 642328886..4320eb819 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -218,7 +218,7 @@ "profile_document_title": "Account", "profile_document_description": "View and edit your Terraso profile and preferences", "profile_form_label": "Manage profile", - "profile_complete_message": "We just need a few details about you before you can get started.", + "profile_complete_message": "Tell us what to call you and review your profile settings.", "form_language_label": "Language", "form_notifications_section_label": "Email notifications", "form_notifications_section_when_label": "Notify me when:", From aa4dffe06a1dbc7fe5f6e53a6a2fcf8f849ead95 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Tue, 12 Mar 2024 14:10:29 -0400 Subject: [PATCH 15/68] fix: update test string to match new text --- src/account/components/AccountProfile.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account/components/AccountProfile.test.js b/src/account/components/AccountProfile.test.js index 99a3ad427..477290e1e 100644 --- a/src/account/components/AccountProfile.test.js +++ b/src/account/components/AccountProfile.test.js @@ -432,7 +432,7 @@ test('AccountProfile: Complete profile', async () => { await setup(); expect(screen.getByRole('alert')).toHaveTextContent( - 'We just need a few details about you before you can get started.' + 'Tell us what to call you and review your profile settings.' ); await act(async () => From 8b77c44090799ac8c8b06748b00a6ec43dbf8932 Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Tue, 12 Mar 2024 13:32:10 -0500 Subject: [PATCH 16/68] chore: Added ES translation --- src/localization/locales/es-ES.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index 6402a384c..6c45f8e4f 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -218,7 +218,7 @@ "profile_document_title": "Cuenta", "profile_document_description": "Administrar las preferencias de su perfil de Terraso", "profile_form_label": "Administrar perfil", - "profile_complete_message": "Sólo necesitamos algunos detalles sobre ti antes de que poder comenzar.", + "profile_complete_message": "Cuentanos cómo llamarte y revisa la configuración de tu perfil.", "form_language_label": "Idioma", "form_notifications_section_label": "Notificaciónes de Correo Electrónico", "form_notifications_section_when_label": "Notifícame cuando:", From 7110c18da942b855df9d7b648dc6ae48fa26b947 Mon Sep 17 00:00:00 2001 From: Ruxandra Machedon Date: Fri, 8 Mar 2024 12:41:39 -0500 Subject: [PATCH 17/68] fix: make nav blocker tool disable on save to avoid react event order bugs --- src/navigation/navigationContext.js | 41 ++++++++---- src/navigation/navigationContext.test.js | 65 +++++++++++++++++++ .../VisualizationConfigForm/index.js | 9 +-- src/storyMap/components/StoryMapForm/index.js | 4 +- 4 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 src/navigation/navigationContext.test.js diff --git a/src/navigation/navigationContext.js b/src/navigation/navigationContext.js index 02da93df4..1a383e2a7 100644 --- a/src/navigation/navigationContext.js +++ b/src/navigation/navigationContext.js @@ -26,7 +26,16 @@ export const useNavigationBlocker = (when, message) => { const [isBlocked, setIsBlocked] = useState(false); const [blockedArgs, setBlockedArgs] = useState(); - const unblock = useCallback(() => { + /* + * we cache a reference to the original push method if we don't have one yet, + * so that we can be sure we're restoring the original when the blocker is + * disabled or otherwised cleaned up. + */ + if (!navigator.originalPush) { + navigator.originalPush = navigator.push; + } + + const proceed = useCallback(() => { const to = blockedArgs[0]; const options = blockedArgs[2]; navigate(to, { @@ -34,26 +43,35 @@ export const useNavigationBlocker = (when, message) => { force: true, }); }, [blockedArgs, navigate]); + const cancel = useCallback(() => { setIsBlocked(false); }, []); + const preventNavigation = useCallback( + event => { + event.preventDefault(); + event.returnValue = message; + }, + [message] + ); + + const disable = useCallback(() => { + window.removeEventListener('beforeunload', preventNavigation); + navigator.push = navigator.originalPush; + }, [preventNavigation, navigator]); + useEffect(() => { if (!when) { setIsBlocked(false); return; } - const beforeUnload = event => { - event.preventDefault(); - event.returnValue = message; - }; - window.addEventListener('beforeunload', beforeUnload); - const push = navigator.push; + window.addEventListener('beforeunload', preventNavigation); navigator.push = (...args) => { const options = args[2]; if (options?.force) { - push(...args); + navigator.originalPush(...args); return; } @@ -62,10 +80,9 @@ export const useNavigationBlocker = (when, message) => { }; return () => { - window.removeEventListener('beforeunload', beforeUnload); - navigator.push = push; + disable(); }; - }, [when, message, navigator]); + }, [when, navigator, preventNavigation, disable]); - return { isBlocked, unblock, cancel }; + return { isBlocked, proceed, cancel, disable }; }; diff --git a/src/navigation/navigationContext.test.js b/src/navigation/navigationContext.test.js new file mode 100644 index 000000000..11f728471 --- /dev/null +++ b/src/navigation/navigationContext.test.js @@ -0,0 +1,65 @@ +/* + * Copyright © 2024 Technology Matters + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { render } from 'tests/utils'; + +import { useNavigationBlocker } from './navigationContext'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), + useLocation: jest.fn(), +})); + +/* + * These tests do not cover all of the blocker system's behavior, but verifying that + * the event handler was added / removed was considered sufficient for an initial pass. + */ + +test('Enable navigation blocker', async () => { + const Component = () => { + useNavigationBlocker(true, 'test'); + return
; + }; + + window.addEventListener = jest.fn(); + + await render(); + + expect(window.addEventListener).toHaveBeenCalledWith( + 'beforeunload', + expect.any(Function) + ); +}); + +test('Disable navigation blocker', async () => { + const Component = () => { + const { disable } = useNavigationBlocker(true, 'test'); + disable(); + return
; + }; + + window.removeEventListener = jest.fn(); + + await render(); + + expect(window.removeEventListener).toHaveBeenCalledWith( + 'beforeunload', + expect.any(Function) + ); +}); diff --git a/src/sharedData/visualization/components/VisualizationConfigForm/index.js b/src/sharedData/visualization/components/VisualizationConfigForm/index.js index e6cb727a6..5b4f3b983 100644 --- a/src/sharedData/visualization/components/VisualizationConfigForm/index.js +++ b/src/sharedData/visualization/components/VisualizationConfigForm/index.js @@ -151,7 +151,7 @@ const VisualizationConfigForm = props => { useState(INITIAL_CONFIG); const [isDirty, setIsDirty] = useState(false); - const { isBlocked, unblock, cancel } = useNavigationBlocker( + const { isBlocked, proceed, cancel, disable } = useNavigationBlocker( isDirty, t('sharedData.visualization_unsaved_changes_message') ); @@ -173,10 +173,11 @@ const VisualizationConfigForm = props => { const onCompleteSuccessWrapper = useCallback( mapSlug => { - onCompleteSuccess(mapSlug); setIsDirty(false); + disable(); + onCompleteSuccess(mapSlug); }, - [onCompleteSuccess] + [onCompleteSuccess, disable] ); return ( @@ -185,7 +186,7 @@ const VisualizationConfigForm = props => { )} diff --git a/src/storyMap/components/StoryMapForm/index.js b/src/storyMap/components/StoryMapForm/index.js index 5190bfacd..4ec0bd5f3 100644 --- a/src/storyMap/components/StoryMapForm/index.js +++ b/src/storyMap/components/StoryMapForm/index.js @@ -110,7 +110,7 @@ const StoryMapForm = props => { [] ); - const { isBlocked, unblock, cancel } = useNavigationBlocker( + const { isBlocked, proceed, cancel } = useNavigationBlocker( isDirty, t('storyMap.form_unsaved_changes_message') ); @@ -235,7 +235,7 @@ const StoryMapForm = props => { )} From c8dc5a15c64db82ccb8a937be2a705e6903555ba Mon Sep 17 00:00:00 2001 From: Jose Buitron Date: Fri, 15 Mar 2024 12:00:01 -0500 Subject: [PATCH 18/68] feat: Added referrer to account profile --- src/account/accountProfileUtils.js | 18 ++++- src/account/components/AccountLogin.js | 42 +++--------- src/account/components/AccountProfile.js | 8 ++- src/account/components/AccountProfile.test.js | 68 ++++++++++++++++++- src/account/components/RequireAuth.js | 14 +--- src/account/components/RequireAuth.test.js | 38 ++++++++++- src/navigation/navigationUtils.js | 62 +++++++++++++++++ 7 files changed, 195 insertions(+), 55 deletions(-) diff --git a/src/account/accountProfileUtils.js b/src/account/accountProfileUtils.js index 16bfc97be..fe9e1f721 100644 --- a/src/account/accountProfileUtils.js +++ b/src/account/accountProfileUtils.js @@ -16,10 +16,12 @@ */ import { useEffect, useState } from 'react'; import { jwtDecode } from 'jwt-decode'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { getToken } from 'terraso-client-shared/account/auth'; import { useSelector } from 'terrasoApi/store'; +import { generateReferrerUrl } from 'navigation/navigationUtils'; + const getIsFirstLogin = async () => { const token = await getToken(); return token === undefined ? undefined : jwtDecode(token).isFirstLogin; @@ -50,6 +52,7 @@ export const profileCompleted = email => { }; export const useCompleteProfile = () => { + const location = useLocation(); const navigate = useNavigate(); const { data: user } = useSelector(state => state.account.currentUser); const [isFirstLogin, setIsFirstLogin] = useState(); @@ -70,6 +73,15 @@ export const useCompleteProfile = () => { return; } - navigate('/account/profile/completeProfile'); - }, [isFirstLogin, user?.email, navigate]); + if (location.pathname === '/account/profile/completeProfile') { + return; + } + + const to = generateReferrerUrl( + '/account/profile/completeProfile', + location + ); + + navigate(to); + }, [isFirstLogin, user?.email, navigate, location]); }; diff --git a/src/account/components/AccountLogin.js b/src/account/components/AccountLogin.js index 6f43f9d68..f5d66791f 100644 --- a/src/account/components/AccountLogin.js +++ b/src/account/components/AccountLogin.js @@ -15,10 +15,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ import React, { useEffect } from 'react'; -import queryString from 'query-string'; import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useNavigate, useSearchParams } from 'react-router-dom'; import { fetchAuthURLs } from 'terraso-client-shared/account/accountSlice'; import { ReactComponent as GoogleLogo } from 'terraso-client-shared/assets/google.svg'; import { ReactComponent as MicrosoftLogo } from 'terraso-client-shared/assets/microsoft.svg'; @@ -35,6 +33,7 @@ import PageHeader from 'layout/PageHeader'; import PageLoader from 'layout/PageLoader'; import LocalePicker from 'localization/components/LocalePicker'; import { useAnalytics } from 'monitoring/analytics'; +import { useReferrer } from 'navigation/navigationUtils'; import logo from 'assets/logo.svg'; @@ -42,50 +41,25 @@ import logo from 'assets/logo.svg'; const MicrosoftIcon = withProps(SvgIcon, { component: MicrosoftLogo }); const GoogleIcon = withProps(SvgIcon, { component: GoogleLogo }); -const appendReferrer = (url, referrer) => { - if (!referrer) { - return url; - } - const parsedUrl = queryString.parseUrl(url); - const redirectUrl = queryString.stringifyUrl({ - url: 'account', - query: { - referrerBase64: btoa(referrer), - }, - }); - return queryString.stringifyUrl({ - ...parsedUrl, - query: { - ...parsedUrl.query, - state: redirectUrl, - }, - }); -}; - const AccountForm = () => { const { t } = useTranslation(); const { trackEvent } = useAnalytics(); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const { fetching, urls } = useSelector(state => state.account.login); const hasToken = useSelector(state => state.account.hasToken); - const referrer = searchParams.get('referrer'); - const referrerBase64 = searchParams.get('referrerBase64'); useDocumentTitle(t('account.login_document_title')); useDocumentDescription(t('account.login_document_description')); useFetchData(fetchAuthURLs); + const { goToReferrer, appendReferrerBase64 } = useReferrer(); + useEffect(() => { if (!hasToken) { return; } - const url = referrerBase64 ? atob(referrerBase64) : referrer; - navigate(url ? decodeURIComponent(url) : '/', { - replace: true, - }); - }, [hasToken, navigate, referrer, referrerBase64]); + goToReferrer(); + }, [hasToken, goToReferrer]); if (fetching) { return ; @@ -124,7 +98,7 @@ const AccountForm = () => { startIcon={ } - href={appendReferrer(urls.google, referrer)} + href={appendReferrerBase64(urls.google)} onClick={() => trackEvent('user.login', { props: { source: 'google' } }) } @@ -141,7 +115,7 @@ const AccountForm = () => { sx={{ paddingLeft: '24px', paddingRight: '5px' }} /> } - href={appendReferrer(urls.microsoft, referrer)} + href={appendReferrerBase64(urls.microsoft)} onClick={() => trackEvent('user.login', { props: { source: 'microsoft' } }) } @@ -154,7 +128,7 @@ const AccountForm = () => { + { const { areaPolygon, setAreaPolygon, setEditHelp } = props; @@ -137,15 +137,17 @@ const OptionDrawPolygon = props => { first - third From 0e4d8afe2d80fea776cd6c9ced3652480ab6e1e2 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Thu, 21 Mar 2024 15:42:32 -0400 Subject: [PATCH 64/68] fix: update basemap label --- src/localization/locales/en-US.json | 2 +- src/localization/locales/es-ES.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index ca127b9d3..9f63825e1 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -384,7 +384,6 @@ "form_boundary_options_draw_polygon": "Draw the landscape’s boundary on a map", "form_boundary_draw_polygon_edit_help": "How to edit: Select a point and drag it to adjust the shape. Select and drag a smaller yellow point to add more points. Remove points using the Delete button. When finished, select {{saveLabel}}.", "form_boundary_draw_polygon_edit_help_draw_polygon": "How to add your boundary: Select a point on the map at the edge of your landscape. Repeat to make a rough outline of your boundary. When finished, select your first point.", - "form_boundary_map_basemap_label": "Background map", "breadcrumbs_visualization": "{{mapTitle}}", "breadcrumbs_visualization_new": "New Map", "breadcrumbs_upload": "Share Files and Links", @@ -782,6 +781,7 @@ "map_search_placeholder": "Enter a city, town or region", "map_search_clear": "Clear search", "default_marker_label": "Marker", + "basemap_label": "Set basemap", "latitude": "latitude", "longitude": "longitude", "boundaries_file_empty": "The file {{name}} is empty.", diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index fe0c63bdf..2c07132bf 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -388,7 +388,6 @@ "form_boundary_options_draw_polygon": "Dibujar el límite del paisaje en un mapa", "form_boundary_draw_polygon_edit_help": "Cómo editar: selecciona un punto y arrástralo para ajustar la forma. Selecciona y arrastra un punto amarillo más pequeño para agregar más puntos. Elimina puntos usando el botón Eliminar. Cuando hayas terminado, selecciona {{saveLabel}}.", "form_boundary_draw_polygon_edit_help_draw_polygon": "Cómo agregar su límite: selecciona un punto en el mapa en el borde de tu paisaje. Repite para hacer un contorno aproximado de tu límite. Cuando hayas terminado, selecciona el primer punto.", - "form_boundary_map_basemap_label": "Mapa de fondo", "breadcrumbs_visualization": "{{mapTitle}}", "breadcrumbs_visualization_new": "Nuevo mapa", "breadcrumbs_upload": "Compartir archivos y enlaces", @@ -789,6 +788,7 @@ "map_layer_streets": "Calles", "map_layer_satellite": "Satélite", "default_marker_label": "Marcador", + "basemap_label": "TKTKTK", "latitude": "latitud", "longitude": "longitud", "boundaries_file_empty": "El archivo {{name}} está vacío.", From c82f89d551a5b56f1902c872ea900459cb3f48ac Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Thu, 21 Mar 2024 15:45:49 -0400 Subject: [PATCH 65/68] fix: add labels for text alignment buttons --- src/localization/locales/en-US.json | 6 +++--- src/localization/locales/es-ES.json | 6 +++--- src/storyMap/components/StoryMapForm/ChapterForm.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 9f63825e1..22ba59131 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -916,9 +916,9 @@ "form_delete_chapter_confirm_button_tooltip": "Delete chapter “{{name}}”", "form_chapter_location_button": "Set Map Location", "form_chapter_alignment_buttons": "Set alignment", - "form_chapter_alignment_left": "Left", - "form_chapter_alignment_center": "Center", - "form_chapter_alignment_right": "Right", + "form_chapter_alignment_left": "Align Left", + "form_chapter_alignment_center": "Align Center", + "form_chapter_alignment_right": "Align Right", "form_chapter_menu_label": "{{chapterLabel}} menu", "form_chapter_move_up": "Move Chapter Up", "form_chapter_move_down": "Move Chapter Down", diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index 2c07132bf..650c6614a 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -923,9 +923,9 @@ "form_delete_chapter_confirm_button_tooltip": "Eliminar el capítulo “{{name}}”", "form_chapter_location_button": "Establecer ubicación en el mapa", "form_chapter_alignment_buttons": "Establecer alineación", - "form_chapter_alignment_left": "Izquierda", - "form_chapter_alignment_center": "Centro", - "form_chapter_alignment_right": "Derecha", + "form_chapter_alignment_left": "alinear a la izquierda", + "form_chapter_alignment_center": "Alinear al centro", + "form_chapter_alignment_right": "Alinear a la derecha", "form_chapter_menu_label": "{{chapterLabel}} menú", "form_chapter_move_up": "Mover capítulo hacia arriba", "form_chapter_move_down": "Mover capítulo hacia abajo", diff --git a/src/storyMap/components/StoryMapForm/ChapterForm.js b/src/storyMap/components/StoryMapForm/ChapterForm.js index 33a8f3597..049120779 100644 --- a/src/storyMap/components/StoryMapForm/ChapterForm.js +++ b/src/storyMap/components/StoryMapForm/ChapterForm.js @@ -124,7 +124,7 @@ const ChapterConfig = props => { {options.map(option => ( onAlignmentChange(option.value)} > From 8cdf86a12ce58291b022ea28249dd421b0f11d27 Mon Sep 17 00:00:00 2001 From: Carissa Knipe Date: Thu, 21 Mar 2024 12:55:49 -0700 Subject: [PATCH 66/68] fix: move uppercasing text to the view layer. This also fixes failing tests. --- src/sharedData/components/SharedDataEntryBase.js | 2 +- src/sharedData/components/SharedDataEntryFile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharedData/components/SharedDataEntryBase.js b/src/sharedData/components/SharedDataEntryBase.js index 9ecbcbb04..2ca31effe 100644 --- a/src/sharedData/components/SharedDataEntryBase.js +++ b/src/sharedData/components/SharedDataEntryBase.js @@ -211,7 +211,7 @@ const SharedDataEntryBase = props => { xs={2} md={1} order={{ xs: 5, md: 4 }} - sx={{ wordWrap: 'break-word' }} + sx={{ wordWrap: 'break-word', textTransform: 'uppercase' }} > {resourceType} diff --git a/src/sharedData/components/SharedDataEntryFile.js b/src/sharedData/components/SharedDataEntryFile.js index c3255ba46..a6ba2ccd7 100644 --- a/src/sharedData/components/SharedDataEntryFile.js +++ b/src/sharedData/components/SharedDataEntryFile.js @@ -360,7 +360,7 @@ const SharedDataEntryFile = props => { DownloadComponent={DownloadComponent} ShareComponent={ShareComponent} fileSize={filesize(sharedResource.dataEntry.size, { round: 0 })} - resourceType={sharedResource.dataEntry.resourceType.toUpperCase()} + resourceType={sharedResource.dataEntry.resourceType} > From 0ef244780d52fdc574bae2c2391d192cb8beb39d Mon Sep 17 00:00:00 2001 From: Carissa Knipe Date: Thu, 21 Mar 2024 14:07:06 -0700 Subject: [PATCH 67/68] test: add expectation of a file name and its extension type --- src/landscape/components/LandscapeView.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/landscape/components/LandscapeView.test.js b/src/landscape/components/LandscapeView.test.js index b6e6a1c0d..a64d9ec5a 100644 --- a/src/landscape/components/LandscapeView.test.js +++ b/src/landscape/components/LandscapeView.test.js @@ -122,6 +122,7 @@ const baseViewTest = async ( description: `Description ${index}`, size: 3456, entryType: 'FILE', + resourceType: 'txt', visualizations: { edges: [] }, }, }, @@ -233,6 +234,9 @@ test('LandscapeView: Display data', async () => { const entriesList = within(sharedDataRegion.getByRole('list')); const items = entriesList.getAllByRole('listitem'); expect(items.length).toBe(6); + const firstEntry = within(items[0]); + expect(firstEntry.getByText('Data Entry 0')).toBeInTheDocument(); + expect(firstEntry.getByText('txt')).toBeInTheDocument(); // Boundary expect( From cdd6a3ca27ae7a0e67f092b40d4e54d9929fbc1d Mon Sep 17 00:00:00 2001 From: Carissa Knipe Date: Fri, 22 Mar 2024 12:12:32 -0700 Subject: [PATCH 68/68] fix: set English and Spanish localization for date and author of shared files --- src/localization/locales/en-US.json | 1 + src/localization/locales/es-ES.json | 1 + src/sharedData/components/SharedDataEntryBase.js | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json index 22ba59131..816985ff2 100644 --- a/src/localization/locales/en-US.json +++ b/src/localization/locales/en-US.json @@ -523,6 +523,7 @@ "title": "Shared files and Links", "description_with_files": "Only members of your {{entityType}} can download these files or create a map with them.", "card_description": "Upload documents and share links with other members. Make maps with data files (latitude and longitude required) and map files (GeoJSON, KML, KMZ, ESRI shapefiles).", + "file_date_and_author": "{{date}}, by $t(user.full_name)", "upload_button": "Share Files and Links", "delete_confirm_title": "Delete “{{name}}”?", "delete_file_confirm_message": "Are you sure you wish to delete the file “{{name}}”?", diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json index 650c6614a..023bc0202 100644 --- a/src/localization/locales/es-ES.json +++ b/src/localization/locales/es-ES.json @@ -570,6 +570,7 @@ "title": "Archivos y enlaces compartidos", "description_with_files": "Solo los miembros de tu {{entityType}} pueden descargar estos archivos o crear un mapa con ellos.", "card_description": "Carga documentos y comparte enlaces con otros miembros. Crea mapas con archivos de datos (se requieren latitud y longitud) y archivos de mapas (GeoJSON, KML, KMZ, Shapefiles de ESRI).", + "file_date_and_author": "{{date}}, por $t(user.full_name)", "upload_button": "Compartir archivos y enlaces", "delete_confirm_title": "Eliminar “{{name}}”?", "delete_file_confirm_message": "¿Estás segura(o) de que deseas eliminar el archivo “{{name}}”?", diff --git a/src/sharedData/components/SharedDataEntryBase.js b/src/sharedData/components/SharedDataEntryBase.js index 5ac36e58e..57df65c24 100644 --- a/src/sharedData/components/SharedDataEntryBase.js +++ b/src/sharedData/components/SharedDataEntryBase.js @@ -227,8 +227,10 @@ const SharedDataEntryBase = props => { - {formatDate(i18n.resolvedLanguage, dataEntry.createdAt)}, by{' '} - {t('user.full_name', { user: dataEntry.createdBy })} + {t('sharedData.file_date_and_author', { + date: formatDate(i18n.resolvedLanguage, dataEntry.createdAt), + user: dataEntry.createdBy, + })}