From 05d70b03fdcb08ea80afb38b891f191e49a3a4f3 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:23:27 +0530 Subject: [PATCH] added button in nav bar for kanban view (#6829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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 --- .../components/RecordIndexPageHeader.tsx | 19 ++- .../RecordIndexPageKanbanAddButton.tsx | 158 ++++++++++++++++++ .../RecordIndexPageKanbanAddMenuItem.tsx | 55 ++++++ .../useRecordIndexPageKanbanAddButton.ts | 65 +++++++ .../useRecordIndexPageKanbanAddMenuItem.ts | 12 ++ .../pages/object-record/RecordIndexPage.tsx | 6 +- 6 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx index 5039ec7a3447..b2296a6f3b0f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx @@ -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 ( - {canAddRecord && } + {isTable ? ( + + ) : ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx new file mode 100644 index 000000000000..6299546e03b6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx @@ -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(); + + 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 ( + + } + dropdownId={dropdownId} + dropdownComponents={ + + {isOpportunity && isSelectingCompany ? ( + + ) : ( + + {columnIds.map((columnId) => ( + + ))} + + )} + + } + dropdownHotkeyScope={{ scope: dropdownId }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx new file mode 100644 index 000000000000..a99b3abfd862 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx @@ -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 ( + + } + onClick={() => onItemClick(columnDefinition)} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts new file mode 100644 index 000000000000..1d64225bf485 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts new file mode 100644 index 000000000000..8e5604cb0fe8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 2df55058eb82..068b95ab8d1f 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -42,7 +42,11 @@ export const RecordIndexPage = () => { return ( - +