Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(standups): Responses grid with static prompt #6353

Merged
merged 8 commits into from
Apr 22, 2022
131 changes: 120 additions & 11 deletions packages/client/components/TeamPromptMeeting.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,80 @@
import styled from '@emotion/styled'
import graphql from 'babel-plugin-relay/macro'
import React, {Suspense} from 'react'
import {Link} from 'react-router-dom'

import { TeamPromptMeeting_meeting$key } from '~/__generated__/TeamPromptMeeting_meeting.graphql'
import logoMarkPurple from '../styles/theme/images/brand/mark-color.svg'
import {useFragment} from 'react-relay'
import useBreakpoint from '~/hooks/useBreakpoint'
import useMeeting from '~/hooks/useMeeting'
import useTransition, {TransitionStatus} from '~/hooks/useTransition'
import {Elevation} from '~/styles/elevation'
import {BezierCurve, Breakpoint, Card} from '~/types/constEnums'
import {TeamPromptMeeting_meeting$key} from '~/__generated__/TeamPromptMeeting_meeting.graphql'
import getPhaseByTypename from '../utils/getPhaseByTypename'
import Avatar from './Avatar/Avatar'
import ErrorBoundary from './ErrorBoundary'
import MeetingArea from './MeetingArea'
import MeetingContent from './MeetingContent'
import MeetingHeaderAndPhase from './MeetingHeaderAndPhase'
import MeetingStyles from './MeetingStyles'
import TeamPromptTopBar from './TeamPrompt/TeamPromptTopBar'
import PhaseWrapper from './PhaseWrapper'
import {useFragment} from 'react-relay'

const Dimensions = {
RESPONSE_WIDTH: 296,
RESPONSE_MIN_HEIGHT: 100
}

const Prompt = styled('h1')({
textAlign: 'center',
margin: 16,
fontSize: 20,
lineHeight: '32px',
fontWeight: 400
})

const ResponsesGridContainer = styled('div')<{maybeTabletPlus: boolean}>(({maybeTabletPlus}) => ({
height: '100%',
overflow: 'auto',
padding: maybeTabletPlus ? '32px 10%' : 16
}))

const ResponsesGrid = styled('div')({
flex: 1,
display: 'flex',
flexWrap: 'wrap',
position: 'relative',
gap: 32
})

const TeamMemberResponse = styled('div')<{
status: TransitionStatus
}>(({status}) => ({
opacity: status === TransitionStatus.MOUNTED || status === TransitionStatus.EXITING ? 0 : 1,
transition: `box-shadow 100ms ${BezierCurve.DECELERATE}, opacity 300ms ${BezierCurve.DECELERATE}`,
display: 'flex',
flexDirection: 'column',
width: Dimensions.RESPONSE_WIDTH,
flexShrink: 0
}))

const ResponseCard = styled('div')({
background: Card.BACKGROUND_COLOR,
borderRadius: Card.BORDER_RADIUS,
boxShadow: Elevation.CARD_SHADOW,
flex: 1,
padding: Card.PADDING,
minHeight: Dimensions.RESPONSE_MIN_HEIGHT,
userSelect: 'none'
})

const ResponseHeader = styled('div')({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: '0 8px'
})

const TeamMemberName = styled('h3')({
padding: '0 8px'
})

interface Props {
meeting: TeamPromptMeeting_meeting$key
Expand All @@ -22,10 +86,35 @@ const TeamPromptMeeting = (props: Props) => {
graphql`
fragment TeamPromptMeeting_meeting on TeamPromptMeeting {
...TeamPromptTopBar_meeting
...useMeeting_meeting
phases {
... on TeamPromptResponsesPhase {
__typename
stages {
teamMember {
id
preferredName
picture
}
}
}
}
}
`,
meetingRef
)
const {phases} = meeting
const maybeTabletPlus = useBreakpoint(Breakpoint.FUZZY_TABLET)

const phase = getPhaseByTypename(phases, 'TeamPromptResponsesPhase')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BartoszJarocki let me know if this gets you whatcha need. if so, go ahead & merge this PR & we'll start using this more in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, looks good! thank you!

const {stages} = phase
const teamMembers = stages.map((stage) => ({
...stage.teamMember,
key: stage.teamMember.id
}))
const transitioningTeamMembers = useTransition(teamMembers)
const {safeRoute} = useMeeting(meeting)
if (!safeRoute) return null

return (
<MeetingStyles>
Expand All @@ -34,11 +123,31 @@ const TeamPromptMeeting = (props: Props) => {
<MeetingContent>
<MeetingHeaderAndPhase hideBottomBar={true}>
<TeamPromptTopBar meetingRef={meeting} />
<PhaseWrapper>
<Link title='My Dashboard' to='/meetings'>
<img alt='Parabol' src={logoMarkPurple} />
</Link>
</PhaseWrapper>
<Prompt>What are you working on today? Stuck on anything?</Prompt>
<ErrorBoundary>
<ResponsesGridContainer maybeTabletPlus={maybeTabletPlus}>
<ResponsesGrid>
{transitioningTeamMembers.map((teamMember) => {
const {child, onTransitionEnd, status} = teamMember
const {key, picture, preferredName} = child

return (
<TeamMemberResponse
key={key}
status={status}
onTransitionEnd={onTransitionEnd}
>
<ResponseHeader>
<Avatar picture={picture} size={48} />
<TeamMemberName>{preferredName}</TeamMemberName>
</ResponseHeader>
<ResponseCard>Test</ResponseCard>
</TeamMemberResponse>
)
})}
</ResponsesGrid>
</ResponsesGridContainer>
</ErrorBoundary>
</MeetingHeaderAndPhase>
</MeetingContent>
</Suspense>
Expand Down
11 changes: 11 additions & 0 deletions packages/client/utils/getPhaseByTypename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {GQLType, MaybeReadonly} from '../types/generics'

const getPhaseByTypename = <TPhase extends {__typename: string}, T extends string>(
phases: MaybeReadonly<TPhase[]>,
typename: T
) => {
const phase = phases.find((phase) => phase.__typename === typename)
return phase as GQLType<TPhase, T>
}

export default getPhaseByTypename
5 changes: 3 additions & 2 deletions packages/client/utils/meetings/lookups.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CardsSVG from '../../components/CardsSVG'
import {NewMeetingPhaseTypeEnum} from '~/__generated__/ActionMeetingSidebar_meeting.graphql'
import CardsSVG from '../../components/CardsSVG'
import {ACTION, RETROSPECTIVE} from '../constants'

/* Used by the server! cannot convert to enums yet */
Expand Down Expand Up @@ -55,5 +55,6 @@ export const phaseTypeToSlug = {
lastcall: 'lastcall',
SUMMARY: 'summary',
SCOPE: 'scope',
ESTIMATE: 'estimate'
ESTIMATE: 'estimate',
RESPONSES: 'responses'
} as Record<NewMeetingPhaseTypeEnum, string>
11 changes: 8 additions & 3 deletions packages/server/graphql/mutations/joinMeeting.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {GraphQLID, GraphQLNonNull} from 'graphql'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
import getPhase from '../../utils/getPhase'
import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId'
import getRethink from '../../database/rethinkDriver'
import rMapIf from '../../database/rMapIf'
import ActionMeetingMember from '../../database/types/ActionMeetingMember'
import CheckInStage from '../../database/types/CheckInStage'
import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage'
import {NewMeetingPhaseTypeEnum} from '../../database/types/GenericMeetingPhase'
import Meeting from '../../database/types/Meeting'
import MeetingRetrospective from '../../database/types/MeetingRetrospective'
import PokerMeetingMember from '../../database/types/PokerMeetingMember'
import RetroMeetingMember from '../../database/types/RetroMeetingMember'
import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember'
import TeamMember from '../../database/types/TeamMember'
import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember'
import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage'
import UpdatesStage from '../../database/types/UpdatesStage'
import {getUserId, isTeamMember} from '../../utils/authorization'
import getPhase from '../../utils/getPhase'
import publish from '../../utils/publish'
import {GQLContext} from '../graphql'
import JoinMeetingPayload from '../types/JoinMeetingPayload'
Expand Down Expand Up @@ -135,6 +135,11 @@ const joinMeeting = {
const appendToTeamPromptResponses = async () => {
const responsesPhase = getPhase(phases, 'RESPONSES')
if (!responsesPhase) return
const teamMemberResponseStage = responsesPhase.stages.find(
(stage) => stage.teamMemberId === teamMemberId
)
// only add a new stage for the new users (ie. invited to the team after the meeting was started)
if (teamMemberResponseStage) return
const responsesStage = new TeamPromptResponseStage({teamMemberId})
return addStageToPhase(responsesStage, 'RESPONSES')
}
Expand Down
7 changes: 7 additions & 0 deletions packages/server/graphql/types/TeamPromptResponseStage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {GraphQLNonNull, GraphQLObjectType} from 'graphql'
import {NewMeetingPhaseTypeEnum} from '../../database/types/GenericMeetingPhase'
import {GQLContext} from '../graphql'
import {resolveTeamMember} from '../resolvers'
import DiscussionThreadStage, {discussionThreadStageFields} from './DiscussionThreadStage'
import NewMeetingStage, {newMeetingStageFields} from './NewMeetingStage'
import TeamMember from './TeamMember'
import TeamPromptResponse from './TeamPromptResponse'

const TeamPromptResponseStage = new GraphQLObjectType<any, GQLContext>({
Expand All @@ -13,6 +15,11 @@ const TeamPromptResponseStage = new GraphQLObjectType<any, GQLContext>({
fields: () => ({
...newMeetingStageFields(),
...discussionThreadStageFields(),
teamMember: {
type: new GraphQLNonNull(TeamMember),
description: 'The team member this stage belongs to',
resolve: resolveTeamMember
},
response: {
type: new GraphQLNonNull(TeamPromptResponse),
description: 'The response to the prompt',
Expand Down