Added "Add record" button in kanban view column headers dropdown (#6649)

Closes #4629 
Refactored `RecordBoardColumnNewOpportunityButton` and
`RecordBoardColumnNewButton` to use the same logic in dropdown.

I kept those hooks inside `record-board-column` where these buttons are.
Let me know if it should be placed somewhere else.

Also Added navigation state preservation when clicked on `edit from
settings`

Thanks :)

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
nitin
2024-08-28 19:46:37 +05:30
committed by GitHub
parent ff1adb06b2
commit e2eaffcf53
9 changed files with 212 additions and 103 deletions

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useRef } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useContext, useRef } from 'react';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';

View File

@ -1,11 +1,16 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { IconDotsVertical, Tag } from 'twenty-ui'; import { IconDotsVertical, IconPlus, Tag } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useAddNewOpportunity } from '@/object-record/record-board/record-board-column/hooks/useAddNewOpportunity';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
@ -38,11 +43,25 @@ const StyledHeaderActions = styled.div`
display: flex; display: flex;
margin-left: auto; margin-left: auto;
`; `;
const StyledHeaderContainer = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;
const StyledLeftContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledRightContainer = styled.div`
align-items: center;
display: flex;
`;
export const RecordBoardColumnHeader = () => { export const RecordBoardColumnHeader = () => {
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false); const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem } = useContext(RecordBoardContext);
const { columnDefinition, recordCount } = useContext( const { columnDefinition, recordCount } = useContext(
RecordBoardColumnContext, RecordBoardColumnContext,
); );
@ -69,42 +88,75 @@ export const RecordBoardColumnHeader = () => {
const boardColumnTotal = 0; const boardColumnTotal = 0;
const {
isCreatingCard,
handleAddNewOpportunityClick,
handleCancel,
handleEntitySelect,
} = useAddNewOpportunity('first');
const { handleAddNewCardClick } = useAddNewCard('first');
const isOpportunity =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity;
const handleClick = isOpportunity
? handleAddNewOpportunityClick
: () => {
handleAddNewCardClick();
};
return ( return (
<> <>
<StyledHeader <StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)} onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)} onMouseLeave={() => setIsHeaderHovered(false)}
> >
<Tag <StyledHeaderContainer>
onClick={handleBoardColumnMenuOpen} <StyledLeftContainer>
variant={ <Tag
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? 'solid'
: 'outline'
}
color={
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? columnDefinition.color
: 'transparent'
}
text={columnDefinition.title}
weight={
columnDefinition.type === RecordBoardColumnDefinitionType.Value
? 'regular'
: 'medium'
}
/>
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
<StyledNumChildren>{recordCount}</StyledNumChildren>
{isHeaderHovered && columnDefinition.actions.length > 0 && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen} onClick={handleBoardColumnMenuOpen}
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'
}
/> />
</StyledHeaderActions> {!!boardColumnTotal && (
)} <StyledAmount>${boardColumnTotal}</StyledAmount>
)}
<StyledNumChildren>{recordCount}</StyledNumChildren>
</StyledLeftContainer>
<StyledRightContainer>
{isHeaderHovered && (
<StyledHeaderActions>
{columnDefinition.actions.length > 0 && (
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
)}
<LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={handleClick}
/>
</StyledHeaderActions>
)}
</StyledRightContainer>
</StyledHeaderContainer>
</StyledHeader> </StyledHeader>
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && ( {isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
<RecordBoardColumnDropdownMenu <RecordBoardColumnDropdownMenu
@ -112,6 +164,16 @@ export const RecordBoardColumnHeader = () => {
stageId={columnDefinition.id} stageId={columnDefinition.id}
/> />
)} )}
{isCreatingCard && (
<SingleEntitySelect
disableBackgroundBlur
onCancel={handleCancel}
onEntitySelected={handleEntitySelect}
relationObjectNameSingular={CoreObjectNameSingular.Company}
relationPickerScopeId="relation-picker"
selectedRelationRecordIds={[]}
/>
)}
</> </>
); );
}; };

View File

@ -1,10 +1,8 @@
import { useContext } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
const StyledButton = styled.button` const StyledButton = styled.button`
align-items: center; align-items: center;
@ -25,19 +23,9 @@ const StyledButton = styled.button`
export const RecordBoardColumnNewButton = () => { export const RecordBoardColumnNewButton = () => {
const theme = useTheme(); const theme = useTheme();
const { columnDefinition } = useContext(RecordBoardColumnContext); const { handleAddNewCardClick } = useAddNewCard('last');
const { createOneRecord, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const onNewClick = () => {
createOneRecord({
[selectFieldMetadataItem.name]: columnDefinition.value,
position: 'last',
});
};
return ( return (
<StyledButton onClick={onNewClick}> <StyledButton onClick={handleAddNewCardClick}>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
New New
</StyledButton> </StyledButton>

View File

@ -1,16 +1,10 @@
import { useCallback, useContext, useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useAddNewOpportunity } from '@/object-record/record-board/record-board-column/hooks/useAddNewOpportunity';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; 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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
const StyledButton = styled.button` const StyledButton = styled.button`
align-items: center; align-items: center;
@ -30,52 +24,13 @@ const StyledButton = styled.button`
`; `;
export const RecordBoardColumnNewOpportunityButton = () => { export const RecordBoardColumnNewOpportunityButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const theme = useTheme(); const theme = useTheme();
const { columnDefinition } = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const { const {
goBackToPreviousHotkeyScope, isCreatingCard,
setHotkeyScopeAndMemorizePreviousScope, handleAddNewOpportunityClick,
} = usePreviousHotkeyScope(); handleCancel,
handleEntitySelect,
const { resetSearchFilter } = useEntitySelectSearch({ } = useAddNewOpportunity('last');
relationPickerScopeId: 'relation-picker',
});
const handleEntitySelect = (company?: EntityForSelect) => {
setIsCreatingCard(false);
goBackToPreviousHotkeyScope();
resetSearchFilter();
if (!company) {
return;
}
createOneRecord({
name: company.name,
companyId: company.id,
position: 'last',
[selectFieldMetadataItem.name]: columnDefinition.value,
});
};
const handleNewClick = useCallback(() => {
setIsCreatingCard(true);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}, [setIsCreatingCard, setHotkeyScopeAndMemorizePreviousScope]);
const handleCancel = () => {
resetSearchFilter();
goBackToPreviousHotkeyScope();
setIsCreatingCard(false);
};
return ( return (
<> <>
{isCreatingCard ? ( {isCreatingCard ? (
@ -88,7 +43,7 @@ export const RecordBoardColumnNewOpportunityButton = () => {
selectedRelationRecordIds={[]} selectedRelationRecordIds={[]}
/> />
) : ( ) : (
<StyledButton onClick={handleNewClick}> <StyledButton onClick={handleAddNewOpportunityClick}>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
New New
</StyledButton> </StyledButton>

View File

@ -0,0 +1,20 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useContext } from 'react';
export const useAddNewCard = (position: string) => {
const { columnDefinition } = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const handleAddNewCardClick = () => {
createOneRecord({
[selectFieldMetadataItem.name]: columnDefinition.value,
position: position,
});
};
return {
handleAddNewCardClick,
};
};

View File

@ -0,0 +1,68 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useCallback, useContext, useState } from 'react';
export const useAddNewOpportunity = (position: string) => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const { columnDefinition } = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem } =
useContext(RecordBoardContext);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const { resetSearchFilter } = useEntitySelectSearch({
relationPickerScopeId: 'relation-picker',
});
const handleEntitySelect = useCallback(
(company?: EntityForSelect) => {
setIsCreatingCard(false);
goBackToPreviousHotkeyScope();
resetSearchFilter();
if (company !== undefined) {
createOneRecord({
name: company.name,
companyId: company.id,
position: position,
[selectFieldMetadataItem.name]: columnDefinition.value,
});
}
},
[
columnDefinition,
createOneRecord,
goBackToPreviousHotkeyScope,
resetSearchFilter,
selectFieldMetadataItem,
position,
],
);
const handleAddNewOpportunityClick = useCallback(() => {
setIsCreatingCard(true);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}, [setHotkeyScopeAndMemorizePreviousScope]);
const handleCancel = useCallback(() => {
resetSearchFilter();
goBackToPreviousHotkeyScope();
setIsCreatingCard(false);
}, [goBackToPreviousHotkeyScope, resetSearchFilter]);
return {
isCreatingCard,
handleEntitySelect,
handleAddNewOpportunityClick,
handleCancel,
};
};

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@ -11,6 +11,7 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata'; import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -60,9 +61,21 @@ export const RecordIndexBoardDataLoaderEffect = ({
}, [recordIndexFieldDefinitions, setFieldDefinitions]); }, [recordIndexFieldDefinitions, setFieldDefinitions]);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
const navigateToSelectSettings = useCallback(() => { const navigateToSelectSettings = useCallback(() => {
setNavigationMemorizedUrl(location.pathname + location.search);
navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`); navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`);
}, [navigate, objectMetadataItem]); }, [
navigate,
objectMetadataItem,
location.pathname,
location.search,
setNavigationMemorizedUrl,
]);
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);

View File

@ -1,4 +1,4 @@
import { IconPencil } from 'twenty-ui'; import { IconSettings } from 'twenty-ui';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { import {
@ -42,8 +42,8 @@ export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
actions: [ actions: [
{ {
id: 'edit', id: 'edit',
label: 'Edit from settings', label: 'Edit from Settings',
icon: IconPencil, icon: IconSettings,
position: 0, position: 0,
callback: navigateToSelectSettings, callback: navigateToSelectSettings,
}, },

View File

@ -33,16 +33,19 @@ export default defineConfig(({ command, mode }) => {
}; };
if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') { if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') {
// eslint-disable-next-line no-console
console.log( console.log(
`VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`, `VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`,
); );
} }
if (VITE_DISABLE_ESLINT_CHECKER === 'true') { if (VITE_DISABLE_ESLINT_CHECKER === 'true') {
// eslint-disable-next-line no-console
console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`); console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`);
} }
if (VITE_BUILD_SOURCEMAP === 'true') { if (VITE_BUILD_SOURCEMAP === 'true') {
// eslint-disable-next-line no-console
console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`); console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`);
} }