added button in nav bar for kanban view (#6829)

@Bonapara 
Addressing issue #6783.
 
I tried to achieve the exact behavior you were looking for, but I
couldn't get the dropdown to render correctly in that specific column.
I'd love some help to make sure it's working as expected! 😊
Most of the logic is shared with the `useHandleOpportunity` and
`useAddNewCard` hooks, which could be refactored to reduce code debt.
Also, please go harsh with the review because I know there's a lot of
code cleaning required.
I also agree with Charles's point in [this
comment](https://github.com/twentyhq/twenty/issues/6783#issuecomment-2323299840).
Thanks :)


https://github.com/user-attachments/assets/bccdb3f1-3946-4e22-b9a4-b7496ef134c9
This commit is contained in:
nitin
2024-09-10 14:23:27 +05:30
committed by GitHub
parent fbe9e2c0db
commit 05d70b03fd
6 changed files with 309 additions and 6 deletions

View File

@ -1,8 +1,8 @@
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageHeader } from '@/ui/layout/page/PageHeader';
@ -12,13 +12,15 @@ import { capitalize } from '~/utils/string/capitalize';
type RecordIndexPageHeaderProps = {
createRecord: () => void;
recordIndexId: string;
objectNamePlural: string;
};
export const RecordIndexPageHeader = ({
createRecord,
recordIndexId,
objectNamePlural,
}: RecordIndexPageHeaderProps) => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const { findObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems();
@ -32,7 +34,7 @@ export const RecordIndexPageHeader = ({
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
const canAddRecord =
const isTable =
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote;
const pageHeaderTitle =
@ -41,7 +43,14 @@ export const RecordIndexPageHeader = ({
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHotkeysEffect onAddButtonClick={createRecord} />
{canAddRecord && <PageAddButton onClick={createRecord} />}
{isTable ? (
<PageAddButton onClick={createRecord} />
) : (
<RecordIndexPageKanbanAddButton
recordIndexId={recordIndexId}
objectNamePlural={objectNamePlural}
/>
)}
</PageHeader>
);
};

View File

@ -0,0 +1,158 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexPageKanbanAddButton } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import styled from '@emotion/styled';
import { useCallback, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconPlus, isDefined } from 'twenty-ui';
const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
width: 100%;
`;
const StyledDropDownMenu = styled(DropdownMenu)`
width: 200px;
`;
type RecordIndexPageKanbanAddButtonProps = {
recordIndexId: string;
objectNamePlural: string;
};
export const RecordIndexPageKanbanAddButton = ({
recordIndexId,
objectNamePlural,
}: RecordIndexPageKanbanAddButtonProps) => {
const dropdownId = `record-index-page-add-button-dropdown`;
const [isSelectingCompany, setIsSelectingCompany] = useState(false);
const [selectedColumnDefinition, setSelectedColumnDefinition] =
useState<RecordBoardColumnDefinition>();
const { columnIdsState } = useRecordBoardStates(recordIndexId);
const columnIds = useRecoilValue(columnIdsState);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { resetSearchFilter } = useEntitySelectSearch({
relationPickerScopeId: 'relation-picker',
});
const { closeDropdown } = useDropdown(dropdownId);
const {
selectFieldMetadataItem,
isOpportunity,
createOpportunity,
createRecordWithoutCompany,
} = useRecordIndexPageKanbanAddButton({
objectNamePlural,
});
const handleItemClick = useCallback(
(columnDefinition: RecordBoardColumnDefinition) => {
if (isOpportunity) {
setIsSelectingCompany(true);
setSelectedColumnDefinition(columnDefinition);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
} else {
createRecordWithoutCompany(columnDefinition);
closeDropdown();
}
},
[
isOpportunity,
createRecordWithoutCompany,
setHotkeyScopeAndMemorizePreviousScope,
closeDropdown,
],
);
const handleEntitySelect = useCallback(
(company?: EntityForSelect) => {
setIsSelectingCompany(false);
goBackToPreviousHotkeyScope();
resetSearchFilter();
if (isDefined(company) && isDefined(selectedColumnDefinition)) {
createOpportunity(company, selectedColumnDefinition);
}
closeDropdown();
},
[
createOpportunity,
goBackToPreviousHotkeyScope,
resetSearchFilter,
selectedColumnDefinition,
closeDropdown,
],
);
const handleCancel = useCallback(() => {
resetSearchFilter();
goBackToPreviousHotkeyScope();
setIsSelectingCompany(false);
}, [goBackToPreviousHotkeyScope, resetSearchFilter]);
if (!selectFieldMetadataItem) {
return null;
}
return (
<Dropdown
dropdownMenuWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={
<IconButton
Icon={IconPlus}
dataTestId="add-button"
size="medium"
variant="secondary"
accent="default"
ariaLabel="Add"
/>
}
dropdownId={dropdownId}
dropdownComponents={
<StyledDropDownMenu>
{isOpportunity && isSelectingCompany ? (
<SingleEntitySelect
disableBackgroundBlur
onCancel={handleCancel}
onEntitySelected={handleEntitySelect}
relationObjectNameSingular={CoreObjectNameSingular.Company}
relationPickerScopeId="relation-picker"
selectedRelationRecordIds={[]}
/>
) : (
<StyledDropdownMenuItemsContainer>
{columnIds.map((columnId) => (
<RecordIndexPageKanbanAddMenuItem
key={columnId}
columnId={columnId}
recordIndexId={recordIndexId}
onItemClick={handleItemClick}
/>
))}
</StyledDropdownMenuItemsContainer>
)}
</StyledDropDownMenu>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
);
};

View File

@ -0,0 +1,55 @@
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import styled from '@emotion/styled';
import { Tag } from 'twenty-ui';
const StyledMenuItem = styled(MenuItem)`
width: 200px;
`;
type RecordIndexPageKanbanAddMenuItemProps = {
columnId: string;
recordIndexId: string;
onItemClick: (columnDefinition: any) => void;
};
export const RecordIndexPageKanbanAddMenuItem = ({
columnId,
recordIndexId,
onItemClick,
}: RecordIndexPageKanbanAddMenuItemProps) => {
const { columnDefinition } = useRecordIndexPageKanbanAddMenuItem(
recordIndexId,
columnId,
);
if (!columnDefinition) {
return null;
}
return (
<StyledMenuItem
text={
<Tag
variant={
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? 'solid'
: 'outline'
}
color={
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? columnDefinition.color
: 'transparent'
}
text={columnDefinition.title}
weight={
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? 'regular'
: 'medium'
}
/>
}
onClick={() => onItemClick(columnDefinition)}
/>
);
};

View File

@ -0,0 +1,65 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
type useRecordIndexPageKanbanAddButtonProps = {
objectNamePlural: string;
};
export const useRecordIndexPageKanbanAddButton = ({
objectNamePlural,
}: useRecordIndexPageKanbanAddButtonProps) => {
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const recordIndexKanbanFieldMetadataId = useRecoilValue(
recordIndexKanbanFieldMetadataIdState,
);
const { createOneRecord } = useCreateOneRecord({ objectNameSingular });
const selectFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === recordIndexKanbanFieldMetadataId,
);
const isOpportunity =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity;
const createOpportunity = (
company: EntityForSelect,
columnDefinition: RecordBoardColumnDefinition,
) => {
if (isDefined(selectFieldMetadataItem)) {
createOneRecord({
name: company.name,
companyId: company.id,
position: 'first',
[selectFieldMetadataItem.name]: columnDefinition?.value,
});
}
};
const createRecordWithoutCompany = (
columnDefinition: RecordBoardColumnDefinition,
) => {
if (isDefined(selectFieldMetadataItem)) {
createOneRecord({
[selectFieldMetadataItem.name]: columnDefinition?.value,
position: 'first',
});
}
};
return {
selectFieldMetadataItem,
isOpportunity,
createOpportunity,
createRecordWithoutCompany,
};
};

View File

@ -0,0 +1,12 @@
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecoilValue } from 'recoil';
export const useRecordIndexPageKanbanAddMenuItem = (
recordIndexId: string,
columnId: string,
) => {
const { columnsFamilySelector } = useRecordBoardStates(recordIndexId);
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
return { columnDefinition };
};

View File

@ -42,7 +42,11 @@ export const RecordIndexPage = () => {
return (
<PageContainer>
<PageTitle title={`${capitalize(objectNamePlural)}`} />
<RecordIndexPageHeader createRecord={handleAddButtonClick} />
<RecordIndexPageHeader
createRecord={handleAddButtonClick}
recordIndexId={recordIndexId}
objectNamePlural={objectNamePlural}
/>
<PageBody>
<StyledIndexContainer>
<RecordIndexContainer