diff --git a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx index 717f1c62bde..d303bcee7b2 100644 --- a/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx +++ b/packages/client/components/ActivityLibrary/ActivityDetailsSidebar.tsx @@ -1,7 +1,7 @@ import {LockOpen} from '@mui/icons-material' import graphql from 'babel-plugin-relay/macro' import clsx from 'clsx' -import React, {useState} from 'react' +import React, {useState, useEffect, useRef} from 'react' import {useFragment} from 'react-relay' import StartSprintPokerMutation from '~/mutations/StartSprintPokerMutation' import {useHistory} from 'react-router' @@ -132,12 +132,24 @@ const ActivityDetailsSidebar = (props: Props) => { : // it is a team-scoped template, templateTeam must exist [templateTeam!] + const availableTeamsRef = useRef(availableTeams) + + useEffect(() => { + availableTeamsRef.current = availableTeams + }, [availableTeams]) + const [selectedTeam, setSelectedTeam] = useState( () => availableTeams.find((team) => team.id === preferredTeamId) ?? templateTeam ?? sortByTier(availableTeams)[0]! ) + + const onSelectTeam = (teamId: string) => { + const currentAvailableTeams = availableTeamsRef.current + const newTeam = currentAvailableTeams.find((team) => team.id === teamId) + newTeam && setSelectedTeam(newTeam) + } const mutationProps = useMutationProps() const {onError, onCompleted, submitting, submitMutation, error} = mutationProps const history = useHistory() @@ -322,10 +334,7 @@ const ActivityDetailsSidebar = (props: Props) => { ) : ( { - const newTeam = availableTeams.find((team) => team.id === teamId) - newTeam && setSelectedTeam(newTeam) - }} + onSelectTeam={onSelectTeam} selectedTeamRef={selectedTeam} teamsRef={availableTeams} customPortal={teamScopePopover} diff --git a/packages/client/components/AddTeamDialog.tsx b/packages/client/components/AddTeamDialog.tsx new file mode 100644 index 00000000000..a93ab6fb335 --- /dev/null +++ b/packages/client/components/AddTeamDialog.tsx @@ -0,0 +1,213 @@ +import React, {useState} from 'react' +import {PreloadedQuery, usePreloadedQuery, useFragment} from 'react-relay' +import FlatPrimaryButton from './FlatPrimaryButton' +import {Dialog} from '../ui/Dialog/Dialog' +import {DialogContent} from '../ui/Dialog/DialogContent' +import {DialogTitle} from '../ui/Dialog/DialogTitle' +import {DialogActions} from '../ui/Dialog/DialogActions' +import {Select} from '../ui/Select/Select' +import {SelectItem} from '../ui/Select/SelectItem' +import {SelectTrigger} from '../ui/Select/SelectTrigger' +import {SelectGroup} from '../ui/Select/SelectGroup' +import {SelectValue} from '../ui/Select/SelectValue' +import {SelectContent} from '../ui/Select/SelectContent' +import graphql from 'babel-plugin-relay/macro' +import {AddTeamDialogQuery} from '../__generated__/AddTeamDialogQuery.graphql' +import {AddTeamDialog_viewer$key} from '../__generated__/AddTeamDialog_viewer.graphql' +import useMutationProps from '../hooks/useMutationProps' +import useAtmosphere from '../hooks/useAtmosphere' +import {AdhocTeamMultiSelect, Option} from '../components/AdhocTeamMultiSelect/AdhocTeamMultiSelect' +import {Input} from '../ui/Input/Input' +import AddTeamMutation from '~/mutations/AddTeamMutation' +import useRouter from '~/hooks/useRouter' +import getGraphQLError from '~/utils/relay/getGraphQLError' +import SendClientSegmentEventMutation from '~/mutations/SendClientSegmentEventMutation' + +interface Props { + isOpen: boolean + onClose: () => void + onAddTeam: (teamId: string) => void + queryRef: PreloadedQuery +} + +const AddTeamDialogViewerFragment = graphql` + fragment AddTeamDialog_viewer on User { + ...AdhocTeamMultiSelect_viewer + organizations { + id + name + } + } +` + +const query = graphql` + query AddTeamDialogQuery { + viewer { + ...AddTeamDialog_viewer + } + } +` + +const AddTeamDialog = (props: Props) => { + const {isOpen, onClose, queryRef, onAddTeam} = props + const atmosphere = useAtmosphere() + + const {submitting, onCompleted, onError, error, submitMutation} = useMutationProps() + const {history} = useRouter() + + const data = usePreloadedQuery(query, queryRef) + const viewer = useFragment(AddTeamDialogViewerFragment, data.viewer) + const {organizations: viewerOrganizations} = viewer + + const [selectedUsers, setSelectedUsers] = useState([]) + const [mutualOrgsIds, setMutualOrgsIds] = useState([]) + + const showOrgPicker = !!( + selectedUsers.length && + (mutualOrgsIds.length > 1 || !mutualOrgsIds.length) && + viewerOrganizations.length > 1 + ) + + const defaultOrgId = mutualOrgsIds[0] + const [selectedOrgId, setSelectedOrgId] = useState(defaultOrgId) + const [teamName, setTeamName] = useState('') + const [teamNameManuallyEdited, setTeamNameManuallyEdited] = useState(false) + + const MAX_TEAM_NAME_LENGTH = 50 + const generateTeamName = (newUsers: Option[]) => { + return newUsers + .map((user) => (user.id ? user.label : user.email.split('@')[0])) + .join(', ') + .substring(0, MAX_TEAM_NAME_LENGTH) + } + + const onSelectedUsersChange = (newUsers: Option[]) => { + setSelectedUsers(newUsers) + const selectedUsersOrganizationIds = new Set() + newUsers.forEach((user) => { + //add user.organizationIds to selectedUserOrganizationIds + user.organizationIds.forEach((organizationId) => { + selectedUsersOrganizationIds.add(organizationId) + }) + }) + + const mutualOrgs = viewerOrganizations.filter((org) => selectedUsersOrganizationIds.has(org.id)) + + const mutualOrgsIds = mutualOrgs.map((org) => org.id) + setMutualOrgsIds(mutualOrgsIds) + setSelectedOrgId(mutualOrgsIds[0] ?? viewerOrganizations[0]?.id) + + if (!teamNameManuallyEdited) { + setTeamName(generateTeamName(newUsers)) + } + + if (newUsers.length && newUsers.length > selectedUsers.length) { + SendClientSegmentEventMutation(atmosphere, 'Teammate Selected', { + selectionLocation: 'addTeamUserPicker' + }) + } + } + + const handleAddTeam = () => { + const newTeam = { + name: teamName, + orgId: selectedOrgId + } + submitMutation() + + AddTeamMutation( + atmosphere, + {newTeam, invitees: selectedUsers.map((user) => user.email)}, + { + onError, + onCompleted: (res, errors) => { + onCompleted(res, errors) + const error = getGraphQLError(res, errors) + if (!error) { + onAddTeam(res.addTeam.team.id) + } + }, + history, + showTeamCreatedToast: false + } + ) + } + + const isValid = selectedUsers.length && teamName.trim() + + const labelStyles = `text-left text-sm font-semibold mb-3` + const fieldsetStyles = `mx-0 mb-6 flex flex-col w-full p-0` + + return ( + + + Add team + +
+ + + + + {selectedUsers.some((user: Option) => !user.id) && ( +
+ Email invitations expire in 30 days. +
+ )} +
+ + {showOrgPicker && ( +
+ + +
+ )} + +
+ + { + if (!teamNameManuallyEdited) { + setTeamNameManuallyEdited(true) + } + setTeamName(e.target.value) + }} + value={teamName} + /> + {error && ( +
{error.message}
+ )} +
+ + + Add team + + +
+
+ ) +} + +export default AddTeamDialog diff --git a/packages/client/components/AddTeamDialogRoot.tsx b/packages/client/components/AddTeamDialogRoot.tsx new file mode 100644 index 00000000000..c9bcfa284b1 --- /dev/null +++ b/packages/client/components/AddTeamDialogRoot.tsx @@ -0,0 +1,26 @@ +import React, {Suspense} from 'react' +import AddTeamDialog from './AddTeamDialog' +import {renderLoader} from '../utils/relay/renderLoader' +import useQueryLoaderNow from '../hooks/useQueryLoaderNow' +import addTeamDialogQuery, {AddTeamDialogQuery} from '../__generated__/AddTeamDialogQuery.graphql' + +interface Props { + onClose: () => void + onAddTeam: (teamId: string) => void +} + +const AddTeamDialogRoot = (props: Props) => { + const {onClose, onAddTeam} = props + + const queryRef = useQueryLoaderNow(addTeamDialogQuery, {}, 'network-only') + + return ( + + {queryRef && ( + + )} + + ) +} + +export default AddTeamDialogRoot diff --git a/packages/client/components/AdhocTeamMultiSelect/AdhocTeamMultiSelect.tsx b/packages/client/components/AdhocTeamMultiSelect/AdhocTeamMultiSelect.tsx index f59863e72ed..2f1a92fb107 100644 --- a/packages/client/components/AdhocTeamMultiSelect/AdhocTeamMultiSelect.tsx +++ b/packages/client/components/AdhocTeamMultiSelect/AdhocTeamMultiSelect.tsx @@ -166,7 +166,7 @@ export const AdhocTeamMultiSelect = (props: Props) => {
{value.map((option, index: number) => ( { className='m-0 box-border min-h-[36px] w-0 min-w-[30px] flex-grow border-0 bg-white pl-1 text-black outline-none' />
-
{error}
+ {error &&
{error}
}
{groupedOptions.length > 0 ? (
    @@ -39,6 +41,8 @@ const NewMeetingTeamPicker = (props: Props) => { } ) + const [addTeamDialogOpen, setAddTeamDialogOpen] = React.useState(false) + const atmosphere = useAtmosphere() const handleSelectTeam = (teamId: string) => { @@ -47,7 +51,8 @@ const NewMeetingTeamPicker = (props: Props) => { } const handleAddTeamClick = () => { - window.open(`/newteam/1`, '_blank', 'noreferrer') + SendClientSegmentEventMutation(atmosphere, 'Add Team Clicked') + setAddTeamDialogOpen(true) } const selectedTeam = useFragment( @@ -96,6 +101,17 @@ const NewMeetingTeamPicker = (props: Props) => { /> ) )} + {addTeamDialogOpen && ( + { + setAddTeamDialogOpen(false) + handleSelectTeam(teamId) + }} + onClose={() => { + setAddTeamDialogOpen(false) + }} + /> + )} ) } diff --git a/packages/client/mutations/AddTeamMutation.ts b/packages/client/mutations/AddTeamMutation.ts index 3428f99bb56..e91768acc35 100644 --- a/packages/client/mutations/AddTeamMutation.ts +++ b/packages/client/mutations/AddTeamMutation.ts @@ -22,6 +22,7 @@ graphql` ...NewTeamForm_teams ...MeetingsDashActiveMeetings ...Team_team + ...ActivityDetailsSidebar_teams } } ` @@ -71,10 +72,13 @@ export const addTeamMutationNotificationUpdater: SharedUpdater< handleRemoveSuggestedActions(removedSuggestedActionId, store) } -const AddTeamMutation: StandardMutation = ( +type ExtendedHistoryLocalHandler = HistoryLocalHandler & { + showTeamCreatedToast?: boolean +} +const AddTeamMutation: StandardMutation = ( atmosphere, variables, - {history, onError, onCompleted} + {history, onError, onCompleted, showTeamCreatedToast = true} ) => { return commitMutation(atmosphere, { mutation, @@ -91,7 +95,9 @@ const AddTeamMutation: StandardMutation = if (!error) { const {authToken} = addTeam atmosphere.setAuthToken(authToken) - popTeamCreatedToast(addTeam, {atmosphere, history}) + if (showTeamCreatedToast) { + popTeamCreatedToast(addTeam, {atmosphere, history}) + } } }, onError diff --git a/packages/client/ui/Input/Input.tsx b/packages/client/ui/Input/Input.tsx new file mode 100644 index 00000000000..7841562d663 --- /dev/null +++ b/packages/client/ui/Input/Input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +import {twMerge} from 'tailwind-merge' + +export interface InputProps extends React.InputHTMLAttributes {} + +export const Input = React.forwardRef( + ({className, type, ...props}, ref) => { + return ( + + ) + } +) diff --git a/packages/client/ui/Select/SelectTrigger.tsx b/packages/client/ui/Select/SelectTrigger.tsx index 20999179d79..fa10a887174 100644 --- a/packages/client/ui/Select/SelectTrigger.tsx +++ b/packages/client/ui/Select/SelectTrigger.tsx @@ -13,7 +13,7 @@ export const SelectTrigger = React.forwardRef { const {authToken} = context - const viewerId = getUserId(authToken) const existingUser = await getUserByEmail(email)