From 07f71bd2236469b5c5ae720546d4d2da58197421 Mon Sep 17 00:00:00 2001 From: David Code Howard Date: Tue, 24 Oct 2023 14:10:57 -0400 Subject: [PATCH] feat: Add selector for site transfer screen --- src/selectors.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++--- src/selectors.ts | 42 +++++++++++++++++++++++++++-- src/utils.ts | 9 +++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/selectors.test.ts b/src/selectors.test.ts index 7b247762..10526805 100644 --- a/src/selectors.test.ts +++ b/src/selectors.test.ts @@ -11,7 +11,12 @@ import { Project, ProjectMembership, } from 'terraso-client-shared/project/projectSlice'; -import { selectProjectMembershipsWithUsers } from 'terraso-client-shared/selectors'; +import { + selectProjectMembershipsWithUsers, + selectProjectsWithTransferrableSites, +} from 'terraso-client-shared/selectors'; +import { Site } from 'terraso-client-shared/site/siteSlice'; +import { SerializableSet } from 'terraso-client-shared/store/utils'; import { createStore } from 'terraso-client-shared/tests/utils'; import { v4 as uuidv4 } from 'uuid'; @@ -30,20 +35,43 @@ const generateUser = () => { const generateProject = ( memberships: ProjectMembership[] = [], privacy?: ProjectPrivacy, + sites: Site[] = [], ): Project => { const id = uuidv4(); + const siteSet: SerializableSet = {}; + for (let site of sites) { + site.projectId = id; + siteSet[site.id] = true; + } return { id, name: id, privacy: privacy ?? 'PRIVATE', description: '', updatedAt: '2023-10-12', - sites: {}, + sites: siteSet, archived: false, memberships: keyBy(memberships, 'id'), }; }; +const generateSite = (project: Project): Site => { + const id = uuidv4(); + const site: Site = { + projectId: project.id, + ownerId: undefined, + id, + name: 'Test Site', + latitude: 0, + longitude: 0, + privacy: 'PRIVATE', + archived: false, + updatedAt: '2023-10-24', + }; + project.sites[site.id] = true; + return site; +}; + const generateMembership = (userId: string, userRole: UserRole) => { return { id: uuidv4(), userId, userRole }; }; @@ -62,7 +90,7 @@ const keyBy = ( ); }; -function initState(projects: Project[], users: User[]) { +function initState(projects: Project[], users: User[], sites: Site[] = []) { return merge( { account: { @@ -71,6 +99,9 @@ function initState(projects: Project[], users: User[]) { project: { projects: keyBy(projects, 'id'), }, + site: { + sites: keyBy(sites, 'id'), + }, }, { account: { ...accountInitialState } }, ); @@ -116,3 +147,29 @@ test('not found project returns empty membership', () => { ); expect(memberships).toStrictEqual([]); }); + +test('can access all projects with role', () => { + const user = generateUser(); + const project1 = generateProject([generateMembership(user.id, 'manager')]); + const project2 = generateProject([ + generateMembership(user.id, 'contributor'), + ]); + const site1 = generateSite(project1); + const site2 = generateSite(project2); + + const store = createStore( + initState([project1, project2], [user], [site1, site2]), + ); + const pairs = selectProjectsWithTransferrableSites( + store.getState(), + 'manager', + ); + expect(pairs).toStrictEqual([ + { + projectId: project1.id, + projectName: project1.name, + siteId: site1.id, + siteName: site1.name, + }, + ]); +}); diff --git a/src/selectors.ts b/src/selectors.ts index 051cdd47..e8f3cc60 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,8 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; import { User } from 'terraso-client-shared/account/accountSlice'; +import { UserRole } from 'terraso-client-shared/graphqlSchema/graphql'; +import { ProjectMembership } from 'terraso-client-shared/project/projectSlice'; import { type SharedState } from 'terraso-client-shared/store/store'; - -import { ProjectMembership } from './project/projectSlice'; +import { exists, filterValues, mapValues } from 'terraso-client-shared/utils'; const selectProjectMemberships = (state: SharedState, projectId: string) => state.project.projects[projectId]?.memberships ?? []; @@ -16,3 +17,40 @@ export const selectProjectMembershipsWithUsers = createSelector( .filter(memb => memb.userId in users) .map(memb => [memb, users[memb.userId]] as [ProjectMembership, User]), ); + +const selectProjects = (state: SharedState) => state.project.projects; + +const selectSites = (state: SharedState) => state.site.sites; + +const selectUserRole = (_state: SharedState, userRole: UserRole) => userRole; + +const selectProjectsWithUserRole = createSelector( + [selectProjects, selectUserRole], + (projects, userRole) => + filterValues(projects, project => + exists( + mapValues(project.memberships, membership => membership.userRole), + userRole, + ), + ), +); + +export const selectProjectsWithTransferrableSites = createSelector( + [selectProjectsWithUserRole, selectSites], + (projects, sites) => { + return projects.flatMap(project => + Object.keys(project.sites) + .filter(siteId => siteId in project.sites) + .map(siteId => { + const joinedSite = sites[siteId]; + + return { + projectId: project.id, + projectName: project.name, + siteId: joinedSite.id, + siteName: joinedSite.name, + }; + }), + ); + }, +); diff --git a/src/utils.ts b/src/utils.ts index 0de5ac9d..7504b5b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,2 +1,11 @@ export const fromEntries = (entries: [K, V][]) => Object.fromEntries(entries) as Record; + +export const filterValues = (obj: Record, f: (arg: T) => boolean) => + Object.values(obj).filter(f); + +export const exists = (haystack: T[], needle: T) => + haystack.filter(item => item === needle).length > 0; + +export const mapValues = (obj: Record, f: (arg: T) => U) => + Object.values(obj).map(f);