diff --git a/package-lock.json b/package-lock.json index 0db865e98..47ba038a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-redux": "^8.1.2", - "terraso-backend": "github:techmatters/terraso-backend#4170565", + "terraso-backend": "github:techmatters/terraso-backend#0ac91bf", "uuid": "^9.0.0" }, "devDependencies": { @@ -13101,7 +13101,7 @@ }, "node_modules/terraso-backend": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/techmatters/terraso-backend.git#4170565525a1b85462685d4e2c861a3cfeec7fba" + "resolved": "git+ssh://git@github.com/techmatters/terraso-backend.git#0ac91bfc6a0ec7c6e8a0a7e409ec95e969459734" }, "node_modules/test-exclude": { "version": "6.0.0", diff --git a/package.json b/package.json index f45f2b993..ba7b99476 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-redux": "^8.1.2", - "terraso-backend": "github:techmatters/terraso-backend#4170565", + "terraso-backend": "github:techmatters/terraso-backend#0ac91bf", "uuid": "^9.0.0" }, "scripts": { diff --git a/src/project/projectSlice.ts b/src/project/projectSlice.ts index 48c73cfa5..3fd3545b6 100644 --- a/src/project/projectSlice.ts +++ b/src/project/projectSlice.ts @@ -15,11 +15,11 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { createAction, createSlice } from '@reduxjs/toolkit'; +import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { setUsers, User } from 'terraso-client-shared/account/accountSlice'; import { UserRole } from 'terraso-client-shared/graphqlSchema/graphql'; import * as projectService from 'terraso-client-shared/project/projectService'; -import { setSites, Site } from 'terraso-client-shared/site/siteSlice'; +import { Site, updateSites } from 'terraso-client-shared/site/siteSlice'; import { createAsyncThunk, dehydrated, @@ -30,7 +30,7 @@ const { plural: dehydrateProjects, sing: dehydrateProject } = dehydrated< HydratedProject >({ users: setUsers, - sites: setSites, + sites: updateSites, }); export type SerializableSet = Record; @@ -125,7 +125,11 @@ export const archiveProject = createAsyncThunk( const projectSlice = createSlice({ name: 'project', initialState, - reducers: {}, + reducers: { + setProjects: (state, action: PayloadAction>) => { + state.projects = action.payload; + }, + }, extraReducers: builder => { builder.addCase( addSiteToProject, @@ -192,4 +196,5 @@ const projectSlice = createSlice({ }, }); +export const { setProjects } = projectSlice.actions; export default projectSlice.reducer; diff --git a/src/site/siteSlice.ts b/src/site/siteSlice.ts index ccae39202..ee65c7a30 100644 --- a/src/site/siteSlice.ts +++ b/src/site/siteSlice.ts @@ -15,7 +15,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { createAction, createSlice } from '@reduxjs/toolkit'; +import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { SiteAddMutationInput } from 'terraso-client-shared/graphqlSchema/graphql'; import { addSiteToProject, @@ -42,8 +42,6 @@ const initialState = { sites: {} as Record, }; -export const setSites = createAction>('site/setSites'); - export const fetchSite = createAsyncThunk( 'site/fetchSite', siteService.fetchSite, @@ -87,12 +85,15 @@ export const deleteSite = createAsyncThunk( const siteSlice = createSlice({ name: 'site', initialState, - reducers: {}, + reducers: { + setSites: (state, { payload }: PayloadAction>) => { + state.sites = payload; + }, + updateSites: (state, { payload }: PayloadAction>) => { + Object.assign(state.sites, payload.sites); + }, + }, extraReducers: builder => { - builder.addCase(setSites, (state, { payload: sites }) => { - Object.assign(state.sites, sites); - }); - // TODO: add case to delete site if not found builder.addCase(fetchSite.fulfilled, (state, { payload: site }) => { state.sites[site.id] = site; @@ -135,4 +136,5 @@ const siteSlice = createSlice({ }, }); +export const { setSites, updateSites } = siteSlice.actions; export default siteSlice.reducer; diff --git a/src/soilId/soilIdFragments.ts b/src/soilId/soilIdFragments.ts new file mode 100644 index 000000000..fce95fe50 --- /dev/null +++ b/src/soilId/soilIdFragments.ts @@ -0,0 +1,114 @@ +/* + * Copyright © 2023 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/. + */ + +export const projectSoilSettings = /* GraphQL */ ` + fragment projectSoilSettings on ProjectSoilSettingsNode { + depthIntervals { + depthInterval { + start + end + } + label + } + measurementUnits + depthIntervalPreset + soilPitRequired + slopeRequired + soilTextureRequired + soilColorRequired + verticalCrackingRequired + carbonatesRequired + phRequired + soilOrganicCarbonMatterRequired + electricalConductivityRequired + sodiumAdsorptionRatioRequired + soilStructureRequired + landUseLandCoverRequired + soilLimitationsRequired + photosRequired + notesRequired + } +`; + +export const soilData = /* GraphQL */ ` + fragment soilData on SoilDataNode { + downSlope + crossSlope + bedrock + slopeLandscapePosition + slopeAspect + slopeSteepnessSelect + slopeSteepnessPercent + slopeSteepnessDegree + depthIntervals { + ...soilDataDepthInterval + } + depthDependentData { + ...depthDependentSoilData + } + } +`; + +export const soilDataDepthInterval = /* GraphQL */ ` + fragment soilDataDepthInterval on SoilDataDepthIntervalNode { + label + depthInterval { + start + end + } + slopeEnabled + soilTextureEnabled + soilColorEnabled + verticalCrackingEnabled + carbonatesEnabled + phEnabled + soilOrganicCarbonMatterEnabled + electricalConductivityEnabled + sodiumAdsorptionRatioEnabled + soilStructureEnabled + landUseLandCoverEnabled + soilLimitationsEnabled + } +`; + +export const depthDependentSoilData = /* GraphQL */ ` + fragment depthDependentSoilData on DepthDependentSoilDataNode { + depthInterval { + start + end + } + texture + rockFragmentVolume + colorHueSubstep + colorHue + colorValue + colorChroma + conductivity + conductivityTest + conductivityUnit + structure + ph + phTestingSolution + phTestingMethod + soilOrganicCarbon + soilOrganicMatter + soilOrganicCarbonTesting + soilOrganicMatterTesting + sodiumAbsorptionRatio + carbonates + } +`; diff --git a/src/soilId/soilIdService.ts b/src/soilId/soilIdService.ts new file mode 100644 index 000000000..2ddc315ce --- /dev/null +++ b/src/soilId/soilIdService.ts @@ -0,0 +1,231 @@ +/* + * Copyright © 2023 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 { graphql } from 'terraso-client-shared/graphqlSchema'; +import type { + DepthDependentSoilDataUpdateMutationInput, + ProjectSoilSettingsUpdateDepthIntervalMutationInput, + ProjectSoilSettingsUpdateMutationInput, + SoilDataDeleteDepthIntervalMutationInput, + SoilDataUpdateDepthIntervalMutationInput, + SoilDataUpdateMutationInput, +} from 'terraso-client-shared/graphqlSchema/graphql'; +import { collapseSiteFields } from 'terraso-client-shared/site/siteService'; +import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; +import { collapseConnectionEdges } from 'terraso-client-shared/terrasoApi/utils'; + +export const fetchSoilDataForUser = async (userId: string) => { + const query = graphql(` + query userSoilData($id: ID!) { + userSites: sites(owner: $id) { + edges { + node { + ...siteData + soilData { + ...soilData + } + } + } + } + projects: projects(member: $id) { + edges { + node { + ...projectData + siteSet { + edges { + node { + soilData { + ...soilData + } + } + } + } + soilSettings { + ...projectSoilSettings + } + } + } + } + } + `); + + const { userSites, projects } = await terrasoApi.requestGraphQL(query, { + id: userId, + }); + + const allProjects = collapseConnectionEdges(projects); + const allSites = collapseConnectionEdges(userSites).concat( + allProjects.flatMap(project => collapseConnectionEdges(project.siteSet)), + ); + + return { + projects: Object.fromEntries( + allProjects.map(({ soilSettings, ...project }) => [project.id, project]), + ), + soilSettings: Object.fromEntries( + allProjects.map(({ soilSettings, id }) => [id, soilSettings]), + ), + sites: Object.fromEntries( + allSites.map(({ soilData, ...site }) => [ + site.id, + collapseSiteFields(site), + ]), + ), + soilData: Object.fromEntries( + allSites.map(({ soilData, id }) => [id, soilData]), + ), + }; +}; + +export const updateSoilData = async (soilData: SoilDataUpdateMutationInput) => { + const query = graphql(` + mutation updateSoilData($input: SoilDataUpdateMutationInput!) { + updateSoilData(input: $input) { + soilData { + ...soilData + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: soilData }); + return resp.updateSoilData.soilData!; +}; + +export const updateDepthDependentSoilData = async ( + depthDependentData: DepthDependentSoilDataUpdateMutationInput, +) => { + const query = graphql(` + mutation updateDepthDependentSoilData( + $input: DepthDependentSoilDataUpdateMutationInput! + ) { + updateDepthDependentSoilData(input: $input) { + soilData { + ...soilData + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { + input: depthDependentData, + }); + + return resp.updateDepthDependentSoilData.soilData!; +}; + +export const updateSoilDataDepthInterval = async ( + soilData: SoilDataUpdateDepthIntervalMutationInput, +) => { + const query = graphql(` + mutation updateSoilDataDepthInterval( + $input: SoilDataUpdateDepthIntervalMutationInput! + ) { + updateSoilDataDepthInterval(input: $input) { + soilData { + ...soilData + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: soilData }); + return resp.updateSoilDataDepthInterval.soilData!; +}; + +export const deleteSoilDataDepthInterval = async ( + soilData: SoilDataDeleteDepthIntervalMutationInput, +) => { + const query = graphql(` + mutation deleteSoilDataDepthInterval( + $input: SoilDataDeleteDepthIntervalMutationInput! + ) { + deleteSoilDataDepthInterval(input: $input) { + soilData { + ...soilData + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: soilData }); + return resp.deleteSoilDataDepthInterval.soilData!; +}; + +export const updateProjectSoilSettings = async ( + soilSettings: ProjectSoilSettingsUpdateMutationInput, +) => { + const query = graphql(` + mutation updateProjectSoilSettings( + $input: ProjectSoilSettingsUpdateMutationInput! + ) { + updateProjectSoilSettings(input: $input) { + soilSettings { + ...projectSoilSettings + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: soilSettings }); + return resp.updateProjectSoilSettings.soilSettings!; +}; + +export const updateProjectDepthInterval = async ( + depthInterval: ProjectSoilSettingsUpdateDepthIntervalMutationInput, +) => { + const query = graphql(` + mutation updateProjectSoilSettingsDepthInterval( + $input: ProjectSoilSettingsUpdateDepthIntervalMutationInput! + ) { + updateProjectSoilSettingsDepthInterval(input: $input) { + soilSettings { + ...projectSoilSettings + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: depthInterval }); + return resp.updateProjectSoilSettingsDepthInterval.soilSettings!; +}; + +export const deleteProjectDepthInterval = async ( + depthInterval: ProjectSoilSettingsUpdateDepthIntervalMutationInput, +) => { + const query = graphql(` + mutation deleteProjectSoilSettingsDepthInterval( + $input: ProjectSoilSettingsDeleteDepthIntervalMutationInput! + ) { + deleteProjectSoilSettingsDepthInterval(input: $input) { + soilSettings { + ...projectSoilSettings + } + errors + } + } + `); + + const resp = await terrasoApi.requestGraphQL(query, { input: depthInterval }); + return resp.deleteProjectSoilSettingsDepthInterval.soilSettings!; +}; diff --git a/src/soilId/soilIdSlice.ts b/src/soilId/soilIdSlice.ts new file mode 100644 index 000000000..fbd3fc53d --- /dev/null +++ b/src/soilId/soilIdSlice.ts @@ -0,0 +1,149 @@ +/* + * Copyright © 2023 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 { createSlice } from '@reduxjs/toolkit'; +import type { + DepthDependentSoilDataNode, + DepthInterval, + ProjectDepthIntervalNode, + ProjectSoilSettingsNode, + SoilDataDepthIntervalNode, + SoilDataNode, +} from 'terraso-client-shared/graphqlSchema/graphql'; +import { setProjects } from 'terraso-client-shared/project/projectSlice'; +import { setSites } from 'terraso-client-shared/site/siteSlice'; +import * as soilIdService from 'terraso-client-shared/soilId/soilIdService'; +import { createAsyncThunk } from 'terraso-client-shared/store/utils'; + +export { DepthInterval }; +export type SoilDataDepthInterval = Omit; +export type DepthDependentSoilData = Omit; +export type SoilData = Omit< + SoilDataNode, + 'site' | 'depthIntervals' | 'depthDependentData' +> & { + depthIntervals: SoilDataDepthInterval[]; + depthDependentData: DepthDependentSoilData[]; +}; +export type ProjectDepthInterval = Omit; +export type ProjectSoilSettings = Omit< + ProjectSoilSettingsNode, + 'project' | 'depthIntervals' +> & { + depthIntervals: ProjectDepthInterval[]; +}; + +const initialState = { + soilData: {} as Record, + projectSoilSettings: {} as Record, +}; + +export const fetchSoilDataForUser = createAsyncThunk< + Awaited> +>('soilId/fetchSoilDataForUser', async (_, user, { dispatch }) => { + if (user === null) return null; + const { sites, projects, ...rest } = await soilIdService.fetchSoilDataForUser( + user.id, + ); + dispatch(setSites(sites)); + dispatch(setProjects(projects)); + return rest; +}); + +export const updateSoilData = createAsyncThunk( + 'soilId/updateSoilData', + soilIdService.updateSoilData, +); + +export const updateDepthDependentSoilData = createAsyncThunk( + 'soilId/updateDepthDependentSoilData', + soilIdService.updateDepthDependentSoilData, +); + +export const updateSoilDataDepthInterval = createAsyncThunk( + 'soilId/updateSoilDataDepthInterval', + soilIdService.updateSoilDataDepthInterval, +); + +export const deleteSoilDataDepthInterval = createAsyncThunk( + 'soilId/deleteSoilDataDepthInterval', + soilIdService.deleteSoilDataDepthInterval, +); + +export const updateProjectSoilSettings = createAsyncThunk( + 'soilId/updateProjectSoilSettings', + soilIdService.updateProjectSoilSettings, +); + +export const updateProjectDepthInterval = createAsyncThunk( + 'soilId/updateProjectDepthInterval', + soilIdService.updateProjectDepthInterval, +); + +export const deleteProjectDepthInterval = createAsyncThunk( + 'soilId/deleteProjectDepthInterval', + soilIdService.deleteProjectDepthInterval, +); + +export const sameDepth = + (a: { depthInterval: DepthInterval }) => + (b: { depthInterval: DepthInterval }) => + a.depthInterval.start === b.depthInterval.start && + a.depthInterval.end === b.depthInterval.end; + +const soilIdSlice = createSlice({ + name: 'soilId', + initialState, + reducers: {}, + extraReducers: builder => { + builder.addCase(fetchSoilDataForUser.fulfilled, (state, action) => { + if (action.payload === null) return; + state.soilData = action.payload.soilData; + state.projectSoilSettings = action.payload.soilSettings; + }); + + builder.addCase(updateSoilData.fulfilled, (state, action) => { + state.soilData[action.meta.arg.siteId] = action.payload; + }); + + builder.addCase(updateDepthDependentSoilData.fulfilled, (state, action) => { + state.soilData[action.meta.arg.siteId] = action.payload; + }); + + builder.addCase(updateSoilDataDepthInterval.fulfilled, (state, action) => { + state.soilData[action.meta.arg.siteId] = action.payload; + }); + + builder.addCase(deleteSoilDataDepthInterval.fulfilled, (state, action) => { + state.soilData[action.meta.arg.siteId] = action.payload; + }); + + builder.addCase(updateProjectSoilSettings.fulfilled, (state, action) => { + state.projectSoilSettings[action.meta.arg.projectId] = action.payload; + }); + + builder.addCase(updateProjectDepthInterval.fulfilled, (state, action) => { + state.projectSoilSettings[action.meta.arg.projectId] = action.payload; + }); + + builder.addCase(deleteProjectDepthInterval.fulfilled, (state, action) => { + state.projectSoilSettings[action.meta.arg.projectId] = action.payload; + }); + }, +}); + +export default soilIdSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts index 47bd76bf6..34e72b062 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -29,6 +29,7 @@ import membershipsReducer from 'terraso-client-shared/memberships/membershipsSli import notificationsReducer from 'terraso-client-shared/notifications/notificationsSlice'; import projectReducer from 'terraso-client-shared/project/projectSlice'; import siteReducer from 'terraso-client-shared/site/siteSlice'; +import soilIdReducer from 'terraso-client-shared/soilId/soilIdSlice'; const handleAbortMiddleware: Middleware = () => next => action => { if (_.getOr(false, 'meta.aborted', action)) { @@ -47,6 +48,7 @@ const sharedReducers = { notifications: notificationsReducer, site: siteReducer, project: projectReducer, + soilId: soilIdReducer, }; // Using some advanced TypeScript features here: since we have