)
}
diff --git a/src/components/TaskPane/TaskPane.js b/src/components/TaskPane/TaskPane.js
index f5ba84333..4c739cfc2 100644
--- a/src/components/TaskPane/TaskPane.js
+++ b/src/components/TaskPane/TaskPane.js
@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'
import _get from 'lodash/get'
import { generateWidgetId, WidgetDataTarget, widgetDescriptor }
from '../../services/Widget/Widget'
-import { isFinalStatus }
+import { isCompletionStatus }
from '../../services/Task/TaskStatus/TaskStatus'
import WithWidgetWorkspaces
from '../HOCs/WithWidgetWorkspaces/WithWidgetWorkspaces'
@@ -267,7 +267,7 @@ export class TaskPane extends Component {
completingTask={this.state.completingTask}
setCompletionResponse={this.setCompletionResponse}
completionResponses={completionResponses}
- disableTemplate={isFinalStatus(this.props.task.status)}
+ disableTemplate={isCompletionStatus(this.props.task.status)}
/>
{this.state.completingTask && this.state.completingTask === this.props.task.id &&
{
+ const [username, setUsername] = useState("")
+ const [selectedUser, setSelectedUser] = useState(null)
+ const [inviteTeamUser, { loading: isSaving }] = useMutation(INVITE_USER)
+
+ const inviteSelectedUser = role => {
+ inviteTeamUser({
+ variables: { teamId: props.team.id, userId: selectedUser.id, role: parseInt(role) },
+ refetchQueries: ['TeamUsers'],
+ })
+ .catch(error => {
+ props.addErrorWithDetails(AppErrors.team.failure, error.message)
+ })
+ setSelectedUser(null)
+ setUsername("")
+ }
+
+ if (isSaving) {
+ return
+ }
+
+ return (
+
+
+ setUsername(username)}
+ onChange={osmUser => setSelectedUser(osmUser)}
+ placeholder={props.intl.formatMessage(messages.osmUsername)}
+ fixedMenu
+ />
+
+ {selectedUser &&
}
+
+ )
+}
+
+AddTeamMember.propTypes = {
+ team: PropTypes.object.isRequired,
+}
+
+export default injectIntl(AddTeamMember)
diff --git a/src/components/Teams/AddTeamMember/Messages.js b/src/components/Teams/AddTeamMember/Messages.js
new file mode 100644
index 000000000..f4a9dda12
--- /dev/null
+++ b/src/components/Teams/AddTeamMember/Messages.js
@@ -0,0 +1,17 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with AddTeamMember
+ */
+export default defineMessages({
+ chooseRole: {
+ id: "AddTeamMember.controls.chooseRole.label",
+ defaultMessage: "Choose Role",
+ },
+
+ osmUsername: {
+ id: "AddTeamMember.controls.chooseOSMUser.placeholder",
+ defaultMessage: "OpenStreetMap username"
+ },
+})
+
diff --git a/src/components/Teams/EditTeam/EditTeam.js b/src/components/Teams/EditTeam/EditTeam.js
new file mode 100644
index 000000000..1ef9905c8
--- /dev/null
+++ b/src/components/Teams/EditTeam/EditTeam.js
@@ -0,0 +1,82 @@
+import React, { useState } from 'react'
+import PropTypes from 'prop-types'
+import { useMutation } from '@apollo/client'
+import Form from 'react-jsonschema-form'
+import { FormattedMessage } from 'react-intl'
+import _isFinite from 'lodash/isFinite'
+import _isEmpty from 'lodash/isEmpty'
+import AppErrors from '../../../services/Error/AppErrors'
+import { jsSchema, uiSchema } from './TeamSchema'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import { CREATE_TEAM, UPDATE_TEAM } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Displays a form for creating or editing team fields
+ */
+export const EditTeam = props => {
+ const [teamFields, setTeamFields] = useState({})
+ const [createTeam, { loading: isCreating }] = useMutation(CREATE_TEAM)
+ const [updateTeam, { loading: isUpdating }] = useMutation(UPDATE_TEAM)
+ const isSaving = isCreating || isUpdating
+
+ if (isSaving) {
+ return
+ }
+
+ return (
+
+ )
+}
+
+EditTeam.propTypes = {
+ finish: PropTypes.func.isRequired,
+}
+
+export default EditTeam
diff --git a/src/components/Teams/EditTeam/Messages.js b/src/components/Teams/EditTeam/Messages.js
new file mode 100644
index 000000000..cd768b6a0
--- /dev/null
+++ b/src/components/Teams/EditTeam/Messages.js
@@ -0,0 +1,36 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with Teams
+ */
+export default defineMessages({
+ nameLabel: {
+ id: "Team.name.label",
+ defaultMessage: "Name",
+ },
+
+ nameDescription: {
+ id: "Team.name.description",
+ defaultMessage: "The unique name of the team",
+ },
+
+ descriptionLabel: {
+ id: "Team.description.label",
+ defaultMessage: "Description",
+ },
+
+ descriptionDescription: {
+ id: "Team.description.description",
+ defaultMessage: "A brief description of the team",
+ },
+
+ saveLabel: {
+ id: "Team.controls.save.label",
+ defaultMessage: "Save",
+ },
+
+ cancelLabel: {
+ id: "Team.controls.cancel.label",
+ defaultMessage: "Cancel",
+ },
+})
diff --git a/src/components/Teams/EditTeam/TeamSchema.js b/src/components/Teams/EditTeam/TeamSchema.js
new file mode 100644
index 000000000..488f19586
--- /dev/null
+++ b/src/components/Teams/EditTeam/TeamSchema.js
@@ -0,0 +1,42 @@
+import messages from './Messages'
+
+/**
+ * Generates a JSON Schema describing Team fields for consumption by
+ * react-jsonschema-form.
+ *
+ * @param intl - intl instance from react-intl
+ *
+ * @see See http://json-schema.org
+ * @see See https://github.com/mozilla-services/react-jsonschema-form
+ *
+ * @author [Neil Rotstan](https://github.com/nrotstan)
+ */
+export const jsSchema = intl => ({
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ name: {
+ title: intl.formatMessage(messages.nameLabel),
+ type: "string",
+ },
+ description: {
+ title: intl.formatMessage(messages.descriptionLabel),
+ type: "string",
+ },
+ },
+})
+
+/**
+ * uiSchema configuration to assist react-jsonschema-form in determining
+ * how to render the schema fields.
+ *
+ * @see See https://github.com/mozilla-services/react-jsonschema-form
+ */
+export const uiSchema = intl => ({
+ name: {
+ "ui:help": intl.formatMessage(messages.nameDescription),
+ },
+ description: {
+ "ui:help": intl.formatMessage(messages.descriptionDescription),
+ },
+})
diff --git a/src/components/Teams/MemberControls/AcceptInviteControlItem.js b/src/components/Teams/MemberControls/AcceptInviteControlItem.js
new file mode 100644
index 000000000..6208e9e18
--- /dev/null
+++ b/src/components/Teams/MemberControls/AcceptInviteControlItem.js
@@ -0,0 +1,55 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import { useMutation } from '@apollo/client'
+import AppErrors from '../../../services/Error/AppErrors'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import { ACCEPT_INVITE } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Control item for accepting an invitation to join a team
+ */
+const AcceptInviteControlItem = props => {
+ const [acceptInvite, { loading: isSaving }] = useMutation(ACCEPT_INVITE)
+
+ // The team member needs have an invitation and be the current user
+ if (!props.teamMember.isInvited() || !props.teamMember.isUser(props.user)) {
+ return null
+ }
+
+ if (isSaving) {
+ return
+ }
+
+ return (
+
+ { /* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ {
+ acceptInvite({
+ variables: { teamId: props.teamMember.team.id },
+ refetchQueries: props.refetchQueries,
+ })
+ .catch(error => {
+ props.addErrorWithDetails(AppErrors.team.failure, error.message)
+ })
+ }}
+ >
+
+
+
+ )
+}
+
+AcceptInviteControlItem.propTypes = {
+ user: PropTypes.object.isRequired,
+ teamMember: PropTypes.object.isRequired,
+ refetchQueries: PropTypes.array,
+}
+
+AcceptInviteControlItem.defaultProps = {
+ refetchQueries: [],
+}
+
+export default AcceptInviteControlItem
diff --git a/src/components/Teams/MemberControls/DeclineInviteControlItem.js b/src/components/Teams/MemberControls/DeclineInviteControlItem.js
new file mode 100644
index 000000000..fee30f023
--- /dev/null
+++ b/src/components/Teams/MemberControls/DeclineInviteControlItem.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import { useMutation } from '@apollo/client'
+import AppErrors from '../../../services/Error/AppErrors'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import { DECLINE_INVITE } from '../TeamQueries'
+import messages from './Messages'
+
+const DeclineInviteControlItem = props => {
+ const [declineInvite, { loading: isSaving }] = useMutation(DECLINE_INVITE)
+
+ // The team member needs have an invitation and be the current user
+ if (!props.teamMember.isInvited() || !props.teamMember.isUser(props.user)) {
+ return null
+ }
+
+ if (isSaving) {
+ return
+ }
+
+ return (
+
+ { /* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ {
+ declineInvite({
+ variables: { teamId: props.teamMember.team.id },
+ refetchQueries: props.refetchQueries,
+ })
+ .catch(error => {
+ props.addErrorWithDetails(AppErrors.team.failure, error.message)
+ })
+ }}
+ >
+
+
+
+ )
+}
+
+DeclineInviteControlItem.propTypes = {
+ user: PropTypes.object.isRequired,
+ teamMember: PropTypes.object.isRequired,
+ refetchQueries: PropTypes.array,
+}
+
+DeclineInviteControlItem.defaultProps = {
+ refetchQueries: [],
+}
+
+export default DeclineInviteControlItem
diff --git a/src/components/Teams/MemberControls/MemberControls.js b/src/components/Teams/MemberControls/MemberControls.js
new file mode 100644
index 000000000..2147dee3a
--- /dev/null
+++ b/src/components/Teams/MemberControls/MemberControls.js
@@ -0,0 +1,48 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { MY_TEAMS } from '../TeamQueries'
+import RemoveMemberControlItem from './RemoveMemberControlItem'
+import AcceptInviteControlItem from './AcceptInviteControlItem'
+import DeclineInviteControlItem from './DeclineInviteControlItem'
+
+/**
+ * Menu of controls for a team member
+ */
+export const MemberControls = props => {
+ return (
+
+ )
+}
+
+MemberControls.propTypes = {
+ user: PropTypes.object.isRequired,
+ team: PropTypes.object.isRequired,
+ teamMember: PropTypes.object.isRequired,
+}
+
+export default MemberControls
diff --git a/src/components/Teams/MemberControls/Messages.js b/src/components/Teams/MemberControls/Messages.js
new file mode 100644
index 000000000..0efd5f5b0
--- /dev/null
+++ b/src/components/Teams/MemberControls/Messages.js
@@ -0,0 +1,26 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with MemberControls
+ */
+export default defineMessages({
+ acceptInviteLabel: {
+ id: "Team.member.controls.acceptInvite.label",
+ defaultMessage: "Join Team",
+ },
+
+ declineInviteLabel: {
+ id: "Team.member.controls.declineInvite.label",
+ defaultMessage: "Decline Invite",
+ },
+
+ removeMemberLabel: {
+ id: "Team.member.controls.delete.label",
+ defaultMessage: "Remove User",
+ },
+
+ leaveTeamLabel: {
+ id: "Team.member.controls.leave.label",
+ defaultMessage: "Leave Team",
+ }
+})
diff --git a/src/components/Teams/MemberControls/RemoveMemberControlItem.js b/src/components/Teams/MemberControls/RemoveMemberControlItem.js
new file mode 100644
index 000000000..0769814fc
--- /dev/null
+++ b/src/components/Teams/MemberControls/RemoveMemberControlItem.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import { useMutation } from '@apollo/client'
+import AppErrors from '../../../services/Error/AppErrors'
+import ConfirmAction from '../../ConfirmAction/ConfirmAction'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import { REMOVE_USER } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Control item for removing a member from a team (either as an admin, or
+ * leaving a team as the current user)
+ */
+const RemoveMemberControlItem = props => {
+ const [removeTeamUser, { loading: isSaving }] = useMutation(REMOVE_USER)
+
+ // Only the member itself or a team admin can remove members, and members can
+ // only remove themselves if they are active on the team (if they are merely
+ // invited, they need to decline the invitation instead)
+ const isAdmin = props.userTeamMember && props.userTeamMember.isTeamAdmin()
+ if (!isAdmin) {
+ if (!props.teamMember.isUser(props.user) || !props.teamMember.isActive()) {
+ return null
+ }
+ }
+
+ if (isSaving) {
+ return
+ }
+
+ return (
+
+
+ { /* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ {
+ removeTeamUser({
+ variables: {
+ teamId: props.teamMember.team.id,
+ userId: props.teamMember.userId
+ },
+ refetchQueries: props.refetchQueries,
+ })
+ .catch(error => {
+ props.addErrorWithDetails(AppErrors.team.failure, error.message)
+ })
+ }}
+ >
+ {props.teamMember.isUser(props.user) ?
+ :
+
+ }
+
+
+
+ )
+}
+
+RemoveMemberControlItem.propTypes = {
+ user: PropTypes.object.isRequired,
+ userTeamMember: PropTypes.object,
+ teamMember: PropTypes.object.isRequired,
+ refetchQueries: PropTypes.array,
+}
+
+RemoveMemberControlItem.defaultProps = {
+ refetchQueries: [],
+}
+
+export default RemoveMemberControlItem
diff --git a/src/components/Teams/MemberItem/MemberItem.js b/src/components/Teams/MemberItem/MemberItem.js
new file mode 100644
index 000000000..b1b5cba6f
--- /dev/null
+++ b/src/components/Teams/MemberItem/MemberItem.js
@@ -0,0 +1,96 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Link } from 'react-router-dom'
+import { FormattedMessage } from 'react-intl'
+import { useMutation } from '@apollo/client'
+import AppErrors from '../../../services/Error/AppErrors'
+import AsTeamMember from '../../../interactions/TeamMember/AsTeamMember'
+import RolePicker from '../../RolePicker/RolePicker'
+import Dropdown from '../../Dropdown/Dropdown'
+import SvgSymbol from '../../SvgSymbol/SvgSymbol'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import MemberControls from '../MemberControls/MemberControls'
+import { UPDATE_ROLE } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Lists a team member along with member controls
+ */
+const MemberItem = props => {
+ const [updateRole, { loading: isLoading }] = useMutation(UPDATE_ROLE)
+
+ if (isLoading) {
+ return
+ }
+
+ const teamMember = AsTeamMember(props.teamUser)
+ return (
+
+
+
+ {teamMember.name}
+ {teamMember.isUser(props.user) &&
+
+
+
+ }
+
+
+
+
+ {(props.userTeamMember.isTeamAdmin() && teamMember.isActive()) ?
+
+ updateRole({
+ variables: {
+ teamId: teamMember.team.id,
+ userId: teamMember.userId,
+ role: parseInt(role),
+ },
+ refetchQueries: ['TeamUsers'],
+ })
+ .catch(error => {
+ props.addErrorWithDetails(AppErrors.team.failure, error.message)
+ })
+ }
+ /> :
+
+ }
+
+
+ {(props.userTeamMember.isTeamAdmin() || teamMember.isUser(props.user)) &&
+ (
+
+
+
+ )}
+ dropdownContent={dropdown =>
+
+ }
+ />
+ }
+
+
+
+
+ )
+}
+
+MemberItem.propTypes = {
+ user: PropTypes.object.isRequired,
+ userTeamMember: PropTypes.object.isRequired,
+ teamUser: PropTypes.object.isRequired,
+}
+
+export default MemberItem
diff --git a/src/components/Teams/MemberItem/Messages.js b/src/components/Teams/MemberItem/Messages.js
new file mode 100644
index 000000000..7e1d2fdaf
--- /dev/null
+++ b/src/components/Teams/MemberItem/Messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with MemberItem
+ */
+export default defineMessages({
+ youLabel: {
+ id: "Team.members.indicator.you.label",
+ defaultMessage: "(you)",
+ },
+})
diff --git a/src/components/Teams/MyTeams/Messages.js b/src/components/Teams/MyTeams/Messages.js
new file mode 100644
index 000000000..12631a5b5
--- /dev/null
+++ b/src/components/Teams/MyTeams/Messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with MyTeams
+ */
+export default defineMessages({
+ noTeams: {
+ id: "Team.noTeams",
+ defaultMessage: "You are not a member of any teams",
+ },
+})
diff --git a/src/components/Teams/MyTeams/MyTeams.js b/src/components/Teams/MyTeams/MyTeams.js
new file mode 100644
index 000000000..97a506adb
--- /dev/null
+++ b/src/components/Teams/MyTeams/MyTeams.js
@@ -0,0 +1,112 @@
+import React, { useEffect } from 'react'
+import PropTypes from 'prop-types'
+import { useQuery } from '@apollo/client'
+import { FormattedMessage } from 'react-intl'
+import _find from 'lodash/find'
+import _throttle from 'lodash/throttle'
+import { subscribeToTeamUpdates, unsubscribeFromTeamUpdates }
+ from '../../../services/Team/Team'
+import AsTeamMember from '../../../interactions/TeamMember/AsTeamMember'
+import Dropdown from '../../Dropdown/Dropdown'
+import SvgSymbol from '../../SvgSymbol/SvgSymbol'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import TeamControls from '../TeamControls/TeamControls'
+import { MY_TEAMS } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Lists all the teams on which the current user is a member
+ */
+export const MyTeams = function(props) {
+ const { loading, error, data, refetch } = useQuery(MY_TEAMS, {
+ variables: { userId: props.user.id }
+ })
+
+ // Refresh the teams when this component updates to avoid stale data. It's
+ // throttled down to one request at most every 2 seconds to avoid rapid
+ // refreshing
+ useEffect(() => refreshTeams(refetch))
+
+ useEffect(() => {
+ subscribeToTeamUpdates(message => {
+ if (message.data.userId === props.user.id ||
+ _find(data.userTeams, {teamId: message.data.teamId})) {
+ refetch()
+ }
+ }, "MyTeams")
+
+ return () => unsubscribeFromTeamUpdates("MyTeams")
+ })
+
+ if (error) {
+ throw error
+ }
+
+ if (loading) {
+ return
+ }
+
+ const teamItems = data.userTeams.map(teamUser => {
+ const teamMember = AsTeamMember(teamUser)
+ return (
+
+
+
+ )
+ })
+
+ return (
+
+ {teamItems.length === 0 ?
+
:
+
+ }
+
+ )
+}
+
+const refreshTeams = _throttle((refetch) => {
+ refetch()
+}, 2000, {leading: true, trailing: false})
+
+MyTeams.propTypes = {
+ user: PropTypes.object.isRequired,
+ viewTeam: PropTypes.func.isRequired,
+ editTeam: PropTypes.func.isRequired,
+}
+
+export default MyTeams
diff --git a/src/components/Teams/TeamControls/Messages.js b/src/components/Teams/TeamControls/Messages.js
new file mode 100644
index 000000000..e583f15a7
--- /dev/null
+++ b/src/components/Teams/TeamControls/Messages.js
@@ -0,0 +1,36 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with TeamControls
+ */
+export default defineMessages({
+ viewTeamLabel: {
+ id: "Team.controls.view.label",
+ defaultMessage: "View Team",
+ },
+
+ editTeamLabel: {
+ id: "Team.controls.edit.label",
+ defaultMessage: "Edit Team",
+ },
+
+ deleteTeamLabel: {
+ id: "Team.controls.delete.label",
+ defaultMessage: "Delete Team",
+ },
+
+ acceptInviteLabel: {
+ id: "Team.controls.acceptInvite.label",
+ defaultMessage: "Join Team",
+ },
+
+ declineInviteLabel: {
+ id: "Team.controls.declineInvite.label",
+ defaultMessage: "Decline Invite",
+ },
+
+ leaveTeamLabel: {
+ id: "Team.controls.leave.label",
+ defaultMessage: "Leave Team",
+ },
+})
diff --git a/src/components/Teams/TeamControls/TeamControls.js b/src/components/Teams/TeamControls/TeamControls.js
new file mode 100644
index 000000000..32af3682c
--- /dev/null
+++ b/src/components/Teams/TeamControls/TeamControls.js
@@ -0,0 +1,91 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import { useMutation } from '@apollo/client'
+import AppErrors from '../../../services/Error/AppErrors'
+import ConfirmAction from '../../ConfirmAction/ConfirmAction'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import RemoveMemberControlItem from '../MemberControls/RemoveMemberControlItem'
+import AcceptInviteControlItem from '../MemberControls/AcceptInviteControlItem'
+import DeclineInviteControlItem from '../MemberControls/DeclineInviteControlItem'
+import { DELETE_TEAM } from '../TeamQueries'
+import messages from './Messages'
+
+/**
+ * Menu of controls for a team
+ */
+export const TeamControls = props => {
+ const [deleteTeam, { loading: isDeleting }] = useMutation(DELETE_TEAM)
+ const isSaving = isDeleting
+
+ if (isSaving) {
+ return
+ }
+
+ const isAdmin = props.teamMember.isUser(props.user) && props.teamMember.isTeamAdmin()
+ return (
+
+ )
+}
+
+TeamControls.propTypes = {
+ teamMember: PropTypes.object.isRequired,
+ viewTeam: PropTypes.func.isRequired,
+ editTeam: PropTypes.func.isRequired,
+}
+
+export default TeamControls
diff --git a/src/components/Teams/TeamQueries.js b/src/components/Teams/TeamQueries.js
new file mode 100644
index 000000000..6b9bbdb40
--- /dev/null
+++ b/src/components/Teams/TeamQueries.js
@@ -0,0 +1,100 @@
+import { gql } from '@apollo/client'
+
+export const MY_TEAMS = gql`
+ query MyTeams($userId: Long!) {
+ userTeams(id: $userId) {
+ id
+ userId
+ status
+ teamGrants {
+ id
+ role
+ }
+ team {
+ id
+ name
+ description
+ }
+ }
+ }
+`
+
+export const TEAM_USERS = gql`
+ query TeamUsers($teamId: Long!) {
+ teamUsers(id: $teamId) {
+ id
+ userId
+ name
+ team {
+ id
+ }
+ teamGrants {
+ id
+ role
+ }
+ status
+ }
+ }
+`
+
+export const CREATE_TEAM = gql`
+ mutation CreateTeam($name: String!, $description: String!) {
+ createTeam(name: $name, description: $description) {
+ id
+ name
+ description
+ }
+ }
+`
+
+export const UPDATE_TEAM = gql`
+ mutation UpdateTeam($id: Long!, $name: String, $description: String) {
+ updateTeam(id: $id, name: $name, description: $description) {
+ id
+ name
+ description
+ }
+ }
+`
+
+export const INVITE_USER = gql`
+ mutation InviteUser($teamId: Long!, $userId: Long!, $role: Int!) {
+ inviteTeamUser(id: $teamId, userId: $userId, role: $role) {
+ id
+ }
+ }
+`
+
+export const ACCEPT_INVITE = gql`
+ mutation AcceptInvite($teamId: Long!) {
+ acceptTeamInvite(id: $teamId) {
+ id
+ }
+ }
+`
+
+export const DECLINE_INVITE = gql`
+ mutation DeclineInvite($teamId: Long!) {
+ declineTeamInvite(id: $teamId)
+ }
+`
+
+export const UPDATE_ROLE = gql`
+ mutation UpdateRole($teamId: Long!, $userId: Long!, $role: Int!) {
+ updateMemberRole(id: $teamId, userId: $userId, role: $role) {
+ id
+ }
+ }
+`
+
+export const REMOVE_USER = gql`
+ mutation RemoveUser($teamId: Long!, $userId: Long!) {
+ removeTeamUser(id: $teamId, userId: $userId)
+ }
+`
+
+export const DELETE_TEAM = gql`
+ mutation DeleteTeam($teamId: Long!) {
+ deleteTeam(id: $teamId)
+ }
+`
diff --git a/src/components/Teams/ViewTeam/Messages.js b/src/components/Teams/ViewTeam/Messages.js
new file mode 100644
index 000000000..fca7e47be
--- /dev/null
+++ b/src/components/Teams/ViewTeam/Messages.js
@@ -0,0 +1,21 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with ViewTeam
+ */
+export default defineMessages({
+ activeMembersHeader: {
+ id: "Team.activeMembers.header",
+ defaultMessage: "Active Members",
+ },
+
+ invitedMembersHeader: {
+ id: "Team.invitedMembers.header",
+ defaultMessage: "Pending Invitations",
+ },
+
+ addMembersHeader: {
+ id: "Team.addMembers.header",
+ defaultMessage: "Invite New Member",
+ },
+})
diff --git a/src/components/Teams/ViewTeam/ViewTeam.js b/src/components/Teams/ViewTeam/ViewTeam.js
new file mode 100644
index 000000000..46c486b0a
--- /dev/null
+++ b/src/components/Teams/ViewTeam/ViewTeam.js
@@ -0,0 +1,103 @@
+import React, { useEffect } from 'react'
+import PropTypes from 'prop-types'
+import { useQuery } from '@apollo/client'
+import { FormattedMessage } from 'react-intl'
+import _find from 'lodash/find'
+import _filter from 'lodash/filter'
+import { subscribeToTeamUpdates, unsubscribeFromTeamUpdates }
+ from '../../../services/Team/Team'
+import AsTeamMember from '../../../interactions/TeamMember/AsTeamMember'
+import BusySpinner from '../../BusySpinner/BusySpinner'
+import AddTeamMember from '../AddTeamMember/AddTeamMember'
+import MemberItem from '../MemberItem/MemberItem'
+import { TEAM_USERS } from '../TeamQueries'
+import messages from './Messages'
+
+export const ViewTeam = props => {
+ const { loading, error, data, refetch } = useQuery(TEAM_USERS, {
+ variables: { teamId: props.team.id }
+ })
+
+ useEffect(() => {
+ subscribeToTeamUpdates(message => {
+ if (message.data.userId === props.user.id ||
+ message.data.teamId === props.team.id) {
+ refetch()
+ }
+ }, "ViewTeam")
+
+ return () => unsubscribeFromTeamUpdates("ViewTeam")
+ })
+
+ if (error) {
+ throw error
+ }
+
+ if (loading) {
+ return
+ }
+
+ const userTeamMember = AsTeamMember(
+ _find(data.teamUsers, teamUser => AsTeamMember(teamUser).isUser(props.user))
+ )
+
+ const activeMembers =
+ _filter(data.teamUsers, teamUser => AsTeamMember(teamUser).isActive())
+
+ const invitedMembers =
+ _filter(data.teamUsers, teamUser => AsTeamMember(teamUser).isInvited())
+
+ return (
+
+
{props.team.name}
+
{props.team.description}
+
+
+
+
+
+ {activeMembers.map(member =>
+
+ )}
+
+
+ {invitedMembers.length > 0 &&
+
+
+
+
+
+ {invitedMembers.map(member =>
+
+ )}
+
+
+ }
+
+ {userTeamMember.isTeamAdmin() &&
+
+
+
+
+
+
+ }
+
+ )
+}
+
+ViewTeam.propTypes = {
+ team: PropTypes.object.isRequired,
+}
+
+export default ViewTeam
diff --git a/src/components/TopUserChallenges/TopUserChallengesWidget.js b/src/components/TopUserChallenges/TopUserChallengesWidget.js
index cfe789d3b..131980409 100644
--- a/src/components/TopUserChallenges/TopUserChallengesWidget.js
+++ b/src/components/TopUserChallenges/TopUserChallengesWidget.js
@@ -87,7 +87,7 @@ const TopChallengeList = function(props) {
}
return (
-
+
{challenge.name}
diff --git a/src/components/WidgetGrid/WidgetGrid.js b/src/components/WidgetGrid/WidgetGrid.js
index 0329f0ca4..e1cf6f8d2 100644
--- a/src/components/WidgetGrid/WidgetGrid.js
+++ b/src/components/WidgetGrid/WidgetGrid.js
@@ -99,6 +99,11 @@ export class WidgetGrid extends Component {
}