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 { 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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<PageContainer>
|
||||
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
||||
<RecordIndexPageHeader createRecord={handleAddButtonClick} />
|
||||
<RecordIndexPageHeader
|
||||
createRecord={handleAddButtonClick}
|
||||
recordIndexId={recordIndexId}
|
||||
objectNamePlural={objectNamePlural}
|
||||
/>
|
||||
<PageBody>
|
||||
<StyledIndexContainer>
|
||||
<RecordIndexContainer
|
||||
|
||||
Reference in New Issue
Block a user