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

fix: tasks in poker scoping #10563

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/client/components/ParabolScopingSearchResultItem.tsx
Original file line number Diff line number Diff line change
@@ -68,10 +68,14 @@ const ParabolScopingSearchResultItem = (props: Props) => {
const disabled = !isSelected && usedServiceTaskIds.size >= Threshold.MAX_POKER_STORIES
const atmosphere = useAtmosphere()
const {onCompleted, onError, submitMutation, submitting} = useMutationProps()
const {editor, linkState, setLinkState} = useTipTapTaskEditor(content, {atmosphere, teamId})
const isEditingThisItem = !plaintextContent
const {editor, linkState, setLinkState} = useTipTapTaskEditor(content, {
atmosphere,
teamId,
readOnly: !isEditingThisItem
})
const {useTaskChild, addTaskChild, removeTaskChild, isTaskFocused} =
useTaskChildFocus(serviceTaskId)
const isEditingThisItem = !plaintextContent

const updatePokerScope = () => {
if (submitting || disabled) return
@@ -96,7 +100,8 @@ const ParabolScopingSearchResultItem = (props: Props) => {
const handleTaskUpdate = () => {
if (!editor) return
const isFocused = isTaskFocused()
if (editor.isEmpty && !isFocused) {
if (isFocused) return
if (editor.isEmpty) {
DeleteTaskMutation(atmosphere, {taskId: serviceTaskId})
return
}
8 changes: 2 additions & 6 deletions packages/client/components/ParabolScopingSearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import styled from '@emotion/styled'
import graphql from 'babel-plugin-relay/macro'
import {useEffect, useState} from 'react'
import {PreloadedQuery, useFragment, usePaginationFragment, usePreloadedQuery} from 'react-relay'
@@ -17,9 +16,6 @@ import NewIntegrationRecordButton from './NewIntegrationRecordButton'
import ParabolScopingSearchResultItem from './ParabolScopingSearchResultItem'
import ParabolScopingSelectAllTasks from './ParabolScopingSelectAllTasks'

const ResultScroller = styled('div')({
overflow: 'auto'
})
interface Props {
queryRef: PreloadedQuery<ParabolScopingSearchResultsQuery>
meetingRef: ParabolScopingSearchResults_meeting$key
@@ -145,7 +141,7 @@ const ParabolScopingSearchResults = (props: Props) => {
tasks={edges}
meetingId={meetingId}
/>
<ResultScroller>
<div className='overflow-auto'>
{edges.map(({node}) => {
return (
<ParabolScopingSearchResultItem
@@ -159,7 +155,7 @@ const ParabolScopingSearchResults = (props: Props) => {
)
})}
{lastItem}
</ResultScroller>
</div>
{!isEditing && (
<NewIntegrationRecordButton labelText={'New Task'} onClick={handleAddTaskClick} />
)}
2 changes: 1 addition & 1 deletion packages/client/components/promptResponse/TipTapEditor.tsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
export const TipTapEditor = (props: Props) => {
const {editor, linkState, setLinkState, showBubbleMenu, useLinkEditor} = props
return (
<div className=' cursor-text px-4 text-sm leading-5'>
<div className='px-4 text-sm leading-5'>
{showBubbleMenu && setLinkState && (
<StandardBubbleMenu editor={editor} setLinkState={setLinkState} />
)}
20 changes: 15 additions & 5 deletions packages/client/components/promptResponse/TipTapLinkMenu.tsx
Original file line number Diff line number Diff line change
@@ -43,10 +43,11 @@ export const TipTapLinkMenu = (props: Props) => {
return {link: '', text}
}
})

const handleEdit = useCallback(() => {
const oldLinkStateRef = useRef<LinkMenuState>(null)
const handleEdit = () => {
setLinkState('edit')
}, [])
oldLinkStateRef.current = 'preview'
}

const onSetLink = useCallback(
({text, url}: {text: string; url: string}) => {
@@ -104,7 +105,16 @@ export const TipTapLinkMenu = (props: Props) => {
setLinkState(null)
}, [editor])
const onOpenChange = (willOpen: boolean) => {
setLinkState(willOpen ? (editor.isActive('link') ? 'preview' : 'edit') : null)
const isLinkActive = editor.isActive('link')
if (willOpen) {
setLinkState(isLinkActive ? 'preview' : 'edit')
} else {
// special case when switching from preview to edit radix-ui triggers onOpenChange(false)
if (!(oldLinkStateRef.current === 'preview' && linkState === 'edit')) {
setLinkState(null)
}
oldLinkStateRef.current = null
}
}
const transformRef = useRef<undefined | string>(undefined)
const getTransform = () => {
@@ -122,7 +132,7 @@ export const TipTapLinkMenu = (props: Props) => {
<Popover.Portal>
<Popover.Content
onOpenAutoFocus={(e) => {
// necessary for link preview to preview focusing the first button
// necessary for link preview to prevent focusing the first button
e.preventDefault()
}}
>
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import {LinkMenuState} from '../../../../components/promptResponse/TipTapLinkMen
import TaskIntegrationLink from '../../../../components/TaskIntegrationLink'
import TaskWatermark from '../../../../components/TaskWatermark'
import useAtmosphere from '../../../../hooks/useAtmosphere'
import useTaskChildFocus, {UseTaskChild} from '../../../../hooks/useTaskChildFocus'
import {UseTaskChild} from '../../../../hooks/useTaskChildFocus'
import UpdateTaskMutation from '../../../../mutations/UpdateTaskMutation'
import {cardFocusShadow, cardHoverShadow, cardShadow, Elevation} from '../../../../styles/elevation'
import cardRootStyles from '../../../../styles/helpers/cardRootStyles'
@@ -55,8 +55,6 @@ const StatusIndicatorBlock = styled('div')({
display: 'flex'
})

const TaskEditorWrapper = styled('div')()

interface Props {
area: AreaEnum
isTaskFocused: boolean
@@ -70,10 +68,14 @@ interface Props {
task: OutcomeCard_task$key
useTaskChild: UseTaskChild
dataCy: string
addTaskChild(name: string): void
removeTaskChild(name: string): void
}

const OutcomeCard = memo((props: Props) => {
const {
addTaskChild,
removeTaskChild,
area,
isTaskFocused,
isTaskHovered,
@@ -140,7 +142,6 @@ const OutcomeCard = memo((props: Props) => {
const {viewerId} = atmosphere
const otherEditors = editors.filter((editor) => editor.userId !== viewerId)
const isEditing = editors.length > otherEditors.length
const {addTaskChild, removeTaskChild} = useTaskChildFocus(taskId)
const type = integration?.__typename
const statusTitle = `Card status: ${taskStatusLabels[status]}`
const privateTitle = ', marked as #private'
@@ -171,7 +172,8 @@ const OutcomeCard = memo((props: Props) => {
</EditingStatus>
<IntegratedTaskContent task={task} />
{!type && (
<TaskEditorWrapper
<div
className='cursor-text'
onBlur={() => {
removeTaskChild('root')
setTimeout(handleCardUpdate)
@@ -182,9 +184,11 @@ const OutcomeCard = memo((props: Props) => {
editor={editor}
linkState={linkState}
setLinkState={setLinkState}
useLinkEditor={() => useTaskChild('editor-link-changer')}
useLinkEditor={() => {
useTaskChild('editor-link-changer')
}}
/>
</TaskEditorWrapper>
</div>
)}
<TaskIntegrationLink dataCy={`${dataCy}`} integration={integration || null} />
<TaskFooter
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from '@emotion/styled'
import {Editor} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
import areEqual from 'fbjs/lib/areEqual'
import {memo, useEffect, useRef, useState} from 'react'
import {useFragment} from 'react-relay'
import {OutcomeCardContainer_task$key} from '~/__generated__/OutcomeCardContainer_task.graphql'
@@ -66,7 +67,7 @@ const OutcomeCardContainer = memo((props: Props) => {
const ref = useRef<HTMLDivElement>(null)
const [isTaskHovered, setIsTaskHovered] = useState(false)

const {useTaskChild, isTaskFocused} = useTaskChildFocus(taskId)
const {useTaskChild, isTaskFocused, addTaskChild, removeTaskChild} = useTaskChildFocus(taskId)

const isHighlighted = isTaskHovered || !!isDraggingOver
useEffect(() => {
@@ -81,12 +82,13 @@ const OutcomeCardContainer = memo((props: Props) => {

const handleCardUpdate = () => {
const isFocused = isTaskFocused()
if (isFocused) return
if (editor.isEmpty && !isFocused) {
DeleteTaskMutation(atmosphere, {taskId})
return
}
const nextContent = JSON.stringify(editor.getJSON())
if (content === nextContent) return
if (areEqual(JSON.parse(content), editor.getJSON())) return
const updatedTask = {
id: taskId,
content: nextContent
@@ -118,6 +120,8 @@ const OutcomeCardContainer = memo((props: Props) => {
isDraggingOver={isDraggingOver}
task={task}
useTaskChild={useTaskChild}
addTaskChild={addTaskChild}
removeTaskChild={removeTaskChild}
/>
</Wrapper>
)
12 changes: 7 additions & 5 deletions packages/client/mutations/handlers/handleUpsertTasks.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import isTaskPrivate from '~/utils/isTaskPrivate'
import {parseQueryParams} from '~/utils/useQueryParameterParser'
import addNodeToArray from '../../utils/relay/addNodeToArray'
import safeRemoveNodeFromConn from '../../utils/relay/safeRemoveNodeFromConn'
import safeRemoveNodeFromUnknownConn from '../../utils/relay/safeRemoveNodeFromUnknownConn'
import getArchivedTasksConn from '../connections/getArchivedTasksConn'
import getScopingTasksConn from '../connections/getScopingTasksConn'
import getTeamTasksConn from '../connections/getTeamTasksConn'
@@ -53,12 +54,18 @@ const handleUpsertTask = (task: Task | null, store: RecordSourceSelectorProxy<an
if (isNowArchived) {
safeRemoveNodeFromConn(taskId, teamConn)
safeRemoveNodeFromConn(taskId, userConn)
safeRemoveNodeFromUnknownConn(store, viewerId, 'ParabolScopingSearchResults_tasks', taskId)
archiveConns.forEach((archiveConn) => safePutNodeInConn(archiveConn, task, store))
} else {
archiveConns.forEach((archiveConn) => safeRemoveNodeFromConn(taskId, archiveConn))
safePutNodeInConn(teamConn, task, store)
safePutNodeInConn(threadConn, task, store, 'threadSortOrder', true)
addNodeToArray(task, meeting, 'tasks', 'createdAt')
/* updates parabol search query if task is created from a sprint poker meeting
* should also implement updating parabol search query if task is created elsewhere?
*/
const scopingTasksConn = getScopingTasksConn(store, meetingId, viewer, [teamId])
safePutNodeInConn(scopingTasksConn, task, store, 'updatedAt', false)
if (userConn) {
const isPrivate = isTaskPrivate(tags)
const ownedByViewer = task.getValue('userId') === viewerId
@@ -69,11 +76,6 @@ const handleUpsertTask = (task: Task | null, store: RecordSourceSelectorProxy<an
}
}
}
/* updates parabol search query if task is created from a sprint poker meeting
* should also implement updating parabol search query if task is created elsewhere?
*/
const scopingTasksConn = getScopingTasksConn(store, meetingId, viewer, [teamId])
safePutNodeInConn(scopingTasksConn, task, store, 'updatedAt', false)
}

const handleUpsertTasks = pluralizeHandler(handleUpsertTask)