diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index 3ba4c2737..ece81af3c 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -2,17 +2,24 @@ import { useCallback, useEffect, useState } from 'react'; import { BlockNoteEditor } from '@blocknote/core'; import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; +import { isArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; import { useDebouncedCallback } from 'use-debounce'; +import { v4 } from 'uuid'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { Activity } from '@/activities/types/Activity'; +import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; @@ -53,6 +60,10 @@ export const ActivityBodyEditor = ({ const modifyActivityFromCache = useModifyRecordFromCache({ objectMetadataItem: objectMetadataItemActivity, }); + const { + goBackToPreviousHotkeyScope, + setHotkeyScopeAndMemorizePreviousScope, + } = usePreviousHotkeyScope(); const { upsertActivity } = useUpsertActivity(); @@ -212,9 +223,93 @@ export const ActivityBodyEditor = ({ } }; + useScopedHotkeys( + Key.Escape, + () => { + editor.domElement?.blur(); + }, + ActivityEditorHotkeyScope.ActivityBody, + ); + + useScopedHotkeys( + '*', + (keyboardEvent) => { + if (keyboardEvent.key === Key.Escape) { + return; + } + + const isWritingText = + !isNonTextWritingKey(keyboardEvent.key) && + !keyboardEvent.ctrlKey && + !keyboardEvent.metaKey; + + if (!isWritingText) { + return; + } + + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + keyboardEvent.stopImmediatePropagation(); + + const blockIdentifier = editor.getTextCursorPosition().block; + const currentBlockContent = blockIdentifier?.content; + + if ( + currentBlockContent && + isArray(currentBlockContent) && + currentBlockContent.length === 0 + ) { + // Empty block case + editor.updateBlock(blockIdentifier, { + content: keyboardEvent.key, + }); + return; + } + + if ( + currentBlockContent && + isArray(currentBlockContent) && + currentBlockContent[0] && + currentBlockContent[0].type === 'text' + ) { + // Text block case + editor.updateBlock(blockIdentifier, { + content: currentBlockContent[0].text + keyboardEvent.key, + }); + return; + } + + const newBlockId = v4(); + const newBlock = { + id: newBlockId, + type: 'paragraph', + content: keyboardEvent.key, + }; + editor.insertBlocks([newBlock], blockIdentifier, 'after'); + + editor.setTextCursorPosition(newBlockId, 'end'); + editor.focus(); + }, + RightDrawerHotkeyScope.RightDrawer, + ); + + const handleBlockEditorFocus = () => { + setHotkeyScopeAndMemorizePreviousScope( + ActivityEditorHotkeyScope.ActivityBody, + ); + }; + + const handlerBlockEditorBlur = () => { + goBackToPreviousHotkeyScope(); + }; + return ( - - + editor.focus()}> + ); }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx index 2c4e2070d..37a9197f2 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx @@ -28,7 +28,6 @@ const StyledThreadItemListContainer = styled.div` justify-content: flex-start; padding: ${({ theme }) => theme.spacing(8)}; - padding-bottom: ${({ theme }) => theme.spacing(32)}; padding-left: ${({ theme }) => theme.spacing(12)}; width: 100%; `; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx index d157773aa..86a7e489d 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx @@ -33,6 +33,7 @@ const StyledContainer = styled.div` height: 100%; justify-content: space-between; overflow-y: auto; + gap: ${({ theme }) => theme.spacing(4)}; `; const StyledUpperPartContainer = styled.div` @@ -41,7 +42,6 @@ const StyledUpperPartContainer = styled.div` display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(4)}; justify-content: flex-start; `; @@ -104,12 +104,18 @@ export const ActivityEditor = ({ customUseUpdateOneObjectHook: useUpsertOneActivityMutation, }); + const { FieldContextProvider: ActivityTargetsContextProvider } = + useFieldContext({ + objectNameSingular: CoreObjectNameSingular.Activity, + objectRecordId: activity?.id ?? '', + fieldMetadataName: 'activityTargets', + fieldPosition: 2, + }); + const [isCreatingActivity, setIsCreatingActivity] = useRecoilState( isCreatingActivityState, ); - // TODO: remove - useRegisterClickOutsideListenerCallback({ callbackId: 'activity-editor', callbackFunction: () => { @@ -143,14 +149,18 @@ export const ActivityEditor = ({ )} - + {ActivityTargetsContextProvider && ( + + + + )} - + {showComment && ( { const { upsertActivity } = useUpsertActivity(); + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + const titleInputRef = useRef(null); + + useScopedHotkeys( + Key.Escape, + () => { + handleBlur(); + }, + ActivityEditorHotkeyScope.ActivityTitle, + ); + + const handleBlur = () => { + goBackToPreviousHotkeyScope(); + titleInputRef.current?.blur(); + }; + + const handleFocus = () => { + setHotkeyScopeAndMemorizePreviousScope( + ActivityEditorHotkeyScope.ActivityTitle, + ); + }; + const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( activityTitleHasBeenSetFamilyState({ activityId: activity.id, @@ -120,10 +150,13 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => { handleTitleChange(event.target.value)} value={internalTitle} completed={completed} + onBlur={handleBlur} + onFocus={handleFocus} /> ); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx index e799b5cd8..0d0ba14b3 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx @@ -73,7 +73,6 @@ export const ActivityTargetInlineCellEditMode = ({ const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => { closeEditableField(); - const activityTargetRecordsToDelete = activityTargetObjectRecords.filter( (activityTargetObjectRecord) => !selectedRecords.some( diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index 78c0716c1..13a6b0236 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -1,13 +1,15 @@ +import { Key } from 'ts-key-enum'; + import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode'; import { Activity } from '@/activities/types/Activity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFieldContext } from '@/object-record/hooks/useFieldContext'; +import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { IconArrowUpRight, IconPencil } from '@/ui/display/icon'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; type ActivityTargetsInlineCellProps = { activity: Activity; @@ -19,40 +21,38 @@ export const ActivityTargetsInlineCell = ({ const { activityTargetObjectRecords } = useActivityTargetObjectRecords({ activityId: activity?.id ?? '', }); + const { closeInlineCell } = useInlineCell(); - const { FieldContextProvider } = useFieldContext({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activity?.id ?? '', - fieldMetadataName: 'activityTargets', - fieldPosition: 2, - }); - - if (!FieldContextProvider) return null; + useScopedHotkeys( + Key.Escape, + () => { + closeInlineCell(); + }, + ActivityEditorHotkeyScope.ActivityTargets, + ); return ( - - - } - label="Relations" - displayModeContent={ - - } - isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0} - /> - + + } + label="Relations" + displayModeContent={ + + } + isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0} + /> ); }; diff --git a/packages/twenty-front/src/modules/activities/types/ActivityEditorHotkeyScope.ts b/packages/twenty-front/src/modules/activities/types/ActivityEditorHotkeyScope.ts new file mode 100644 index 000000000..221ee25e1 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/types/ActivityEditorHotkeyScope.ts @@ -0,0 +1,5 @@ +export enum ActivityEditorHotkeyScope { + ActivityTitle = 'activity-title', + ActivityBody = 'activity-body', + ActivityTargets = 'activity-targets', +} diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx index 33a6c504e..af1954081 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx @@ -1,19 +1,10 @@ import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; -import { useInlineCell } from '../hooks/useInlineCell'; - export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => { - const { openInlineCell } = useInlineCell(); - - const handleClick = () => { - openInlineCell(); - }; - return ( diff --git a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx index 95d40a4d0..7fd67c6bf 100644 --- a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { HotkeysEvent } from 'react-hotkeys-hook/dist/types'; import TextareaAutosize from 'react-textarea-autosize'; import styled from '@emotion/styled'; @@ -7,6 +7,7 @@ import { Key } from 'ts-key-enum'; import { IconArrowRight } from '@/ui/display/icon/index'; import { Button } from '@/ui/input/button/components/Button'; import { RoundedIconButton } from '@/ui/input/button/components/RoundedIconButton'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { InputHotkeyScope } from '../types/InputHotkeyScope'; @@ -28,6 +29,7 @@ type AutosizeTextInputProps = { buttonTitle?: string; value?: string; className?: string; + onBlur?: () => void; }; const StyledContainer = styled.div` @@ -120,13 +122,18 @@ export const AutosizeTextInput = ({ buttonTitle, value = '', className, + onBlur, }: AutosizeTextInputProps) => { const [isFocused, setIsFocused] = useState(false); const [isHidden, setIsHidden] = useState( variant === AutosizeTextInputVariant.Button, ); const [text, setText] = useState(value); - + const textInputRef = useRef(null); + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); const isSendButtonDisabled = !text; const words = text.split(/\s|\n/).filter((word) => word).length; @@ -161,6 +168,8 @@ export const AutosizeTextInput = ({ event.preventDefault(); setText(''); + goBackToPreviousHotkeyScope(); + textInputRef.current?.blur(); }, InputHotkeyScope.TextInput, [onValidate, setText, isFocused], @@ -182,6 +191,18 @@ export const AutosizeTextInput = ({ setText(''); }; + const handleFocus = () => { + onFocus?.(); + setIsFocused(true); + setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput); + }; + + const handleBlur = () => { + onBlur?.(); + setIsFocused(false); + goBackToPreviousHotkeyScope(); + }; + const computedMinRows = minRows > MAX_ROWS ? MAX_ROWS : minRows; return ( @@ -190,17 +211,15 @@ export const AutosizeTextInput = ({ {!isHidden && ( { - onFocus?.(); - setIsFocused(true); - }} - onBlur={() => setIsFocused(false)} + onFocus={handleFocus} + onBlur={handleBlur} variant={variant} /> )} diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx index b767b3f9d..0ca49bca5 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx @@ -5,10 +5,11 @@ import styled from '@emotion/styled'; interface BlockEditorProps { editor: BlockNoteEditor; + onFocus?: () => void; + onBlur?: () => void; } const StyledEditor = styled.div` - min-height: 200px; width: 100%; & .editor { background: ${({ theme }) => theme.background.primary}; @@ -21,12 +22,26 @@ const StyledEditor = styled.div` } `; -export const BlockEditor = ({ editor }: BlockEditorProps) => { +export const BlockEditor = ({ editor, onFocus, onBlur }: BlockEditorProps) => { const theme = useTheme(); const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark'; + + const handleFocus = () => { + onFocus?.(); + }; + + const handleBlur = () => { + onBlur?.(); + }; + return ( - + ); };