mirror of
				https://github.com/lingble/twenty.git
				synced 2025-11-03 22:27:57 +00:00 
			
		
		
		
	Refactor kanban new card creation (#8339)
On the kanban page, the record creation was changed a few weeks ago to enable creation on top / bottom of the columns. However, this introduced a glitch (missing background opacity). While fixing it, I have refactored the component structure to: - separate "New" button from the Empty record card
This commit is contained in:
		@@ -9,8 +9,9 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna
 | 
			
		||||
import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader';
 | 
			
		||||
import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
 | 
			
		||||
import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader';
 | 
			
		||||
import { RecordBoardColumnNewButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton';
 | 
			
		||||
import { RecordBoardColumnNewOpportunityButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton';
 | 
			
		||||
import { RecordBoardColumnNewOpportunity } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity';
 | 
			
		||||
import { RecordBoardColumnNewRecord } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecord';
 | 
			
		||||
import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton';
 | 
			
		||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
 | 
			
		||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
 | 
			
		||||
import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading';
 | 
			
		||||
@@ -74,6 +75,33 @@ export const RecordBoardColumnCardsContainer = ({
 | 
			
		||||
      // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
      {...droppableProvided?.droppableProps}
 | 
			
		||||
    >
 | 
			
		||||
      <Draggable
 | 
			
		||||
        draggableId={`new-${columnDefinition.id}-top`}
 | 
			
		||||
        index={-1}
 | 
			
		||||
        isDragDisabled={true}
 | 
			
		||||
      >
 | 
			
		||||
        {(draggableProvided) => (
 | 
			
		||||
          <div
 | 
			
		||||
            ref={draggableProvided?.innerRef}
 | 
			
		||||
            // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
            {...draggableProvided?.draggableProps}
 | 
			
		||||
          >
 | 
			
		||||
            {objectMetadataItem.nameSingular ===
 | 
			
		||||
              CoreObjectNameSingular.Opportunity &&
 | 
			
		||||
            !isOpportunitiesCompanyFieldDisabled ? (
 | 
			
		||||
              <RecordBoardColumnNewOpportunity
 | 
			
		||||
                columnId={columnDefinition.id}
 | 
			
		||||
                position="first"
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <RecordBoardColumnNewRecord
 | 
			
		||||
                columnId={columnDefinition.id}
 | 
			
		||||
                position="first"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Draggable>
 | 
			
		||||
      {isRecordIndexBoardColumnLoading ? (
 | 
			
		||||
        Array.from(
 | 
			
		||||
          {
 | 
			
		||||
@@ -98,7 +126,7 @@ export const RecordBoardColumnCardsContainer = ({
 | 
			
		||||
      )}
 | 
			
		||||
      <RecordBoardColumnFetchMoreLoader />
 | 
			
		||||
      <Draggable
 | 
			
		||||
        draggableId={`new-${columnDefinition.id}`}
 | 
			
		||||
        draggableId={`new-${columnDefinition.id}-bottom`}
 | 
			
		||||
        index={recordIds.length}
 | 
			
		||||
        isDragDisabled={true}
 | 
			
		||||
      >
 | 
			
		||||
@@ -108,16 +136,23 @@ export const RecordBoardColumnCardsContainer = ({
 | 
			
		||||
            // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
            {...draggableProvided?.draggableProps}
 | 
			
		||||
          >
 | 
			
		||||
            <StyledNewButtonContainer>
 | 
			
		||||
            {objectMetadataItem.nameSingular ===
 | 
			
		||||
              CoreObjectNameSingular.Opportunity &&
 | 
			
		||||
            !isOpportunitiesCompanyFieldDisabled ? (
 | 
			
		||||
                <RecordBoardColumnNewOpportunityButton
 | 
			
		||||
              <RecordBoardColumnNewOpportunity
 | 
			
		||||
                columnId={columnDefinition.id}
 | 
			
		||||
                position="last"
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
                <RecordBoardColumnNewButton columnId={columnDefinition.id} />
 | 
			
		||||
              <RecordBoardColumnNewRecord
 | 
			
		||||
                columnId={columnDefinition.id}
 | 
			
		||||
                position="last"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <StyledNewButtonContainer>
 | 
			
		||||
              <RecordBoardColumnNewRecordButton
 | 
			
		||||
                columnId={columnDefinition.id}
 | 
			
		||||
              />
 | 
			
		||||
            </StyledNewButtonContainer>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,16 @@
 | 
			
		||||
import styled from '@emotion/styled';
 | 
			
		||||
import { useContext, useState } from 'react';
 | 
			
		||||
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
 | 
			
		||||
 | 
			
		||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
 | 
			
		||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
 | 
			
		||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
 | 
			
		||||
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
 | 
			
		||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
 | 
			
		||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
 | 
			
		||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
 | 
			
		||||
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
 | 
			
		||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
 | 
			
		||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
 | 
			
		||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
 | 
			
		||||
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
 | 
			
		||||
 | 
			
		||||
const StyledHeader = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
@@ -102,12 +100,9 @@ export const RecordBoardColumnHeader = () => {
 | 
			
		||||
 | 
			
		||||
  const boardColumnTotal = 0;
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    newRecord,
 | 
			
		||||
    handleNewButtonClick,
 | 
			
		||||
    handleCreateSuccess,
 | 
			
		||||
    handleEntitySelect,
 | 
			
		||||
  } = useColumnNewCardActions(columnDefinition?.id ?? '');
 | 
			
		||||
  const { handleNewButtonClick } = useColumnNewCardActions(
 | 
			
		||||
    columnDefinition?.id ?? '',
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const { isOpportunitiesCompanyFieldDisabled } =
 | 
			
		||||
    useIsOpportunitiesCompanyFieldDisabled();
 | 
			
		||||
@@ -173,26 +168,6 @@ export const RecordBoardColumnHeader = () => {
 | 
			
		||||
          stageId={columnDefinition.id}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {newRecord?.isCreating &&
 | 
			
		||||
        newRecord.position === 'first' &&
 | 
			
		||||
        (newRecord.isOpportunity ? (
 | 
			
		||||
          <SingleEntitySelect
 | 
			
		||||
            disableBackgroundBlur
 | 
			
		||||
            onCancel={() => handleCreateSuccess('first', columnDefinition.id)}
 | 
			
		||||
            onEntitySelected={(company) =>
 | 
			
		||||
              company && handleEntitySelect('first', company)
 | 
			
		||||
            }
 | 
			
		||||
            relationObjectNameSingular={CoreObjectNameSingular.Company}
 | 
			
		||||
            relationPickerScopeId="relation-picker"
 | 
			
		||||
            selectedRelationRecordIds={[]}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <RecordBoardCard
 | 
			
		||||
            isCreating={true}
 | 
			
		||||
            onCreateSuccess={() => handleCreateSuccess('first')}
 | 
			
		||||
            position="first"
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
    </StyledColumn>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
import styled from '@emotion/styled';
 | 
			
		||||
 | 
			
		||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
 | 
			
		||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
 | 
			
		||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
 | 
			
		||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
 | 
			
		||||
import { useRecoilValue } from 'recoil';
 | 
			
		||||
 | 
			
		||||
const StyledCompanyPickerContainer = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  align-self: baseline;
 | 
			
		||||
  background-color: ${({ theme }) => theme.background.primary};
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: ${({ theme }) => theme.border.radius.sm};
 | 
			
		||||
  color: ${({ theme }) => theme.font.color.tertiary};
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: ${({ theme }) => theme.spacing(1)};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const RecordBoardColumnNewOpportunity = ({
 | 
			
		||||
  columnId,
 | 
			
		||||
  position,
 | 
			
		||||
}: {
 | 
			
		||||
  columnId: string;
 | 
			
		||||
  position: 'last' | 'first';
 | 
			
		||||
}) => {
 | 
			
		||||
  const newRecord = useRecoilValue(
 | 
			
		||||
    recordBoardNewRecordByColumnIdSelector({
 | 
			
		||||
      familyKey: columnId,
 | 
			
		||||
      scopeId: columnId,
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  const { handleCreateSuccess, handleEntitySelect } = useAddNewCard();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {newRecord.isCreating && newRecord.position === position && (
 | 
			
		||||
        <StyledCompanyPickerContainer>
 | 
			
		||||
          <SingleEntitySelect
 | 
			
		||||
            disableBackgroundBlur
 | 
			
		||||
            onCancel={() => handleCreateSuccess(position, columnId, false)}
 | 
			
		||||
            onEntitySelected={(company) =>
 | 
			
		||||
              company ? handleEntitySelect(position, company) : null
 | 
			
		||||
            }
 | 
			
		||||
            relationObjectNameSingular={CoreObjectNameSingular.Company}
 | 
			
		||||
            relationPickerScopeId="relation-picker"
 | 
			
		||||
            selectedRelationRecordIds={[]}
 | 
			
		||||
          />
 | 
			
		||||
        </StyledCompanyPickerContainer>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
import { useTheme } from '@emotion/react';
 | 
			
		||||
import styled from '@emotion/styled';
 | 
			
		||||
import { IconPlus } from 'twenty-ui';
 | 
			
		||||
 | 
			
		||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
 | 
			
		||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
 | 
			
		||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
 | 
			
		||||
 | 
			
		||||
const StyledButton = styled.button`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  align-self: baseline;
 | 
			
		||||
  background-color: ${({ theme }) => theme.background.primary};
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: ${({ theme }) => theme.border.radius.sm};
 | 
			
		||||
  color: ${({ theme }) => theme.font.color.tertiary};
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: ${({ theme }) => theme.spacing(1)};
 | 
			
		||||
  padding: ${({ theme }) => theme.spacing(1)};
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: ${({ theme }) => theme.background.tertiary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const RecordBoardColumnNewOpportunityButton = ({
 | 
			
		||||
  columnId,
 | 
			
		||||
}: {
 | 
			
		||||
  columnId: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    newRecord,
 | 
			
		||||
    handleNewButtonClick,
 | 
			
		||||
    handleEntitySelect,
 | 
			
		||||
    handleCreateSuccess,
 | 
			
		||||
  } = useColumnNewCardActions(columnId);
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {newRecord.isCreating &&
 | 
			
		||||
      newRecord.position === 'last' &&
 | 
			
		||||
      newRecord.isOpportunity ? (
 | 
			
		||||
        <SingleEntitySelect
 | 
			
		||||
          disableBackgroundBlur
 | 
			
		||||
          onCancel={() => handleCreateSuccess('last', columnId, false)}
 | 
			
		||||
          onEntitySelected={(company) =>
 | 
			
		||||
            company ? handleEntitySelect('last', company) : null
 | 
			
		||||
          }
 | 
			
		||||
          relationObjectNameSingular={CoreObjectNameSingular.Company}
 | 
			
		||||
          relationPickerScopeId="relation-picker"
 | 
			
		||||
          selectedRelationRecordIds={[]}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <StyledButton onClick={() => handleNewButtonClick('last', true)}>
 | 
			
		||||
          <IconPlus size={theme.icon.size.md} />
 | 
			
		||||
          New
 | 
			
		||||
        </StyledButton>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
 | 
			
		||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
 | 
			
		||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
 | 
			
		||||
import { useRecoilValue } from 'recoil';
 | 
			
		||||
 | 
			
		||||
export const RecordBoardColumnNewRecord = ({
 | 
			
		||||
  columnId,
 | 
			
		||||
  position,
 | 
			
		||||
}: {
 | 
			
		||||
  columnId: string;
 | 
			
		||||
  position: 'first' | 'last';
 | 
			
		||||
}) => {
 | 
			
		||||
  const newRecord = useRecoilValue(
 | 
			
		||||
    recordBoardNewRecordByColumnIdSelector({
 | 
			
		||||
      familyKey: columnId,
 | 
			
		||||
      scopeId: columnId,
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  const { handleCreateSuccess } = useAddNewCard();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {newRecord.isCreating && newRecord.position === position && (
 | 
			
		||||
        <RecordBoardCard
 | 
			
		||||
          isCreating={true}
 | 
			
		||||
          onCreateSuccess={() => handleCreateSuccess(position)}
 | 
			
		||||
          position={position}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
 | 
			
		||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
 | 
			
		||||
import { useTheme } from '@emotion/react';
 | 
			
		||||
import styled from '@emotion/styled';
 | 
			
		||||
@@ -15,34 +14,20 @@ const StyledNewButton = styled.button`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: ${({ theme }) => theme.spacing(1)};
 | 
			
		||||
  padding: ${({ theme }) => theme.spacing(1)};
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: ${({ theme }) => theme.background.tertiary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const RecordBoardColumnNewButton = ({
 | 
			
		||||
export const RecordBoardColumnNewRecordButton = ({
 | 
			
		||||
  columnId,
 | 
			
		||||
}: {
 | 
			
		||||
  columnId: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  const { newRecord, handleNewButtonClick, handleCreateSuccess } =
 | 
			
		||||
    useColumnNewCardActions(columnId);
 | 
			
		||||
 | 
			
		||||
  if (
 | 
			
		||||
    newRecord.isCreating &&
 | 
			
		||||
    newRecord.position === 'last' &&
 | 
			
		||||
    !newRecord.isOpportunity
 | 
			
		||||
  ) {
 | 
			
		||||
    return (
 | 
			
		||||
      <RecordBoardCard
 | 
			
		||||
        isCreating={true}
 | 
			
		||||
        onCreateSuccess={() => handleCreateSuccess('last')}
 | 
			
		||||
        position="last"
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  const { handleNewButtonClick } = useColumnNewCardActions(columnId);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledNewButton onClick={() => handleNewButtonClick('last', false)}>
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
 | 
			
		||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
 | 
			
		||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
 | 
			
		||||
import { useRecoilValue } from 'recoil';
 | 
			
		||||
 | 
			
		||||
export const useColumnNewCardActions = (columnId: string) => {
 | 
			
		||||
@@ -12,15 +11,7 @@ export const useColumnNewCardActions = (columnId: string) => {
 | 
			
		||||
    (field) => field.isLabelIdentifier,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const { handleAddNewCardClick, handleCreateSuccess, handleEntitySelect } =
 | 
			
		||||
    useAddNewCard();
 | 
			
		||||
 | 
			
		||||
  const newRecord = useRecoilValue(
 | 
			
		||||
    recordBoardNewRecordByColumnIdSelector({
 | 
			
		||||
      familyKey: columnId,
 | 
			
		||||
      scopeId: columnId,
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  const { handleAddNewCardClick } = useAddNewCard();
 | 
			
		||||
 | 
			
		||||
  const handleNewButtonClick = (
 | 
			
		||||
    position: 'first' | 'last',
 | 
			
		||||
@@ -36,9 +27,6 @@ export const useColumnNewCardActions = (columnId: string) => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    newRecord,
 | 
			
		||||
    handleNewButtonClick,
 | 
			
		||||
    handleCreateSuccess,
 | 
			
		||||
    handleEntitySelect,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user