diff --git a/packages/client/components/MeetingOptions.tsx b/packages/client/components/MeetingOptions.tsx new file mode 100644 index 00000000000..d0fd3d97a7c --- /dev/null +++ b/packages/client/components/MeetingOptions.tsx @@ -0,0 +1,62 @@ +import React, {useState} from 'react' +import IconLabel from './IconLabel' +import {Menu} from '../ui/Menu/Menu' +import {MenuItem} from '../ui/Menu/MenuItem' +import SwapHorizIcon from '@mui/icons-material/SwapHoriz' +import {OptionsButton} from './TeamPrompt/TeamPromptOptions' +import {Tooltip} from '../ui/Tooltip/Tooltip' +import {TooltipTrigger} from '../ui/Tooltip/TooltipTrigger' +import {TooltipContent} from '../ui/Tooltip/TooltipContent' + +type Props = { + setShowDrawer: (showDrawer: boolean) => void + showDrawer: boolean + hasReflections: boolean +} + +const MeetingOptions = (props: Props) => { + const {setShowDrawer, showDrawer, hasReflections} = props + const [isOpen, setIsOpen] = useState(false) + + const handleClick = () => { + if (hasReflections) return + setShowDrawer(!showDrawer) + } + + const handleMouseEnter = () => { + if (hasReflections) { + setIsOpen(true) + } + } + + const handleMouseLeave = () => { + setIsOpen(false) + } + + return ( + + +
Options
+ + } + > + +
+ + +
{}
+ Change template +
+
+
+ + {'You can only change the template if no reflections have been added.'} + +
+
+ ) +} + +export default MeetingOptions diff --git a/packages/client/components/MeetingTopBar.tsx b/packages/client/components/MeetingTopBar.tsx index a9d37c7e096..287a53012b4 100644 --- a/packages/client/components/MeetingTopBar.tsx +++ b/packages/client/components/MeetingTopBar.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled' import {Comment} from '@mui/icons-material' -import React, {ReactElement, ReactNode} from 'react' +import React, {ReactElement, ReactNode, useState} from 'react' import {PALETTE} from '~/styles/paletteV3' import {meetingAvatarMediaQueries} from '../styles/meeting' import hasToken from '../utils/hasToken' @@ -9,6 +9,7 @@ import makeMinWidthMediaQuery from '../utils/makeMinWidthMediaQuery' import DemoCreateAccountButton from './DemoCreateAccountButton' import PlainButton from './PlainButton/PlainButton' import SidebarToggle from './SidebarToggle' +import RetroDrawerRoot from './RetroDrawerRoot' const localHeaderBreakpoint = makeMinWidthMediaQuery(600) @@ -148,6 +149,7 @@ interface Props { isRightDrawerOpen?: boolean toggleSidebar: () => void toggleDrawer?: () => void + meetingId?: string } const MeetingTopBar = (props: Props) => { @@ -158,10 +160,14 @@ const MeetingTopBar = (props: Props) => { isMeetingSidebarCollapsed, isRightDrawerOpen, toggleDrawer, - toggleSidebar + toggleSidebar, + meetingId } = props const showButton = isDemoRoute() && !hasToken() const showDiscussionButton = toggleDrawer && !isRightDrawerOpen + const [showDrawer, setShowDrawer] = useState(false) + const isOptionsVisible = !!meetingId && !isDemoRoute() + return ( @@ -177,6 +183,13 @@ const MeetingTopBar = (props: Props) => { )} {avatarGroup} + {isOptionsVisible && ( + + )} {showDiscussionButton && toggleDrawer && ( diff --git a/packages/client/components/RetroDrawer.tsx b/packages/client/components/RetroDrawer.tsx new file mode 100644 index 00000000000..8d6404ada0e --- /dev/null +++ b/packages/client/components/RetroDrawer.tsx @@ -0,0 +1,108 @@ +import {Close} from '@mui/icons-material' +import graphql from 'babel-plugin-relay/macro' +import React, {useEffect} from 'react' +import {PreloadedQuery, usePreloadedQuery} from 'react-relay' +import {Breakpoint, DiscussionThreadEnum} from '../types/constEnums' +import ResponsiveDashSidebar from './ResponsiveDashSidebar' +import MeetingOptions from './MeetingOptions' +import RetroDrawerTemplateCard from './RetroDrawerTemplateCard' +import {Drawer} from './TeamPrompt/TeamPromptDrawer' +import {RetroDrawerQuery} from '../__generated__/RetroDrawerQuery.graphql' +import useBreakpoint from '../hooks/useBreakpoint' + +interface Props { + setShowDrawer: (showDrawer: boolean) => void + showDrawer: boolean + queryRef: PreloadedQuery +} + +const RetroDrawer = (props: Props) => { + const {queryRef, showDrawer, setShowDrawer} = props + const {viewer} = usePreloadedQuery( + graphql` + query RetroDrawerQuery($first: Int!, $type: MeetingTypeEnum!, $meetingId: ID!) { + viewer { + meeting(meetingId: $meetingId) { + ... on RetrospectiveMeeting { + reflectionGroups { + id + } + localPhase { + ... on ReflectPhase { + phaseType + } + } + } + } + availableTemplates(first: $first, type: $type) + @connection(key: "RetroDrawer_availableTemplates") { + edges { + node { + ...RetroDrawerTemplateCard_template + id + } + } + } + } + } + `, + queryRef + ) + + const templates = viewer.availableTemplates.edges + const meeting = viewer.meeting + const hasReflections = !!(meeting?.reflectionGroups && meeting.reflectionGroups.length > 0) + const phaseType = meeting?.localPhase?.phaseType + const isMobile = !useBreakpoint(Breakpoint.FUZZY_TABLET) + const isDesktop = useBreakpoint(Breakpoint.SIDEBAR_LEFT) + + const toggleDrawer = () => { + setShowDrawer(!showDrawer) + } + + useEffect(() => { + if (hasReflections && showDrawer) { + setShowDrawer(false) + } + }, [hasReflections]) + + if (!phaseType || phaseType !== 'reflect') return null + return ( + <> + + + +
+
+
Templates
+
+ +
+
+ {templates.map((template) => ( + + ))} +
+
+
+ + ) +} +export default RetroDrawer diff --git a/packages/client/components/RetroDrawerRoot.tsx b/packages/client/components/RetroDrawerRoot.tsx new file mode 100644 index 00000000000..58bf63544ff --- /dev/null +++ b/packages/client/components/RetroDrawerRoot.tsx @@ -0,0 +1,28 @@ +import React, {Suspense} from 'react' +import useQueryLoaderNow from '../hooks/useQueryLoaderNow' +import retroDrawerQuery, {RetroDrawerQuery} from '../__generated__/RetroDrawerQuery.graphql' +import RetroDrawer from './RetroDrawer' + +type Props = { + showDrawer: boolean + setShowDrawer: (showDrawer: boolean) => void + meetingId: string +} + +const RetroDrawerRoot = (props: Props) => { + const {showDrawer, setShowDrawer, meetingId} = props + const queryRef = useQueryLoaderNow(retroDrawerQuery, { + first: 200, + type: 'retrospective', + meetingId + }) + return ( + + {queryRef && ( + + )} + + ) +} + +export default RetroDrawerRoot diff --git a/packages/client/components/RetroDrawerTemplateCard.tsx b/packages/client/components/RetroDrawerTemplateCard.tsx new file mode 100644 index 00000000000..c126f6d0d6c --- /dev/null +++ b/packages/client/components/RetroDrawerTemplateCard.tsx @@ -0,0 +1,55 @@ +import {ActivityBadge} from './ActivityLibrary/ActivityBadge' +import {ActivityLibraryCardDescription} from './ActivityLibrary/ActivityLibraryCardDescription' +import graphql from 'babel-plugin-relay/macro' +import React from 'react' +import {useFragment} from 'react-relay' +import {ActivityLibraryCard} from './ActivityLibrary/ActivityLibraryCard' +import {ActivityCardImage} from './ActivityLibrary/ActivityCard' +import {RetroDrawerTemplateCard_template$key} from '~/__generated__/RetroDrawerTemplateCard_template.graphql' +import {CategoryID, CATEGORY_THEMES} from '././ActivityLibrary/Categories' + +interface Props { + templateRef: RetroDrawerTemplateCard_template$key +} + +const RetroDrawerTemplateCard = (props: Props) => { + const {templateRef} = props + const template = useFragment( + graphql` + fragment RetroDrawerTemplateCard_template on MeetingTemplate { + ...ActivityLibraryCardDescription_template + name + category + illustrationUrl + isFree + } + `, + templateRef + ) + + return ( +
+ Premium + ) : null + } + > + + + +
+ ) +} +export default RetroDrawerTemplateCard diff --git a/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx b/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx index 74a5f422cb3..493ae73247c 100644 --- a/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx +++ b/packages/client/components/RetroReflectPhase/RetroReflectPhase.tsx @@ -30,6 +30,7 @@ const RetroReflectPhase = (props: Props) => { ...StageTimerDisplay_meeting ...StageTimerControl_meeting ...PhaseItemColumn_meeting + id endedAt localPhase { ...RetroReflectPhase_phase @relay(mask: false) @@ -59,6 +60,7 @@ const RetroReflectPhase = (props: Props) => { ( +export const Drawer = styled('div')<{isDesktop: boolean; isMobile: boolean; isOpen: boolean}>( ({isDesktop, isMobile, isOpen}) => ({ boxShadow: isDesktop ? desktopSidebarShadow : undefined, backgroundColor: '#FFFFFF', diff --git a/packages/client/components/TeamPrompt/TeamPromptOptions.tsx b/packages/client/components/TeamPrompt/TeamPromptOptions.tsx index f34a7530830..bef32e09be0 100644 --- a/packages/client/components/TeamPrompt/TeamPromptOptions.tsx +++ b/packages/client/components/TeamPrompt/TeamPromptOptions.tsx @@ -14,7 +14,7 @@ import TeamPromptOptionsMenu from './TeamPromptOptionsMenu' const COPIED_TOOLTIP_DURATION_MS = 2000 -const OptionsButton = styled(BaseButton)({ +export const OptionsButton = styled(BaseButton)({ color: PALETTE.SKY_500, display: 'flex', flexDirection: 'column', diff --git a/packages/client/ui/Menu/Menu.tsx b/packages/client/ui/Menu/Menu.tsx new file mode 100644 index 00000000000..47afbbbf35b --- /dev/null +++ b/packages/client/ui/Menu/Menu.tsx @@ -0,0 +1,32 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import React from 'react' +import {twMerge} from 'tailwind-merge' + +interface MenuProps { + trigger: React.ReactNode + className?: string + children: React.ReactNode +} + +export const Menu = React.forwardRef( + ({trigger, className, children}, ref) => { + return ( + + {trigger} + + + {children} + + + + ) + } +) diff --git a/packages/client/ui/Menu/MenuItem.tsx b/packages/client/ui/Menu/MenuItem.tsx new file mode 100644 index 00000000000..fe117676eb0 --- /dev/null +++ b/packages/client/ui/Menu/MenuItem.tsx @@ -0,0 +1,28 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import React from 'react' +import {twMerge} from 'tailwind-merge' + +interface MenuItemProps { + onClick: (event: Event) => void + isDisabled?: boolean + className?: string + children: React.ReactNode +} + +export const MenuItem = React.forwardRef( + ({onClick, isDisabled, className, children}, ref) => { + return ( + + {children} + + ) + } +) diff --git a/packages/client/ui/Tooltip/TooltipContent.tsx b/packages/client/ui/Tooltip/TooltipContent.tsx index 03f77bea4ef..c119f4a2b70 100644 --- a/packages/client/ui/Tooltip/TooltipContent.tsx +++ b/packages/client/ui/Tooltip/TooltipContent.tsx @@ -1,7 +1,7 @@ import {Content, Portal} from '@radix-ui/react-tooltip' import * as React from 'react' import {twMerge} from 'tailwind-merge' -import {forwardRadix} from '../fordwardRadix' +import {forwardRadix} from '../forwardRadix' export const TooltipContent = forwardRadix( ({className, children, ...props}, ref) => ( diff --git a/packages/client/ui/Tooltip/TooltipTrigger.tsx b/packages/client/ui/Tooltip/TooltipTrigger.tsx index b364812034c..2a0b7b0ca36 100644 --- a/packages/client/ui/Tooltip/TooltipTrigger.tsx +++ b/packages/client/ui/Tooltip/TooltipTrigger.tsx @@ -1,6 +1,6 @@ import {Trigger} from '@radix-ui/react-tooltip' import * as React from 'react' -import {forwardRadix} from '../fordwardRadix' +import {forwardRadix} from '../forwardRadix' export const TooltipTrigger = forwardRadix( ({className, children, ...props}, ref) => ( diff --git a/packages/client/ui/fordwardRadix.tsx b/packages/client/ui/forwardRadix.tsx similarity index 100% rename from packages/client/ui/fordwardRadix.tsx rename to packages/client/ui/forwardRadix.tsx diff --git a/packages/server/graphql/public/typeDefs/User.graphql b/packages/server/graphql/public/typeDefs/User.graphql index 232c2027391..ce643e797e5 100644 --- a/packages/server/graphql/public/typeDefs/User.graphql +++ b/packages/server/graphql/public/typeDefs/User.graphql @@ -433,6 +433,10 @@ type User { The cursor, which is the templateId """ after: ID + """ + An optional argument to filter by template type + """ + type: MeetingTypeEnum ): MeetingTemplateConnection! """ diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index cd396dd8963..f46afe30511 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -114,7 +114,7 @@ const User: UserResolvers = { const invoice = await manager.retrieveInvoice(invoiceId) return generateInvoice(invoice, stripeLineItems, orgId, invoiceId, dataLoader) }, - availableTemplates: async ({id: userId}, {first, after}, {authToken, dataLoader}) => { + availableTemplates: async ({id: userId}, {first, after, type}, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const user = await dataLoader.get('users').loadNonNull(userId) const teamIds = @@ -175,6 +175,7 @@ const User: UserResolvers = { ...activity, sortOrder: getScore(activity, teamIds) })) + .filter((activity) => !type || activity.type === type) .sort((a, b) => (a.sortOrder > b.sortOrder ? -1 : 1)) return connectionFromTemplateArray(allActivities, first, after)