2394-feat(front): create new record on click of plus icon (#2660)

* 2394-feat(front): create new record on click of plus icon

* 2394-feat(front): fix of Icon Button

* 2394-fix: PR fixes

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Kanav Arora 2023-11-30 00:37:55 +05:30 committed by GitHub
parent 7e454d2013
commit 976f86093c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 238 additions and 174 deletions

View File

@ -6,7 +6,7 @@ import { AttachmentDropdown } from '@/activities/files/components/AttachmentDrop
import { AttachmentIcon } from '@/activities/files/components/AttachmentIcon';
import { Attachment } from '@/activities/files/types/Attachment';
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { IconCalendar } from '@/ui/display/icon';
import {
FieldContext,
@ -60,8 +60,8 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
[attachment?.id],
);
const { deleteOneObject: deleteOneAttachment } =
useDeleteOneObjectRecord<Attachment>({
const { deleteOneRecord: deleteOneAttachment } =
useDeleteOneRecord<Attachment>({
objectNameSingular: 'attachment',
});

View File

@ -8,7 +8,7 @@ import { Attachment } from '@/activities/files/types/Attachment';
import { getFileType } from '@/activities/files/utils/getFileType';
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
@ -65,8 +65,8 @@ export const Attachments = ({
const [uploadFile] = useUploadFileMutation();
const { createOneObject: createOneAttachment } =
useCreateOneObjectRecord<Attachment>({
const { createOneRecord: createOneAttachment } =
useCreateOneRecord<Attachment>({
objectNameSingular: 'attachment',
});

View File

@ -1,10 +1,10 @@
import { Attachment } from '@/activities/files/types/Attachment';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
export const useAttachments = (entity: ActivityTargetableEntity) => {
const { objects: attachments } = useFindManyObjectRecords({
const { records: attachments } = useFindManyRecords({
objectNamePlural: 'attachments',
filter: {
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },

View File

@ -24,8 +24,10 @@ const StyledContainer = styled.div`
export const RecordTableContainer = ({
objectNamePlural,
createRecord,
}: {
objectNamePlural: string;
createRecord: () => void;
}) => {
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
@ -87,7 +89,8 @@ export const RecordTableContainer = ({
<RecordTable
recordTableId={recordTableId}
viewBarId={viewBarId}
updateEntityMutation={updateEntity}
updateRecordMutation={updateEntity}
createRecord={createRecord}
/>
</StyledContainer>
);

View File

@ -67,7 +67,10 @@ export const RecordTablePage = () => {
</PageHeader>
<PageBody>
<StyledTableContainer>
<RecordTableContainer objectNamePlural={objectNamePlural} />
<RecordTableContainer
objectNamePlural={objectNamePlural}
createRecord={handleAddButtonClick}
/>
</StyledTableContainer>
<RecordTableActionBar />
<RecordTableContextMenu />

View File

@ -33,7 +33,8 @@ export const SignInBackgroundMockContainer = () => {
<RecordTable
recordTableId={recordTableId}
viewBarId={viewBarId}
updateEntityMutation={() => {}}
createRecord={() => {}}
updateRecordMutation={() => {}}
/>
</StyledContainer>
);

View File

@ -2,6 +2,7 @@ import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { RecordTableHeader } from '@/ui/object/record-table/components/RecordTableHeader';
import { RecordTableInternalEffect } from '@/ui/object/record-table/components/RecordTableInternalEffect';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { RecordTableScope } from '@/ui/object/record-table/scopes/RecordTableScope';
@ -13,7 +14,6 @@ import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinit
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
import { RecordTableBody } from './RecordTableBody';
import { RecordTableHeader } from './RecordTableHeader';
const StyledTable = styled.table`
border-collapse: collapse;
@ -77,13 +77,15 @@ const StyledTableContainer = styled.div`
type RecordTableProps = {
recordTableId: string;
viewBarId: string;
updateEntityMutation: (params: any) => void;
updateRecordMutation: (params: any) => void;
createRecord: () => void;
};
export const RecordTable = ({
updateRecordMutation,
createRecord,
recordTableId,
viewBarId,
updateEntityMutation,
}: RecordTableProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);
@ -100,12 +102,12 @@ export const RecordTable = ({
})}
>
<ScrollWrapper>
<EntityUpdateMutationContext.Provider value={updateEntityMutation}>
<EntityUpdateMutationContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<div ref={tableBodyRef}>
<StyledTable className="entity-table-cell">
<RecordTableHeader />
<RecordTableHeader createRecord={createRecord} />
<RecordTableBody />
</StyledTable>
<DragSelect

View File

@ -1,76 +1,21 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilValue } from 'recoil';
import { IconPlus } from '@/ui/display/icon';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { RecordTableHeaderCell } from '@/ui/object/record-table/components/RecordTableHeaderCell';
import { useRecordTableScopedStates } from '../hooks/internal/useRecordTableScopedStates';
import { useTableColumns } from '../hooks/useTableColumns';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
import { RecordTableHeaderPlusButtonContent } from './RecordTableHeaderPlusButtonContent';
import { SelectAllCheckbox } from './SelectAllCheckbox';
const COLUMN_MIN_WIDTH = 104;
const StyledColumnHeaderCell = styled.th<{
columnWidth: number;
isResizing?: boolean;
}>`
${({ columnWidth }) => `
min-width: ${columnWidth}px;
width: ${columnWidth}px;
`}
position: relative;
user-select: none;
${({ theme }) => {
return `
&:hover {
background: ${theme.background.transparent.light};
};
`;
}};
${({ isResizing, theme }) => {
if (isResizing) {
return `&:after {
background-color: ${theme.color.blue};
bottom: 0;
content: '';
display: block;
position: absolute;
right: -1px;
top: 0;
width: 2px;
}`;
}
}};
`;
const StyledResizeHandler = styled.div`
bottom: 0;
cursor: col-resize;
padding: 0 ${({ theme }) => theme.spacing(2)};
position: absolute;
right: -9px;
top: 0;
width: 3px;
z-index: 1;
`;
const StyledTableHead = styled.thead`
cursor: pointer;
`;
const StyledColumnHeadContainer = styled.div`
position: relative;
z-index: 1;
`;
const StyledPlusIconHeaderCell = styled.th`
${({ theme }) => {
return `
@ -101,83 +46,17 @@ const HIDDEN_TABLE_COLUMN_DROPDOWN_SCOPE_ID =
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
'hidden-table-columns-dropdown-hotkey-scope-id';
export const RecordTableHeader = () => {
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
resizeFieldOffsetState,
);
export const RecordTableHeader = ({
createRecord,
}: {
createRecord: () => void;
}) => {
const { hiddenTableColumnsSelector, visibleTableColumnsSelector } =
useRecordTableScopedStates();
const {
tableColumnsState,
tableColumnsByKeySelector,
hiddenTableColumnsSelector,
visibleTableColumnsSelector,
} = useRecordTableScopedStates();
const tableColumns = useRecoilValue(tableColumnsState);
const tableColumnsByKey = useRecoilValue(tableColumnsByKeySelector);
const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector);
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
>(null);
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
const { handleColumnsChange } = useTableColumns();
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
const handleResizeHandlerMove = useCallback(
(positionX: number) => {
if (!initialPointerPositionX) return;
setResizeFieldOffset(positionX - initialPointerPositionX);
},
[setResizeFieldOffset, initialPointerPositionX],
);
const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) =>
async () => {
if (!resizedFieldKey) return;
const nextWidth = Math.round(
Math.max(
tableColumnsByKey[resizedFieldKey].size +
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
COLUMN_MIN_WIDTH,
),
);
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldKey(null);
if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) {
const nextColumns = tableColumns.map((column) =>
column.fieldMetadataId === resizedFieldKey
? { ...column, size: nextWidth }
: column,
);
await handleColumnsChange(nextColumns);
}
},
[resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange],
);
useTrackPointer({
shouldTrackPointer: resizedFieldKey !== null,
onMouseDown: handleResizeHandlerStart,
onMouseMove: handleResizeHandlerMove,
onMouseUp: handleResizeHandlerEnd,
});
const primaryColumn = visibleTableColumns.find(
(column) => column.position === 0,
);
const theme = useTheme();
return (
@ -193,35 +72,11 @@ export const RecordTableHeader = () => {
<SelectAllCheckbox />
</th>
{visibleTableColumns.map((column) => (
<StyledColumnHeaderCell
<RecordTableHeaderCell
key={column.fieldMetadataId}
isResizing={resizedFieldKey === column.fieldMetadataId}
columnWidth={Math.max(
tableColumnsByKey[column.fieldMetadataId].size +
(resizedFieldKey === column.fieldMetadataId
? resizeFieldOffset
: 0),
COLUMN_MIN_WIDTH,
)}
>
<StyledColumnHeadContainer>
<ColumnHeadWithDropdown
column={column}
isFirstColumn={column.position === 1}
isLastColumn={
column.position === visibleTableColumns.length - 1
}
primaryColumnKey={primaryColumn?.fieldMetadataId || ''}
/>
</StyledColumnHeadContainer>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldKey(column.fieldMetadataId);
}}
/>
</StyledColumnHeaderCell>
column={column}
createRecord={createRecord}
/>
))}
{hiddenTableColumns.length > 0 && (
<StyledPlusIconHeaderCell>

View File

@ -0,0 +1,200 @@
import { useCallback, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconPlus } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates';
import { useTableColumns } from '@/ui/object/record-table/hooks/useTableColumns';
import { resizeFieldOffsetState } from '@/ui/object/record-table/states/resizeFieldOffsetState';
import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
const COLUMN_MIN_WIDTH = 104;
const StyledColumnHeaderCell = styled.th<{
columnWidth: number;
isResizing?: boolean;
}>`
${({ columnWidth }) => `
min-width: ${columnWidth}px;
width: ${columnWidth}px;
`}
position: relative;
user-select: none;
${({ theme }) => {
return `
&:hover {
background: ${theme.background.transparent.light};
};
`;
}};
${({ isResizing, theme }) => {
if (isResizing) {
return `&:after {
background-color: ${theme.color.blue};
bottom: 0;
content: '';
display: block;
position: absolute;
right: -1px;
top: 0;
width: 2px;
}`;
}
}};
`;
const StyledResizeHandler = styled.div`
bottom: 0;
cursor: col-resize;
padding: 0 ${({ theme }) => theme.spacing(2)};
position: absolute;
right: -9px;
top: 0;
width: 3px;
z-index: 1;
`;
const StyledColumnHeadContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
z-index: 1;
`;
const StyledHeaderIcon = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(1)};
margin-right: ${({ theme }) => theme.spacing(1)};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
export const RecordTableHeaderCell = ({
column,
createRecord,
}: {
column: ColumnDefinition<FieldMetadata>;
createRecord: () => void;
}) => {
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
resizeFieldOffsetState,
);
const {
tableColumnsState,
tableColumnsByKeySelector,
visibleTableColumnsSelector,
} = useRecordTableScopedStates();
const tableColumns = useRecoilValue(tableColumnsState);
const tableColumnsByKey = useRecoilValue(tableColumnsByKeySelector);
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
>(null);
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
const { handleColumnsChange } = useTableColumns();
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
const [iconVisibility, setIconVisibility] = useState(false);
const primaryColumn = visibleTableColumns.find(
(column) => column.position === 0,
);
const handleResizeHandlerMove = useCallback(
(positionX: number) => {
if (!initialPointerPositionX) return;
setResizeFieldOffset(positionX - initialPointerPositionX);
},
[setResizeFieldOffset, initialPointerPositionX],
);
const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) =>
async () => {
if (!resizedFieldKey) return;
const nextWidth = Math.round(
Math.max(
tableColumnsByKey[resizedFieldKey].size +
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
COLUMN_MIN_WIDTH,
),
);
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldKey(null);
if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) {
const nextColumns = tableColumns.map((column) =>
column.fieldMetadataId === resizedFieldKey
? { ...column, size: nextWidth }
: column,
);
await handleColumnsChange(nextColumns);
}
},
[resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange],
);
useTrackPointer({
shouldTrackPointer: resizedFieldKey !== null,
onMouseDown: handleResizeHandlerStart,
onMouseMove: handleResizeHandlerMove,
onMouseUp: handleResizeHandlerEnd,
});
return (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
isResizing={resizedFieldKey === column.fieldMetadataId}
columnWidth={Math.max(
tableColumnsByKey[column.fieldMetadataId].size +
(resizedFieldKey === column.fieldMetadataId ? resizeFieldOffset : 0) +
24,
COLUMN_MIN_WIDTH,
)}
>
<StyledColumnHeadContainer
onMouseEnter={() => setIconVisibility(true)}
onMouseLeave={() => setIconVisibility(false)}
>
<ColumnHeadWithDropdown
column={column}
isFirstColumn={column.position === 1}
isLastColumn={column.position === visibleTableColumns.length - 1}
primaryColumnKey={primaryColumn?.fieldMetadataId || ''}
/>
{iconVisibility && column.position === 0 && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={createRecord}
/>
</StyledHeaderIcon>
)}
</StyledColumnHeadContainer>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldKey(column.fieldMetadataId);
}}
/>
</StyledColumnHeaderCell>
);
};