Skip to content

Commit

Permalink
feat: add meeting selector table
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick committed Feb 25, 2025
1 parent c16c5a5 commit 6f4e9b0
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 23 deletions.
12 changes: 7 additions & 5 deletions packages/client/components/MeetingDatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import DateRangeIcon from '@mui/icons-material/DateRange'
import type {NodeViewProps} from '@tiptap/core'
import dayjs from 'dayjs'
import {DayPicker} from 'react-day-picker'
Expand All @@ -12,15 +13,16 @@ interface Props {

export const MeetingDatePicker = (props: Props) => {
const {updateAttributes, attrs} = props
const {startAt, endAt} = attrs
const dateRangeLabel = `${dayjs(startAt).format('MMM D, YYYY')} - ${dayjs(endAt).format('MMM D, YYYY')}`
const {after, before} = attrs
const dateRangeLabel = `${dayjs(after).format('MMM D, YYYY')} - ${dayjs(before).format('MMM D, YYYY')}`

return (
<Menu
className='data-[side=bottom]:animate-slide-down data-[side=top]:animate-slide-up'
trigger={
<div className='group flex cursor-pointer items-center justify-between rounded-md bg-white'>
<div className='p-2 leading-4'>{dateRangeLabel}</div>
<DateRangeIcon className='text-slate-600' />
</div>
}
>
Expand All @@ -29,12 +31,12 @@ export const MeetingDatePicker = (props: Props) => {
<div className='py-2'>
<DayPicker
mode='range'
selected={{from: new Date(startAt), to: new Date(endAt)}}
selected={{from: new Date(after), to: new Date(before)}}
disabled={{after: new Date()}}
onSelect={(newSelected) => {
updateAttributes({
startAt: newSelected?.from?.toISOString(),
endAt: newSelected?.to?.toISOString()
after: newSelected?.from?.toISOString(),
before: newSelected?.to?.toISOString()
})
}}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/components/MeetingTypePickerCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface Props {
attrs: InsightsBlockAttrs
}

const MeetingTypeToReadable = {
export const MeetingTypeToReadable = {
action: 'Team Check-in',
poker: 'Sprint Poker',
retrospective: 'Retrospective',
Expand Down
165 changes: 165 additions & 0 deletions packages/client/components/SpecificMeetingPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import CheckBoxIcon from '@mui/icons-material/CheckBox'
import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'
import * as Checkbox from '@radix-ui/react-checkbox'
import type {NodeViewProps} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
import dayjs from 'dayjs'
import {useEffect} from 'react'
import {usePreloadedQuery, type PreloadedQuery} from 'react-relay'
import type {SpecificMeetingPickerQuery} from '../__generated__/SpecificMeetingPickerQuery.graphql'
import type {InsightsBlockAttrs} from '../tiptap/extensions/imageBlock/InsightsBlock'
import {MeetingTypeToReadable} from './MeetingTypePickerCombobox'
const query = graphql`
query SpecificMeetingPickerQuery(
$after: DateTime!
$first: Int!
$before: DateTime!
$meetingTypes: [MeetingTypeEnum!]!
$teamIds: [ID!]!
) {
viewer {
meetings(
after: $after
before: $before
first: $first
meetingTypes: $meetingTypes
teamIds: $teamIds
) {
edges {
node {
id
name
createdAt
meetingType
team {
name
}
}
}
}
}
}
`
interface Props {
updateAttributes: NodeViewProps['updateAttributes']
attrs: InsightsBlockAttrs
queryRef: PreloadedQuery<SpecificMeetingPickerQuery>
}

export const SpecificMeetingPicker = (props: Props) => {
const {updateAttributes, attrs, queryRef} = props
const {meetingIds, after, before} = attrs
const data = usePreloadedQuery<SpecificMeetingPickerQuery>(query, queryRef)
const {viewer} = data
const {meetings} = viewer
const {edges} = meetings

useEffect(() => {
const meetingIds = edges.map(({node}) => node.id)
updateAttributes({meetingIds})
}, [edges])
const rows = edges.map((edge) => {
const {node} = edge
const {id, createdAt, meetingType, name, team} = node
const {name: teamName} = team
return {
id,
createdAt,
meetingType: MeetingTypeToReadable[meetingType],
name,
teamName
// isSelected: meetingIds.includes(id)
}
})
const allColumns = ['Name', 'Date', 'Team', 'Type']
const ignoredTeamColumn = attrs.teamIds.length === 1 ? ['Team'] : []
const ignoredTypeColumn = attrs.meetingTypes.length === 1 ? ['Type'] : []
const ignoredColumns = ignoredTeamColumn.concat(ignoredTypeColumn)
const columns = allColumns.filter((column) => !ignoredColumns.includes(column))
const includeYear = new Date(after).getFullYear() !== new Date(before).getFullYear()
const formatter = includeYear ? 'MMM D, YYYY' : 'MMM D'
const allChecked = meetingIds.length === edges.length
const MultiIcon = allChecked ? CheckBoxIcon : IndeterminateCheckBoxIcon
return (
<div className='flex max-h-52 overflow-auto'>
<table className='w-full border-collapse'>
<thead className='sticky top-0 z-10 bg-slate-200'>
<tr className='border-b-[1px] border-slate-400'>
<th className='w-5 border-b-[1px] border-b-transparent pt-1 pr-1'>
<Checkbox.Root
onClick={() => {
const nextMeetingIds = allChecked ? [] : edges.map(({node}) => node.id)
updateAttributes({meetingIds: nextMeetingIds})
}}
checked={allChecked ? true : meetingIds.length === 0 ? false : 'indeterminate'}
className={
'flex size-4 appearance-none items-center justify-center rounded-xs border-slate-600 bg-white outline-none data-[state=unchecked]:border-2'
}
>
<Checkbox.Indicator asChild>
<MultiIcon className='w-5 fill-sky-500' />
</Checkbox.Indicator>
</Checkbox.Root>
</th>
{columns.map((column) => {
return (
<th
className={
'border-slate-400 p-2 text-left font-bold not-last-of-type:border-r-[1px]'
}
>
{column}
</th>
)
})}
</tr>
</thead>
<tbody>
{rows.map((row) => {
const {createdAt, id, meetingType, name, teamName} = row
const date = dayjs(createdAt).format(formatter)
const checked = meetingIds.includes(id)
return (
<tr
className='cursor-pointer border-slate-400 not-last-of-type:border-b-[1px]'
onClick={() => {
const nextMeetingIds = checked
? meetingIds.filter((id) => id !== row.id)
: [...meetingIds, id]
updateAttributes({meetingIds: nextMeetingIds})
}}
>
<td className='w-5 border-b-[1px] border-b-transparent pt-1'>
<Checkbox.Root
checked={checked}
className={
'flex size-4 appearance-none items-center justify-center rounded-xs border-slate-600 bg-white outline-none data-[state=unchecked]:border-2'
}
>
<Checkbox.Indicator asChild>
<CheckBoxIcon className='w-5 fill-sky-500' />
</Checkbox.Indicator>
</Checkbox.Root>
</td>
<td className='border-r-[1px] border-slate-400 p-2'>{name}</td>
<td className='border-r-[1px] border-slate-400 p-2 last-of-type:border-r-0'>
{date}
</td>
{columns.includes('Team') && (
<td className='border-r-[1px] border-slate-400 p-2 last-of-type:border-r-0'>
{teamName}
</td>
)}
{columns.includes('Type') && (
<td className='border-r-[1px] border-slate-400 p-2 last-of-type:border-r-0'>
{meetingType}
</td>
)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
37 changes: 37 additions & 0 deletions packages/client/components/SpecificMeetingPickerRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {NodeViewProps} from '@tiptap/core'
import {Suspense} from 'react'
import type {SpecificMeetingPickerQuery} from '../__generated__/SpecificMeetingPickerQuery.graphql'
import query from '../__generated__/TeamPickerComboboxQuery.graphql'
import useQueryLoaderNow from '../hooks/useQueryLoaderNow'
import type {InsightsBlockAttrs} from '../tiptap/extensions/imageBlock/InsightsBlock'
import {Loader} from '../utils/relay/renderLoader'
import {SpecificMeetingPicker} from './SpecificMeetingPicker'

interface Props {
updateAttributes: NodeViewProps['updateAttributes']
attrs: InsightsBlockAttrs
}

export const SpecificMeetingPickerRoot = (props: Props) => {
const {attrs, updateAttributes} = props
const {teamIds, meetingTypes, after, before} = attrs
const queryRef = useQueryLoaderNow<SpecificMeetingPickerQuery>(query, {
teamIds,
meetingTypes,
after,
before,
first: 500
})

return (
<Suspense fallback={<Loader />}>
{queryRef && (
<SpecificMeetingPicker
queryRef={queryRef}
attrs={attrs}
updateAttributes={updateAttributes}
/>
)}
</Suspense>
)
}
16 changes: 8 additions & 8 deletions packages/client/tiptap/extensions/imageBlock/InsightsBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {InsightsBlockView} from '../imageUpload/InsightsBlockView'
export interface InsightsBlockAttrs {
teamIds: string[]
meetingTypes: MeetingTypeEnum[]
startAt: string
endAt: string
after: string
before: string
meetingIds: string[]
title: string
}
Expand All @@ -30,18 +30,18 @@ export const InsightsBlock = InsightsBlockBase.extend({
'data-meeting-types': attributes.meetingTypes
})
},
startAt: {
after: {
default: () => new Date(Date.now() - ms('12w')).toISOString(),
parseHTML: (element) => new Date(element.getAttribute('data-start-at') as string),
parseHTML: (element) => new Date(element.getAttribute('data-after') as string),
renderHTML: (attributes: InsightsBlockAttrs) => ({
'data-start-at': attributes.startAt
'data-after': attributes.after
})
},
endAt: {
before: {
default: () => new Date().toISOString(),
parseHTML: (element) => new Date(element.getAttribute('data-end-at') as string),
parseHTML: (element) => new Date(element.getAttribute('data-before') as string),
renderHTML: (attributes: InsightsBlockAttrs) => ({
'data-end-at': attributes.endAt
'data-before': attributes.before
})
},
meetingIds: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {NodeViewWrapper, type NodeViewProps} from '@tiptap/react'
import {MeetingDatePicker} from '../../../components/MeetingDatePicker'
import {MeetingTypePickerCombobox} from '../../../components/MeetingTypePickerCombobox'
import {SpecificMeetingPickerRoot} from '../../../components/SpecificMeetingPickerRoot'
import {TeamPickerComboboxRoot} from '../../../components/TeamPickerComboboxRoot'
import {Button} from '../../../ui/Button/Button'
import type {InsightsBlockAttrs} from '../imageBlock/InsightsBlock'
export const InsightsBlockView = (props: NodeViewProps) => {
const {node, updateAttributes} = props
const attrs = node.attrs as InsightsBlockAttrs
const {title} = attrs
const {title, after, before, meetingTypes, teamIds} = attrs
const canQueryMeetings = teamIds.length > 0 && meetingTypes.length > 0 && after && before
return (
<NodeViewWrapper>
<div className='m-0 p-0 text-slate-900'>
<div className='flex max-w-fit cursor-pointer flex-col rounded-sm bg-slate-200 p-4'>
<div className='flex max-w-fit flex-col rounded-sm bg-slate-200 p-4'>
<input
className='bg-inherit p-4 text-lg ring-0 outline-0'
onChange={(e) => {
Expand All @@ -30,11 +32,14 @@ export const InsightsBlockView = (props: NodeViewProps) => {
<label className='self-center font-semibold'>Meetings started between</label>
<MeetingDatePicker updateAttributes={updateAttributes} attrs={attrs} />
<div></div>
<div className='flex justify-end'>
<Button variant='secondary' shape='pill' size='md'>
Generate Insights
</Button>
</div>
</div>
{canQueryMeetings && (
<SpecificMeetingPickerRoot updateAttributes={updateAttributes} attrs={attrs} />
)}
<div className='flex justify-end p-4'>
<Button variant='secondary' shape='pill' size='md'>
Generate Insights
</Button>
</div>
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions packages/server/graphql/public/typeDefs/MeetingConnection.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
A connection to list of meetings
"""
type MeetingConnection {
"""
Page info with cursors as strings
"""
pageInfo: PageInfoDateCursor!

"""
A list of edges.
"""
edges: [MeetingEdge!]!
}
10 changes: 10 additions & 0 deletions packages/server/graphql/public/typeDefs/MeetingEdge.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
An edge in a connection.
"""
type MeetingEdge {
"""
The item at the end of the edge
"""
node: NewMeeting!
cursor: DateTime!
}
21 changes: 21 additions & 0 deletions packages/server/graphql/public/typeDefs/User.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,25 @@ type User {
The number of free custom poker templates remaining
"""
freeCustomPokerTemplatesRemaining: Int!
"""
AI-provided Insights
"""
pageInsights(meetingIds: [ID!]!, prompt: String): String!

meetings(
"""
The max number of meetings to return
"""
first: Int!
teamIds: [ID!]!
meetingTypes: [MeetingTypeEnum!]!
"""
The createdAt DateTime used as a cursor
"""
after: DateTime
"""
The createdAt DateTime used as an end cursor
"""
before: DateTime!
): MeetingConnection!
}
Loading

0 comments on commit 6f4e9b0

Please sign in to comment.