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:
@ -1,8 +1,8 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
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 { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||||
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||||
@ -12,13 +12,15 @@ import { capitalize } from '~/utils/string/capitalize';
|
|||||||
|
|
||||||
type RecordIndexPageHeaderProps = {
|
type RecordIndexPageHeaderProps = {
|
||||||
createRecord: () => void;
|
createRecord: () => void;
|
||||||
|
recordIndexId: string;
|
||||||
|
objectNamePlural: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordIndexPageHeader = ({
|
export const RecordIndexPageHeader = ({
|
||||||
createRecord,
|
createRecord,
|
||||||
|
recordIndexId,
|
||||||
|
objectNamePlural,
|
||||||
}: RecordIndexPageHeaderProps) => {
|
}: RecordIndexPageHeaderProps) => {
|
||||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
|
||||||
|
|
||||||
const { findObjectMetadataItemByNamePlural } =
|
const { findObjectMetadataItemByNamePlural } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ export const RecordIndexPageHeader = ({
|
|||||||
|
|
||||||
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
||||||
|
|
||||||
const canAddRecord =
|
const isTable =
|
||||||
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote;
|
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote;
|
||||||
|
|
||||||
const pageHeaderTitle =
|
const pageHeaderTitle =
|
||||||
@ -41,7 +43,14 @@ export const RecordIndexPageHeader = ({
|
|||||||
return (
|
return (
|
||||||
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
||||||
<PageHotkeysEffect onAddButtonClick={createRecord} />
|
<PageHotkeysEffect onAddButtonClick={createRecord} />
|
||||||
{canAddRecord && <PageAddButton onClick={createRecord} />}
|
{isTable ? (
|
||||||
|
<PageAddButton onClick={createRecord} />
|
||||||
|
) : (
|
||||||
|
<RecordIndexPageKanbanAddButton
|
||||||
|
recordIndexId={recordIndexId}
|
||||||
|
objectNamePlural={objectNamePlural}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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 };
|
||||||
|
};
|
||||||
@ -42,7 +42,11 @@ export const RecordIndexPage = () => {
|
|||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
||||||
<RecordIndexPageHeader createRecord={handleAddButtonClick} />
|
<RecordIndexPageHeader
|
||||||
|
createRecord={handleAddButtonClick}
|
||||||
|
recordIndexId={recordIndexId}
|
||||||
|
objectNamePlural={objectNamePlural}
|
||||||
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<StyledIndexContainer>
|
<StyledIndexContainer>
|
||||||
<RecordIndexContainer
|
<RecordIndexContainer
|
||||||
|
|||||||
Reference in New Issue
Block a user