diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts index e4c16d239..a8d3894f8 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivitySummary.test.ts @@ -1,14 +1,14 @@ import { getActivitySummary } from '../getActivitySummary'; describe('getActivitySummary', () => { - it('should work for empty body', () => { + it('should work for empty body ""', () => { const activityBody = {}; const res = getActivitySummary(JSON.stringify(activityBody)); expect(res).toEqual(''); }); - it('should work for empty body', () => { + it('should work for empty body {}', () => { const activityBody = ''; const res = getActivitySummary(JSON.stringify(activityBody)); @@ -109,4 +109,34 @@ describe('getActivitySummary', () => { expect(res).toEqual(''); }); + + it('should work for table as first block', () => { + const activityBody = [ + { + id: '591c3aa1-9e51-465d-bb59-611ef60344fb', + type: 'table', + props: { textColor: 'default', backgroundColor: 'default' }, + content: { + type: 'tableContent', + rows: [{ cells: [[], [], []] }, { cells: [[], [], []] }], + }, + children: [], + }, + { + id: '1899ab29-0122-4890-bb50-4d7cf2802f98', + type: 'paragraph', + props: { + textColor: 'default', + backgroundColor: 'default', + textAlignment: 'left', + }, + content: [], + children: [], + }, + ]; + + const res = getActivitySummary(JSON.stringify(activityBody)); + + expect(res).toEqual(''); + }); }); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts index 7032ffac1..88b27c900 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivitySummary.ts @@ -1,9 +1,25 @@ +import { isArray } from '@sniptt/guards'; + export const getActivitySummary = (activityBody: string) => { const noteBody = activityBody ? JSON.parse(activityBody) : []; - return ( - noteBody[0]?.content?.text || - noteBody[0]?.content?.map((content: any) => content?.text).join(' ') || - '' - ); + if (!noteBody.length) { + return ''; + } + + const firstNoteBlockContent = noteBody[0].content; + + if (!firstNoteBlockContent) { + return ''; + } + + if (firstNoteBlockContent.text) { + return noteBody[0].content.text; + } + + if (isArray(firstNoteBlockContent)) { + return firstNoteBlockContent.map((content: any) => content.text).join(' '); + } + + return ''; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index ef3c3ab22..3cfddff76 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -1,14 +1,16 @@ -import { useRef } from 'react'; +import { useContext, useRef } from 'react'; import styled from '@emotion/styled'; -import { DragDropContext } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 -import { useRecoilValue } from 'recoil'; +import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 +import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn'; import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; export type RecordBoardProps = { recordBoardId: string; @@ -38,11 +40,50 @@ const StyledBoardHeader = styled.div` `; export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { + const { updateOneRecord, objectMetadataItem } = + useContext(RecordBoardContext); const boardRef = useRef(null); - const { getColumnIdsState } = useRecordBoardStates(recordBoardId); + const { getColumnIdsState, columnsFamilySelector } = + useRecordBoardStates(recordBoardId); const columnIds = useRecoilValue(getColumnIdsState()); + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.type === FieldMetadataType.Select, + ); + + const onDragEnd: OnDragEndResponder = useRecoilCallback( + ({ snapshot }) => + async (result) => { + const draggedRecordId = result.draggableId; + const destinationColumnId = result.destination?.droppableId; + + if (!destinationColumnId) { + return; + } + + const column = await snapshot + .getLoadable(columnsFamilySelector(destinationColumnId)) + .getValue(); + + if (!column) { + return; + } + + if (!selectFieldMetadataItem) { + return; + } + + updateOneRecord({ + idToUpdate: draggedRecordId, + updateOneRecordInput: { + [selectFieldMetadataItem.name]: column.value, + }, + }); + }, + [columnsFamilySelector, selectFieldMetadataItem, updateOneRecord], + ); + return ( { - {}}> + {columnIds.map((columnId) => ( ) => void; + updateOneRecord: ({ + idToUpdate, + updateOneRecordInput, + }: { + idToUpdate: string; + updateOneRecordInput: Partial>; + }) => void; + deleteOneRecord: (idToDelete: string) => Promise; +}; + +export const RecordBoardContext = createContext( + {} as RecordBoardContextProps, +); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 14e8af122..efbc96d1f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -1,17 +1,24 @@ import { ReactNode, useContext, useState } from 'react'; import styled from '@emotion/styled'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { RecordChip } from '@/object-record/components/RecordChip'; +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; -import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { + FieldContext, + RecordUpdateHook, + RecordUpdateHookParams, +} from '@/object-record/record-field/contexts/FieldContext'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { IconEye } from '@/ui/display/icon/index'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; +import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; +import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState'; import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; const StyledBoardCard = styled.div<{ selected: boolean }>` @@ -105,7 +112,7 @@ const StyledCheckboxContainer = styled.div` const StyledFieldContainer = styled.div` display: flex; flex-direction: row; - width: 100%; + width: fit-content; `; const StyledCompactIconContainer = styled.div` @@ -116,7 +123,7 @@ const StyledCompactIconContainer = styled.div` export const RecordBoardCard = () => { const { recordId } = useContext(RecordBoardCardContext); - + const { updateOneRecord } = useContext(RecordBoardContext); const { getObjectSingularNameState, getIsCompactModeActiveState, @@ -139,6 +146,19 @@ export const RecordBoardCard = () => { const record = useRecoilValue(recordStoreFamilyState(recordId)); + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + setIsCurrentCardSelected(true); + setContextMenuPosition({ + x: event.clientX, + y: event.clientY, + }); + setContextMenuOpenState(true); + }; + const PreventSelectOnClickContainer = ({ children, }: { @@ -159,16 +179,29 @@ export const RecordBoardCard = () => { } }; + const useUpdateOneRecordHook: RecordUpdateHook = () => { + const updateEntity = ({ variables }: RecordUpdateHookParams) => { + updateOneRecord?.({ + idToUpdate: variables.where.id as string, + updateOneRecordInput: variables.updateOneRecordInput, + }); + }; + + return [updateEntity, { loading: false }]; + }; + if (!objectNameSingular || !record) { return null; } return ( - + setIsCurrentCardSelected(!isCurrentCardSelected)} + onClick={() => { + setIsCurrentCardSelected(!isCurrentCardSelected); + }} > @@ -212,6 +245,7 @@ export const RecordBoardCard = () => { type: fieldDefinition.type, metadata: fieldDefinition.metadata, }, + useUpdateRecord: useUpdateOneRecordHook, hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx new file mode 100644 index 000000000..f8f0b9c8d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx @@ -0,0 +1,30 @@ +import { Draggable } from '@hello-pangea/dnd'; + +import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard'; + +export const RecordBoardCardDraggableContainer = ({ + recordId, + index, +}: { + recordId: string; + index: number; +}) => { + return ( + + {(draggableProvided) => ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 20f84ece1..0f65fdf3b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -3,8 +3,6 @@ import { Droppable } from '@hello-pangea/dnd'; import { useRecoilValue } from 'recoil'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard'; -import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer'; import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; @@ -52,7 +50,7 @@ export const RecordBoardColumn = ({ recordBoardRecordIdsByColumnIdFamilyState(recordBoardColumnId), ); - if (!columnDefinition || !recordIds) { + if (!columnDefinition) { return null; } @@ -70,16 +68,8 @@ export const RecordBoardColumn = ({ - {recordIds.map((recordId) => ( - - - - ))} - + recordIds={recordIds} + /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 157a5d6f2..d9bc202be 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -2,6 +2,8 @@ import React from 'react'; import styled from '@emotion/styled'; import { DroppableProvided } from '@hello-pangea/dnd'; +import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo'; + const StyledPlaceholder = styled.div` min-height: 1px; `; @@ -13,12 +15,12 @@ const StyledColumnCardsContainer = styled.div` `; type RecordBoardColumnCardsContainerProps = { - children: React.ReactNode; + recordIds: string[]; droppableProvided: DroppableProvided; }; export const RecordBoardColumnCardsContainer = ({ - children, + recordIds, droppableProvided, }: RecordBoardColumnCardsContainerProps) => { return ( @@ -27,7 +29,7 @@ export const RecordBoardColumnCardsContainer = ({ // eslint-disable-next-line react/jsx-props-no-spreading {...droppableProvided?.droppableProps} > - {children} + {droppableProvided?.placeholder} ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx new file mode 100644 index 000000000..5cf24118a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer'; +import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; + +type RecordBoardColumnCardsMemoProps = { + recordIds: string[]; +}; + +export const RecordBoardColumnCardsMemo = React.memo( + ({ recordIds }: RecordBoardColumnCardsMemoProps) => { + return recordIds.map((recordId, index) => ( + + + + )); + }, +); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index 312e6859c..39e1fba80 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -1,4 +1,9 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordBoard } from '@/object-record/record-board/components/RecordBoard'; +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; type RecordIndexBoardContainerProps = { recordBoardId: string; @@ -9,6 +14,24 @@ type RecordIndexBoardContainerProps = { export const RecordIndexBoardContainer = ({ recordBoardId, + objectNameSingular, }: RecordIndexBoardContainerProps) => { - return ; + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + + const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular }); + const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); + const { createOneRecord } = useCreateOneRecord({ objectNameSingular }); + + return ( + + + + ); };