Allow Card field update and card drag on new record board (#3661)

This commit is contained in:
Charles Bochet
2024-01-29 08:59:13 +01:00
committed by GitHub
parent 6eca6dc780
commit 7fdd7119d2
10 changed files with 240 additions and 35 deletions

View File

@ -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('');
});
});

View File

@ -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 '';
};

View File

@ -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<HTMLDivElement>(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 (
<RecordBoardScope
recordBoardScopeId={getScopeIdFromComponentId(recordBoardId)}
@ -53,7 +94,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
<StyledBoardHeader />
<ScrollWrapper>
<StyledContainer ref={boardRef}>
<DragDropContext onDragEnd={() => {}}>
<DragDropContext onDragEnd={onDragEnd}>
{columnIds.map((columnId) => (
<RecordBoardColumn
key={columnId}

View File

@ -0,0 +1,21 @@
import { createContext } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
type RecordBoardContextProps = {
objectMetadataItem: ObjectMetadataItem;
createOneRecord: (recordInput: Partial<ObjectRecord>) => void;
updateOneRecord: ({
idToUpdate,
updateOneRecordInput,
}: {
idToUpdate: string;
updateOneRecordInput: Partial<Omit<ObjectRecord, 'id'>>;
}) => void;
deleteOneRecord: (idToDelete: string) => Promise<unknown>;
};
export const RecordBoardContext = createContext<RecordBoardContextProps>(
{} as RecordBoardContextProps,
);

View File

@ -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 (
<StyledBoardCardWrapper>
<StyledBoardCardWrapper onContextMenu={handleContextMenu}>
<StyledBoardCard
selected={isCurrentCardSelected}
onMouseLeave={onMouseLeaveBoard}
onClick={() => setIsCurrentCardSelected(!isCurrentCardSelected)}
onClick={() => {
setIsCurrentCardSelected(!isCurrentCardSelected);
}}
>
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
<RecordChip objectNameSingular={objectNameSingular} record={record} />
@ -212,6 +245,7 @@ export const RecordBoardCard = () => {
type: fieldDefinition.type,
metadata: fieldDefinition.metadata,
},
useUpdateRecord: useUpdateOneRecordHook,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>

View File

@ -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 (
<Draggable key={recordId} draggableId={recordId} index={index}>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
className="entity-board-card"
data-selectable-id={recordId}
data-select-disable
>
<RecordBoardCard />
</div>
)}
</Draggable>
);
};

View File

@ -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 = ({
<RecordBoardColumnHeader />
<RecordBoardColumnCardsContainer
droppableProvided={droppableProvided}
>
{recordIds.map((recordId) => (
<RecordBoardCardContext.Provider
value={{ recordId }}
key={recordId}
>
<RecordBoardCard />
</RecordBoardCardContext.Provider>
))}
</RecordBoardColumnCardsContainer>
recordIds={recordIds}
/>
</StyledColumn>
)}
</Droppable>

View File

@ -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}
<RecordBoardColumnCardsMemo recordIds={recordIds} />
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
</StyledColumnCardsContainer>
);

View File

@ -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) => (
<RecordBoardCardContext.Provider value={{ recordId }} key={recordId}>
<RecordBoardCardDraggableContainer recordId={recordId} index={index} />
</RecordBoardCardContext.Provider>
));
},
);

View File

@ -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 <RecordBoard recordBoardId={recordBoardId} />;
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
const { createOneRecord } = useCreateOneRecord({ objectNameSingular });
return (
<RecordBoardContext.Provider
value={{
objectMetadataItem,
createOneRecord,
updateOneRecord,
deleteOneRecord,
}}
>
<RecordBoard recordBoardId={recordBoardId} />
</RecordBoardContext.Provider>
);
};