diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx index 868733eb95..3bec5e489a 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx @@ -1,28 +1,36 @@ import Container from "@mui/material/Container"; import Typography from "@mui/material/Typography"; -import React, { useState } from "react"; +import { Role } from "@opensystemslab/planx-core/types"; +import { groupBy } from "lodash"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; import SettingsSection from "ui/editor/SettingsSection"; -import { AddNewEditorModal } from "./components/AddNewEditorModal"; +import { + filterByEmailPresent, + filterExcludingPlatformAdmins, + hasEmailPresent, +} from "./components/lib/filterTeamMembers"; import { MembersTable } from "./components/MembersTable"; -import { TeamMember, TeamMembersProps } from "./types"; +import { TeamMember } from "./types"; -export const TeamMembers = ({ teamMembersByRole }: TeamMembersProps) => { - const [showModal, setShowModal] = useState(false); +export const TeamMembers = () => { + const teamMembers = useStore((state) => state.teamMembers); - const platformAdmins = (teamMembersByRole.platformAdmin || []).filter( - (member) => member.email, - ); - const otherRoles = Object.keys(teamMembersByRole) - .filter((role) => role !== "platformAdmin") - .reduce((acc: TeamMember[], role) => { - return acc.concat(teamMembersByRole[role]); - }, []); + const teamMembersByRole = groupBy(teamMembers, "role") as Record< + Role, + TeamMember[] + >; + + const platformAdmins = + teamMembersByRole.platformAdmin.filter(hasEmailPresent); - const activeMembers = otherRoles.filter((member) => member.email); + const otherRoles = filterExcludingPlatformAdmins(teamMembers); - const archivedMembers = otherRoles.filter( - (member) => member.role !== "platformAdmin" && !member.email, + const activeMembers = filterByEmailPresent(otherRoles); + + const archivedMembers: TeamMember[] = otherRoles.filter( + (member) => !hasEmailPresent(member), ); return ( @@ -34,11 +42,7 @@ export const TeamMembers = ({ teamMembersByRole }: TeamMembersProps) => { Editors have access to edit your services. - + @@ -61,9 +65,6 @@ export const TeamMembers = ({ teamMembersByRole }: TeamMembersProps) => { )} - {showModal && ( - - )} ); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx index e31746f004..a2b24ff48a 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx @@ -4,102 +4,143 @@ import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import Typography from "@mui/material/Typography"; +import { FormikHelpers, useFormik } from "formik"; +import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; import InputGroup from "ui/editor/InputGroup"; import InputLabel from "ui/editor/InputLabel"; import Input from "ui/shared/Input"; -import { AddNewEditorModalProps } from "../types"; +import { addNewEditorFormSchema } from "../formSchema"; +import { createAndAddUserToTeam } from "../queries/createAndAddUserToTeam"; +import { AddNewEditorFormValues, AddNewEditorModalProps } from "../types"; +import { optimisticallyUpdateMembersTable } from "./lib/optimisticallyUpdateMembersTable"; export const AddNewEditorModal = ({ showModal, setShowModal, -}: AddNewEditorModalProps) => ( - ({ - width: "100%", - maxWidth: theme.breakpoints.values.md, - borderRadius: 0, - borderTop: `20px solid ${theme.palette.primary.main}`, - background: "#FFF", - margin: theme.spacing(2), - }), - }} - open={showModal} - onClose={() => setShowModal(false)} - > -
- - - - Add a new editor - - - - - { - console.log("bla"); // TODO in next PR - }} - value={""} - errorMessage={""} - id="firstname" - /> - - - { - console.log("bla"); // TODO in next PR - }} - value={""} - errorMessage={""} - id="lastname" - /> - - - { - console.log("bla"); // TODO in next PR - }} - value={""} - errorMessage={""} - id="email" - /> - - - - - - - - - -
-
-); +}: AddNewEditorModalProps) => { + const handleSubmit = async ( + values: AddNewEditorFormValues, + { resetForm }: FormikHelpers, + ) => { + const { teamId, teamSlug } = useStore.getState(); + + const newUserId = await createAndAddUserToTeam( + values.email, + values.firstName, + values.lastName, + teamId, + teamSlug, + ); + + optimisticallyUpdateMembersTable(values, newUserId); + + setShowModal(false); + resetForm({ values }); + }; + + const formik = useFormik({ + initialValues: { + firstName: "", + lastName: "", + email: "", + }, + validationSchema: addNewEditorFormSchema, + onSubmit: handleSubmit, + }); + + return ( + ({ + width: "100%", + maxWidth: theme.breakpoints.values.md, + borderRadius: 0, + borderTop: `20px solid ${theme.palette.primary.main}`, + background: "#FFF", + margin: theme.spacing(2), + }), + }} + open={showModal} + onClose={() => setShowModal(false)} + > +
+ + + + Add a new editor + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx index 70139fcc15..9b3a6eb063 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx @@ -7,16 +7,18 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import { hasFeatureFlag } from "lib/featureFlags"; import { AddButton } from "pages/Team"; -import React from "react"; +import React, { useState } from "react"; import { StyledAvatar, StyledTableRow } from "./../styles"; import { MembersTableProps } from "./../types"; +import { AddNewEditorModal } from "./AddNewEditorModal"; export const MembersTable = ({ members, showAddMemberButton, - setShowModal = () => true, }: MembersTableProps) => { + const [showModal, setShowModal] = useState(false); + const roleLabels: Record = { platformAdmin: "Admin", teamEditor: "Editor", @@ -42,58 +44,65 @@ export const MembersTable = ({ } return ( - - - - - - User - - - Role - - - Email - - - - - {members.map((member) => ( - - - - {member.firstName[0]} - {member.lastName[0]} - - {member.firstName} {member.lastName} + <> + +
+ + + + User + + + Role - + Email - {member.email} - ))} - {showAddMemberButton && hasFeatureFlag("ADD_NEW_EDITOR") && ( - - - setShowModal(true)}> - Add a new editor - - - - )} - -
-
+ + + {members.map((member) => ( + + + + {member.firstName[0]} + {member.lastName[0]} + + {member.firstName} {member.lastName} + + + + + {member.email} + + ))} + {showAddMemberButton && hasFeatureFlag("ADD_NEW_EDITOR") && ( + + + setShowModal(true)}> + Add a new editor + + + + )} + + + + {showModal && ( + + )} + ); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/filterTeamMembers.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/filterTeamMembers.tsx new file mode 100644 index 0000000000..df727528d9 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/filterTeamMembers.tsx @@ -0,0 +1,13 @@ +import { TeamMember } from "../../types"; + +export const hasEmailPresent = (member: TeamMember) => !!member.email; + +export const filterByEmailPresent = (members: TeamMember[]): TeamMember[] => { + return members.filter(hasEmailPresent); +}; + +export const filterExcludingPlatformAdmins = ( + members: TeamMember[], +): TeamMember[] => { + return members.filter((member) => member.role !== "platformAdmin"); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/optimisticallyUpdateMembersTable.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/optimisticallyUpdateMembersTable.tsx new file mode 100644 index 0000000000..c29b2537ce --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/lib/optimisticallyUpdateMembersTable.tsx @@ -0,0 +1,18 @@ +import { useStore } from "pages/FlowEditor/lib/store"; + +import { AddNewEditorFormValues, TeamMember } from "../../types"; + +export const optimisticallyUpdateMembersTable = async ( + values: AddNewEditorFormValues, + userId: number, +) => { + const newMember: TeamMember = { + ...values, + role: "teamEditor", + id: userId, + }; + + const existingMembers = useStore.getState().teamMembers; + + await useStore.getState().setTeamMembers([...existingMembers, newMember]); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/formSchema.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/formSchema.tsx new file mode 100644 index 0000000000..b2bd831c6f --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/formSchema.tsx @@ -0,0 +1,7 @@ +import * as Yup from "yup"; + +export const addNewEditorFormSchema = Yup.object({ + firstName: Yup.string().required("Required"), + lastName: Yup.string().required("Required"), + email: Yup.string().email("Invalid email address").required("Required"), +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx new file mode 100644 index 0000000000..46a4161e55 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx @@ -0,0 +1,45 @@ +import { gql } from "@apollo/client"; +import { GET_USERS_FOR_TEAM_QUERY } from "routes/teamMembers"; + +import { client } from "../../../../../lib/graphql"; + +export const createAndAddUserToTeam = async ( + email: string, + firstName: string, + lastName: string, + teamId: number, + teamSlug: string, +) => { + // NB: the user is hard-coded with the 'teamEditor' role for now + const response = (await client.mutate({ + mutation: gql` + mutation CreateAndAddUserToTeam( + $email: String! + $firstName: String! + $lastName: String! + $teamId: Int! + ) { + insert_users_one( + object: { + email: $email + first_name: $firstName + last_name: $lastName + teams: { data: { role: teamEditor, team_id: $teamId } } + } + ) { + id + } + } + `, + variables: { + email, + firstName, + lastName, + teamId, + }, + refetchQueries: [ + { query: GET_USERS_FOR_TEAM_QUERY, variables: { teamSlug } }, + ], + })) as any; + return response.data.insert_users_one; +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx index 377959aaf7..43dd52fc21 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx @@ -1,27 +1,31 @@ -/* eslint-disable jest/expect-expect */ -import { screen, within } from "@testing-library/react"; + +import { screen, waitFor, within } from "@testing-library/react"; +import { FullStore, useStore } from "pages/FlowEditor/lib/store"; import { vi } from "vitest"; import { setupTeamMembersScreen } from "./helpers/setupTeamMembersScreen"; +import { userEntersInput } from "./helpers/userEntersInput"; vi.mock("lib/featureFlags.ts", () => ({ hasFeatureFlag: vi.fn().mockReturnValue(true), })); -describe("when a user views the Team members screen with the ADD_NEW_EDITOR feature flag enabled", () => { - beforeEach(async () => { - await setupTeamMembersScreen(); - }); +vi.mock( + "pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx", + () => ({ + createAndAddUserToTeam: vi.fn().mockResolvedValue({ + id: 1, + __typename: "users", + }), + }), +); - it("shows the 'add new editor' button", async () => { - const teamEditorsTable = screen.getByTestId("team-editors"); - await within(teamEditorsTable).findByText("Add a new editor"); - }); -}); +let initialState: FullStore; describe("when a user with the ADD_NEW_EDITOR feature flag enabled presses 'add a new editor'", () => { beforeEach(async () => { const user = await setupTeamMembersScreen(); + const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByText( "Add a new editor", @@ -34,3 +38,39 @@ describe("when a user with the ADD_NEW_EDITOR feature flag enabled presses 'add expect(await screen.findByLabelText("First name")).toBeVisible(); }); }); + +describe("when a user fills in the 'add a new editor' form correctly", () => { + afterAll(() => useStore.setState(initialState)); + beforeEach(async () => { + const user = await setupTeamMembersScreen(); + const teamEditorsTable = screen.getByTestId("team-editors"); + const addEditorButton = await within(teamEditorsTable).findByText( + "Add a new editor", + ); + user.click(addEditorButton); + const addNewEditorModal = await screen.findByTestId("modal-create-user"); + await userEntersInput("First name", "Mickey", addNewEditorModal); + await userEntersInput("Last name", "Mouse", addNewEditorModal); + await userEntersInput( + "Email address", + "mickeymouse@email.com", + addNewEditorModal, + ); + + const createUserButton = await screen.findByTestId( + "modal-create-user-button", + ); + + user.click(createUserButton); + }); + + it("adds the new user row to the Team Editors table", async () => { + const membersTable = screen.getByTestId("members-table-add-editor"); + + await waitFor(() => { + expect( + within(membersTable).getByText(/Mickey Mouse/), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx index f9700d87ed..f1b15fd214 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx @@ -1,26 +1,18 @@ -import { Role } from "@opensystemslab/planx-core/types"; - import { TeamMember } from "../types"; -export const exampleTeamMembersData: Record = { - platformAdmin: [ - { - firstName: "Donella", - lastName: "Meadows", - email: "donella@example.com", - id: 1, - role: "platformAdmin", - }, - ], - teamEditor: [ - { - firstName: "Bill", - lastName: "Sharpe", - email: "bill@example.com", - id: 2, - role: "teamEditor", - }, - ], - teamViewer: [], - public: [], -}; +export const exampleTeamMembersData: TeamMember[] = [ + { + firstName: "Donella", + lastName: "Meadows", + email: "donella@example.com", + id: 1, + role: "platformAdmin", + }, + { + firstName: "Bill", + lastName: "Sharpe", + email: "bill@example.com", + id: 2, + role: "teamEditor", + }, +]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx index 0c80592bc3..a6eea16445 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx @@ -1,18 +1,21 @@ import { screen } from "@testing-library/react"; +import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { setup } from "../../../../../../testUtils"; import { TeamMembers } from "../../TeamMembers"; -import { exampleTeamMembersData } from "./../exampleTeamMembersData"; +import { exampleTeamMembersData } from "../exampleTeamMembersData"; + +export const setupTeamMembersScreen = async () => { + useStore.setState({ teamMembers: exampleTeamMembersData }); -export async function setupTeamMembersScreen() { const { user } = setup( - + , ); await screen.findByText("Team editors"); return user; -} +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx new file mode 100644 index 0000000000..546955d2c6 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx @@ -0,0 +1,13 @@ +import { fireEvent, within } from "@testing-library/react"; + +export const userEntersInput = async ( + labelText: string, + inputString: string, + container: HTMLElement, +) => { + const inputField = await within(container).findByLabelText(labelText); + + fireEvent.change(inputField, { + target: { value: inputString }, + }); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts b/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts index 725f959212..bf823c5352 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts @@ -5,15 +5,17 @@ export type TeamMember = Omit & { role: Role; }; -export interface TeamMembersProps { - teamMembersByRole: Record; -} export interface MembersTableProps { members: TeamMember[]; showAddMemberButton?: boolean; - setShowModal?: React.Dispatch>; } -export type AddNewEditorModalProps = { +export interface AddNewEditorModalProps { showModal: boolean; setShowModal: React.Dispatch>; -}; +} + +export interface AddNewEditorFormValues { + email: string; + firstName: string; + lastName: string; +} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts index 99238abce5..24ab3aeb3d 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts @@ -6,6 +6,7 @@ import { } from "@opensystemslab/planx-core/types"; import gql from "graphql-tag"; import { client } from "lib/graphql"; +import { TeamMember } from "pages/FlowEditor/components/Team/types"; import type { StateCreator } from "zustand"; import { SharedStore } from "./shared"; @@ -17,6 +18,7 @@ export interface TeamStore { teamSettings: TeamSettings; teamSlug: string; teamTheme: TeamTheme; + teamMembers: TeamMember[]; teamDomain: string; setTeam: (team: Team) => void; @@ -27,6 +29,7 @@ export interface TeamStore { updateTeamTheme: (theme: Partial) => Promise; updateTeamSettings: (teamSettings: Partial) => Promise; createTeam: (newTeam: { name: string; slug: string }) => Promise; + setTeamMembers: (teamMembers: TeamMember[]) => Promise; } export const teamStore: StateCreator< @@ -41,6 +44,7 @@ export const teamStore: StateCreator< teamSettings: {} as TeamSettings, teamSlug: "", teamTheme: {} as TeamTheme, + teamMembers: [] as TeamMember[], teamDomain: "", setTeam: (team) => { @@ -67,13 +71,13 @@ export const teamStore: StateCreator< settings: get().teamSettings, slug: get().teamSlug, theme: get().teamTheme, + members: get().teamMembers, domain: get().teamDomain, }), createTeam: async (newTeam) => { const { $client } = get(); - const isSuccess = await $client.team.create(newTeam); - return isSuccess; + return await $client.team.create(newTeam); }, initTeamStore: async (slug) => { @@ -122,6 +126,7 @@ export const teamStore: StateCreator< teamSettings: undefined, teamSlug: "", teamTheme: undefined, + teamMembers: [], }), /** @@ -130,22 +135,20 @@ export const teamStore: StateCreator< */ fetchCurrentTeam: async () => { const { teamSlug, $client } = get(); - const team = await $client.team.getBySlug(teamSlug); - return team; + return await $client.team.getBySlug(teamSlug); }, updateTeamTheme: async (theme: Partial) => { const { teamId, $client } = get(); - const isSuccess = await $client.team.updateTheme(teamId, theme); - return isSuccess; + return await $client.team.updateTheme(teamId, theme); }, updateTeamSettings: async (teamSettings: Partial) => { const { teamId, $client } = get(); - const isSuccess = await $client.team.updateTeamSettings( - teamId, - teamSettings, - ); - return isSuccess; + return await $client.team.updateTeamSettings(teamId, teamSettings); + }, + + setTeamMembers: async (teamMembers: TeamMember[]) => { + set(() => ({ teamMembers })); }, }); diff --git a/editor.planx.uk/src/routes/teamMembers.tsx b/editor.planx.uk/src/routes/teamMembers.tsx index 331d2ab6ea..c36d7999cc 100644 --- a/editor.planx.uk/src/routes/teamMembers.tsx +++ b/editor.planx.uk/src/routes/teamMembers.tsx @@ -1,6 +1,5 @@ -import { Role, User } from "@opensystemslab/planx-core/types"; +import { User } from "@opensystemslab/planx-core/types"; import gql from "graphql-tag"; -import { groupBy } from "lodash"; import { compose, mount, NotFoundError, route, withData } from "navi"; import { TeamMembers } from "pages/FlowEditor/components/Team/TeamMembers"; import { TeamMember } from "pages/FlowEditor/components/Team/types"; @@ -10,10 +9,33 @@ import React from "react"; import { client } from "../lib/graphql"; import { makeTitle } from "./utils"; -interface GetUsersForTeam { +export interface GetUsersForTeam { users: User[]; } +export const GET_USERS_FOR_TEAM_QUERY = gql` + query GetUsersForTeam($teamSlug: String!) { + users( + where: { + _or: [ + { is_platform_admin: { _eq: true } } + { teams: { team: { slug: { _eq: $teamSlug } } } } + ] + } + order_by: { first_name: asc } + ) { + id + firstName: first_name + lastName: last_name + isPlatformAdmin: is_platform_admin + email + teams(where: { team: { slug: { _eq: $teamSlug } } }) { + role + } + } + } +`; + const teamMembersRoutes = compose( withData((req) => ({ mountpath: req.mountpath, @@ -32,28 +54,7 @@ const teamMembersRoutes = compose( const { data: { users }, } = await client.query({ - query: gql` - query GetUsersForTeam($teamSlug: String!) { - users( - where: { - _or: [ - { is_platform_admin: { _eq: true } } - { teams: { team: { slug: { _eq: $teamSlug } } } } - ] - } - order_by: { first_name: asc } - ) { - id - firstName: first_name - lastName: last_name - isPlatformAdmin: is_platform_admin - email - teams(where: { team: { slug: { _eq: $teamSlug } } }) { - role - } - } - } - `, + query: GET_USERS_FOR_TEAM_QUERY, variables: { teamSlug }, }); @@ -65,14 +66,11 @@ const teamMembersRoutes = compose( role: user.isPlatformAdmin ? "platformAdmin" : user.teams[0].role, })); - const teamMembersByRole = groupBy(teamMembers, "role") as Record< - Role, - TeamMember[] - >; + await useStore.getState().setTeamMembers(teamMembers); return { title: makeTitle("Team Members"), - view: , + view: , }; }), }),