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 (
-
+