Skip to content
Draft
Show file tree
Hide file tree
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
10 changes: 6 additions & 4 deletions src/components/cards/CommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { fetcher } from '@/utils/fetcher'
import { getTimeDifference } from '@/utils/getTimeDifference'
import { isTapwriteContentEmpty } from '@/utils/isTapwriteContentEmpty'
import { checkOptimisticStableId, OptimisticUpdate } from '@/utils/optimisticCommentUtils'
import { canSubmitTapwriteContent, isTapwriteInteractionActive } from '@/utils/tapwriteEditorState'
import { ReplyResponse } from '@api/activity-logs/schemas/CommentAddedSchema'
import { LogResponse } from '@api/activity-logs/schemas/LogResponseSchema'
import { Box, Collapse, Stack, Typography } from '@mui/material'
Expand Down Expand Up @@ -134,6 +135,8 @@ export const CommentCard = ({
setEditedContent(content)
}
const handleEdit = async () => {
if (!canSubmitTapwriteContent(isListOrMenuActive)) return

if (isTapwriteContentEmpty(editedContent)) {
setEditedContent(content)
setIsReadOnly(true)
Expand All @@ -153,11 +156,11 @@ export const CommentCard = ({
if (!isFocused || isMobile()) {
return
}
if (event.key === 'Enter' && !event.shiftKey && !isListOrMenuActive) {
if (event.key === 'Enter' && !event.shiftKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleEdit()
}
if (event.key === 'Enter' && event.ctrlKey) {
if (event.key === 'Enter' && event.ctrlKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleEdit() //Invoke submit if ctrl+enter is pressed at any time
}
Expand Down Expand Up @@ -321,8 +324,7 @@ export const CommentCard = ({
<Tapwrite
content={editedContent}
onActiveStatusChange={(prop) => {
const { isListActive, isFloatingMenuActive } = prop
setIsListOrMenuActive(isListActive || isFloatingMenuActive)
setIsListOrMenuActive(isTapwriteInteractionActive(prop))
}}
getContent={setEditedContent}
readonly={isReadOnly}
Expand Down
10 changes: 6 additions & 4 deletions src/components/cards/ReplyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { deleteEditorAttachmentsHandler, getAttachmentPayload, getCustomFilePath
import { createUploadFn } from '@/utils/createUploadFn'
import { getTimeDifference } from '@/utils/getTimeDifference'
import { isTapwriteContentEmpty } from '@/utils/isTapwriteContentEmpty'
import { canSubmitTapwriteContent, isTapwriteInteractionActive } from '@/utils/tapwriteEditorState'
import { Box, Stack } from '@mui/material'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
Expand Down Expand Up @@ -86,6 +87,8 @@ export const ReplyCard = ({
const { postAttachment } = usePostAttachment()

const handleEdit = async () => {
if (!canSubmitTapwriteContent(isListOrMenuActive)) return

if (isTapwriteContentEmpty(editedContent)) {
setEditedContent(content)
setIsReadOnly(true)
Expand All @@ -105,11 +108,11 @@ export const ReplyCard = ({
if (!isFocused || isMobile()) {
return
}
if (event.key === 'Enter' && !event.shiftKey && !isListOrMenuActive) {
if (event.key === 'Enter' && !event.shiftKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleEdit()
}
if (event.key === 'Enter' && event.ctrlKey) {
if (event.key === 'Enter' && event.ctrlKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleEdit() //Invoke submit if ctrl+enter is pressed at any time
}
Expand Down Expand Up @@ -236,8 +239,7 @@ export const ReplyCard = ({
<Tapwrite
content={editedContent}
onActiveStatusChange={(prop) => {
const { isListActive, isFloatingMenuActive } = prop
setIsListOrMenuActive(isListActive || isFloatingMenuActive)
setIsListOrMenuActive(isTapwriteInteractionActive(prop))
}}
getContent={setEditedContent}
readonly={isReadOnly}
Expand Down
10 changes: 6 additions & 4 deletions src/components/inputs/CommentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { deleteEditorAttachmentsHandler, uploadAttachmentHandler } from '@/utils
import { createUploadFn } from '@/utils/createUploadFn'
import { getMentionsList } from '@/utils/getMentionList'
import { isTapwriteContentEmpty } from '@/utils/isTapwriteContentEmpty'
import { canSubmitTapwriteContent, isTapwriteInteractionActive } from '@/utils/tapwriteEditorState'
import { Stack } from '@mui/material'
import { useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
Expand Down Expand Up @@ -41,6 +42,8 @@ export const CommentInput = ({ createComment, task_id, token }: Prop) => {
}

const handleSubmit = () => {
if (!canSubmitTapwriteContent(isListOrMenuActive)) return

let content = detail
const END_P = '<p></p>'
const endChunk = content.slice(-7)
Expand All @@ -65,11 +68,11 @@ export const CommentInput = ({ createComment, task_id, token }: Prop) => {
if (!isFocused || isMobile()) {
return
}
if (event.key === 'Enter' && !event.shiftKey && !isListOrMenuActive) {
if (event.key === 'Enter' && !event.shiftKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault() // Prevent new line in the editor
handleSubmit()
}
if (event.key === 'Enter' && event.ctrlKey) {
if (event.key === 'Enter' && event.ctrlKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleSubmit() //Invoke submit if ctrl+enter is pressed at any time
}
Expand Down Expand Up @@ -156,8 +159,7 @@ export const CommentInput = ({ createComment, task_id, token }: Prop) => {
editorClass="tapwrite-comment-input"
hardbreak
onActiveStatusChange={(prop) => {
const { isListActive, isFloatingMenuActive } = prop
setIsListOrMenuActive(isListActive || isFloatingMenuActive)
setIsListOrMenuActive(isTapwriteInteractionActive(prop))
}}
parentContainerStyle={{
width: '100%',
Expand Down
12 changes: 7 additions & 5 deletions src/components/inputs/ReplyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CreateComment } from '@/types/dto/comment.dto'
import { deleteEditorAttachmentsHandler } from '@/utils/attachmentUtils'
import { isPopoverInputFocused } from '@/utils/isPopoverInputFocused'
import { isTapwriteContentEmpty } from '@/utils/isTapwriteContentEmpty'
import { canSubmitTapwriteContent, isTapwriteInteractionActive } from '@/utils/tapwriteEditorState'
import { Box, Stack } from '@mui/material'
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
Expand Down Expand Up @@ -47,6 +48,8 @@ export const ReplyInput = ({
const [pendingReplies, setPendingReplies] = useState<{ content: string; taskId: string }[]>([])

const handleReplySubmission = useCallback(() => {
if (!canSubmitTapwriteContent(isListOrMenuActive)) return

let content = detail
const END_P = '<p></p>'
if (content.slice(-7) === END_P) {
Expand All @@ -57,7 +60,7 @@ export const ReplyInput = ({
setDetail('')
setPendingReplies((prev) => [...prev, { content, taskId: task_id }])
}
}, [comment, detail, task_id])
}, [detail, isListOrMenuActive, task_id])

useEffect(() => {
if (pendingReplies.length > 0 && !comment.details.id.includes('temp-comment')) {
Expand All @@ -76,11 +79,11 @@ export const ReplyInput = ({
if (!focusReplyInput || isMobile()) {
return
}
if (event.key === 'Enter' && !event.shiftKey && !isListOrMenuActive) {
if (event.key === 'Enter' && !event.shiftKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleReplySubmission()
}
if (event.key === 'Enter' && event.ctrlKey) {
if (event.key === 'Enter' && event.ctrlKey && canSubmitTapwriteContent(isListOrMenuActive)) {
event.preventDefault()
handleReplySubmission()
}
Expand Down Expand Up @@ -181,8 +184,7 @@ export const ReplyInput = ({
className={'tapwrite-reply-input'}
hardbreak
onActiveStatusChange={(prop) => {
const { isListActive, isFloatingMenuActive } = prop
setIsListOrMenuActive(isListActive || isFloatingMenuActive)
setIsListOrMenuActive(isTapwriteInteractionActive(prop))
}}
attachmentLayout={(props) => (
<AttachmentLayout {...props} isComment={true} onUploadStatusChange={handleUploadStatusChange} />
Expand Down
7 changes: 5 additions & 2 deletions src/components/inputs/tiptap/useTitleEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
tapwriteDynamicFields,
} from '@/components/inputs/TapwriteDynamicFieldDropdown'
import { getWorstCaseResolvedLength } from '@/utils/dynamicFields'
import { clampProseMirrorPosition } from '@/utils/tapwriteEditorState'
import Document from '@tiptap/extension-document'
import History from '@tiptap/extension-history'
import Paragraph from '@tiptap/extension-paragraph'
Expand Down Expand Up @@ -132,8 +133,10 @@ export function useTitleEditor({ value, onChange, placeholder = '', autoFocus, o
const { schema } = view.state
const node = schema.nodes.autofillField.create({ value: fieldKey })
const space = schema.text(' ')
const tr = view.state.tr.insert(pos.pos, Fragment.from([node, space]))
tr.setSelection(TextSelection.create(tr.doc, pos.pos + node.nodeSize + space.nodeSize))
const insertPos = clampProseMirrorPosition(pos.pos, view.state.doc.content.size)
const tr = view.state.tr.insert(insertPos, Fragment.from([node, space]))
const selectionPos = clampProseMirrorPosition(insertPos + node.nodeSize + space.nodeSize, tr.doc.content.size)
tr.setSelection(TextSelection.create(tr.doc, selectionPos))
view.dispatch(tr)
view.focus()
// Firefox hides the caret after drag-and-drop; a blur/focus cycle forces it to repaint.
Expand Down
66 changes: 66 additions & 0 deletions src/utils/tapwriteEditorState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
canSubmitTapwriteContent,
clampProseMirrorPosition,
hasTapwritePopover,
isTapwriteInteractionActive,
} from '@/utils/tapwriteEditorState'

const originalDocument = globalThis.document

function mockDocumentQuerySelector(result: Element | null) {
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
querySelector: jest.fn(() => result),
},
})
}

function restoreDocument() {
if (originalDocument) {
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: originalDocument,
})
return
}

delete (globalThis as { document?: Document }).document
}

describe('tapwriteEditorState', () => {
afterEach(() => {
restoreDocument()
})

it('detects active Tapwrite interactions from status flags', () => {
expect(isTapwriteInteractionActive({ isListActive: true })).toBe(true)
expect(isTapwriteInteractionActive({ isFloatingMenuActive: true })).toBe(true)
expect(isTapwriteInteractionActive({ isListActive: false, isFloatingMenuActive: false })).toBe(false)
})

it('treats Tippy popovers as active Tapwrite interactions', () => {
mockDocumentQuerySelector({} as Element)

expect(hasTapwritePopover()).toBe(true)
expect(isTapwriteInteractionActive({ isListActive: false, isFloatingMenuActive: false })).toBe(true)
})

it('blocks content submission while Tapwrite interactions are active', () => {
expect(canSubmitTapwriteContent(true)).toBe(false)

mockDocumentQuerySelector({} as Element)

expect(canSubmitTapwriteContent(false)).toBe(false)
})

it('allows content submission when no Tapwrite interaction is active', () => {
expect(canSubmitTapwriteContent(false)).toBe(true)
})

it('clamps ProseMirror positions to the current document bounds', () => {
expect(clampProseMirrorPosition(-7, 12)).toBe(0)
expect(clampProseMirrorPosition(7, 12)).toBe(7)
expect(clampProseMirrorPosition(20, 12)).toBe(12)
})
})
22 changes: 22 additions & 0 deletions src/utils/tapwriteEditorState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interface TapwriteActiveStatus {
isListActive?: boolean
isFloatingMenuActive?: boolean
}

export function hasTapwritePopover() {
if (typeof document === 'undefined') return false

return document.querySelector('.tippy-box') !== null
}

export function isTapwriteInteractionActive(status: TapwriteActiveStatus) {
return Boolean(status.isListActive || status.isFloatingMenuActive || hasTapwritePopover())
}

export function canSubmitTapwriteContent(isListOrMenuActive: boolean) {
return !isListOrMenuActive && !hasTapwritePopover()
}

export function clampProseMirrorPosition(position: number, docSize: number) {
return Math.max(0, Math.min(position, docSize))
}
Loading