-
Notifications
You must be signed in to change notification settings - Fork 337
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ad-hoc): Add create team dialog (#8846)
* chore(one-on-one): add oneOnOne org feature flag * chore(one-on-one): add one-on-one meeting template * Change color * Put one on one in standup * feat(one-on-one): allow oneOnOne input in startCheckIn mutation * fix checkin * revert duplicate meeting check * Remove userId option * Publish team payload * chore(one-on-one): Add user picker * Submit mutation * remove ui/input * Code cleanup * chore(one-on-one): add user picker styles * args validation * prettier * generateOneOnOneTeamName * isOneOnOneTeam * chore(one-on-one): show organiation picker if can't determine org automatically * Enable multiple false * chore(one-on-one): Add team exists warning * use isOneOnOne team to find teams * Add analytics * Show only mutual orgs * Show name in warning * Exclude self * Add a validation * Avatar * fix meeting name * Separate files * simplify * Remove useEffect * autoselect * Hide schedule button * Move team exists check to organization * feat(ad-hoc): allow to create team from teams dropdown * Rename component * use organization(id) * handleAddTeamClick * add Teammate Selected event * feat(ad-hoc): Add create team dialog * Don't redirect after team creation * Fix new team select * automatically select team * validation * remove comment * Auto-generate team name * Add email expires warning * Fix unnecessary spacing * tweak padding * tweak select height * Add events * Show name length error from backend * Move styles outside * Remove React prefix * Bigger button * Update styles * Don't show org picker if user has a single org
- Loading branch information
1 parent
2b9a5c9
commit 14a32aa
Showing
9 changed files
with
303 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<AddTeamDialogQuery> | ||
} | ||
|
||
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<AddTeamDialogQuery>(query, queryRef) | ||
const viewer = useFragment<AddTeamDialog_viewer$key>(AddTeamDialogViewerFragment, data.viewer) | ||
const {organizations: viewerOrganizations} = viewer | ||
|
||
const [selectedUsers, setSelectedUsers] = useState<Option[]>([]) | ||
const [mutualOrgsIds, setMutualOrgsIds] = useState<string[]>([]) | ||
|
||
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 ( | ||
<Dialog isOpen={isOpen} onClose={onClose}> | ||
<DialogContent className='z-10'> | ||
<DialogTitle className='mb-4'>Add team</DialogTitle> | ||
|
||
<fieldset className={fieldsetStyles}> | ||
<label className={labelStyles}>Add teammates</label> | ||
|
||
<AdhocTeamMultiSelect | ||
viewerRef={viewer} | ||
value={selectedUsers} | ||
onChange={onSelectedUsersChange} | ||
/> | ||
|
||
{selectedUsers.some((user: Option) => !user.id) && ( | ||
<div className='mt-3 text-xs font-semibold text-slate-700'> | ||
Email invitations expire in 30 days. | ||
</div> | ||
)} | ||
</fieldset> | ||
|
||
{showOrgPicker && ( | ||
<fieldset className={fieldsetStyles}> | ||
<label className={labelStyles}>Organization</label> | ||
<Select onValueChange={(orgId) => setSelectedOrgId(orgId)} value={selectedOrgId}> | ||
<SelectTrigger className='bg-white'> | ||
<SelectValue /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
<SelectGroup> | ||
{viewerOrganizations | ||
.filter((org) => (mutualOrgsIds.length ? mutualOrgsIds.includes(org.id) : true)) | ||
.map((org) => ( | ||
<SelectItem value={org.id} key={org.id}> | ||
{org.name} | ||
</SelectItem> | ||
))} | ||
</SelectGroup> | ||
</SelectContent> | ||
</Select> | ||
</fieldset> | ||
)} | ||
|
||
<fieldset className={fieldsetStyles}> | ||
<label className={labelStyles}>Team name</label> | ||
<Input | ||
onChange={(e) => { | ||
if (!teamNameManuallyEdited) { | ||
setTeamNameManuallyEdited(true) | ||
} | ||
setTeamName(e.target.value) | ||
}} | ||
value={teamName} | ||
/> | ||
{error && ( | ||
<div className='mt-2 text-sm font-semibold text-tomato-500'>{error.message}</div> | ||
)} | ||
</fieldset> | ||
<DialogActions> | ||
<FlatPrimaryButton | ||
size='medium' | ||
onClick={handleAddTeam} | ||
disabled={submitting || !isValid} | ||
> | ||
Add team | ||
</FlatPrimaryButton> | ||
</DialogActions> | ||
</DialogContent> | ||
</Dialog> | ||
) | ||
} | ||
|
||
export default AddTeamDialog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>(addTeamDialogQuery, {}, 'network-only') | ||
|
||
return ( | ||
<Suspense fallback={renderLoader()}> | ||
{queryRef && ( | ||
<AddTeamDialog onAddTeam={onAddTeam} isOpen={true} onClose={onClose} queryRef={queryRef} /> | ||
)} | ||
</Suspense> | ||
) | ||
} | ||
|
||
export default AddTeamDialogRoot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.