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:
@ -1,5 +1,5 @@
|
||||
import { useCallback, useContext, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useContext, useRef } from 'react';
|
||||
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
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 { 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 { 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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
|
||||
@ -38,11 +43,25 @@ const StyledHeaderActions = styled.div`
|
||||
display: flex;
|
||||
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 = () => {
|
||||
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
||||
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
|
||||
|
||||
const { objectMetadataItem } = useContext(RecordBoardContext);
|
||||
const { columnDefinition, recordCount } = useContext(
|
||||
RecordBoardColumnContext,
|
||||
);
|
||||
@ -69,42 +88,75 @@ export const RecordBoardColumnHeader = () => {
|
||||
|
||||
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 (
|
||||
<>
|
||||
<StyledHeader
|
||||
onMouseEnter={() => setIsHeaderHovered(true)}
|
||||
onMouseLeave={() => setIsHeaderHovered(false)}
|
||||
>
|
||||
<Tag
|
||||
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'
|
||||
}
|
||||
/>
|
||||
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
|
||||
<StyledNumChildren>{recordCount}</StyledNumChildren>
|
||||
{isHeaderHovered && columnDefinition.actions.length > 0 && (
|
||||
<StyledHeaderActions>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
<StyledHeaderContainer>
|
||||
<StyledLeftContainer>
|
||||
<Tag
|
||||
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>
|
||||
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
|
||||
<RecordBoardColumnDropdownMenu
|
||||
@ -112,6 +164,16 @@ export const RecordBoardColumnHeader = () => {
|
||||
stageId={columnDefinition.id}
|
||||
/>
|
||||
)}
|
||||
{isCreatingCard && (
|
||||
<SingleEntitySelect
|
||||
disableBackgroundBlur
|
||||
onCancel={handleCancel}
|
||||
onEntitySelected={handleEntitySelect}
|
||||
relationObjectNameSingular={CoreObjectNameSingular.Company}
|
||||
relationPickerScopeId="relation-picker"
|
||||
selectedRelationRecordIds={[]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { useContext } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
align-items: center;
|
||||
@ -25,19 +23,9 @@ const StyledButton = styled.button`
|
||||
|
||||
export const RecordBoardColumnNewButton = () => {
|
||||
const theme = useTheme();
|
||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||
const { createOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
|
||||
const onNewClick = () => {
|
||||
createOneRecord({
|
||||
[selectFieldMetadataItem.name]: columnDefinition.value,
|
||||
position: 'last',
|
||||
});
|
||||
};
|
||||
|
||||
const { handleAddNewCardClick } = useAddNewCard('last');
|
||||
return (
|
||||
<StyledButton onClick={onNewClick}>
|
||||
<StyledButton onClick={handleAddNewCardClick}>
|
||||
<IconPlus size={theme.icon.size.md} />
|
||||
New
|
||||
</StyledButton>
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useAddNewOpportunity } from '@/object-record/record-board/record-board-column/hooks/useAddNewOpportunity';
|
||||
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`
|
||||
align-items: center;
|
||||
@ -30,52 +24,13 @@ const StyledButton = styled.button`
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnNewOpportunityButton = () => {
|
||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||
const { createOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const { resetSearchFilter } = useEntitySelectSearch({
|
||||
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);
|
||||
};
|
||||
|
||||
isCreatingCard,
|
||||
handleAddNewOpportunityClick,
|
||||
handleCancel,
|
||||
handleEntitySelect,
|
||||
} = useAddNewOpportunity('last');
|
||||
return (
|
||||
<>
|
||||
{isCreatingCard ? (
|
||||
@ -88,7 +43,7 @@ export const RecordBoardColumnNewOpportunityButton = () => {
|
||||
selectedRelationRecordIds={[]}
|
||||
/>
|
||||
) : (
|
||||
<StyledButton onClick={handleNewClick}>
|
||||
<StyledButton onClick={handleAddNewOpportunityClick}>
|
||||
<IconPlus size={theme.icon.size.md} />
|
||||
New
|
||||
</StyledButton>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
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 { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
@ -60,9 +61,21 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const navigateToSelectSettings = useCallback(() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`);
|
||||
}, [navigate, objectMetadataItem]);
|
||||
}, [
|
||||
navigate,
|
||||
objectMetadataItem,
|
||||
location.pathname,
|
||||
location.search,
|
||||
setNavigationMemorizedUrl,
|
||||
]);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IconPencil } from 'twenty-ui';
|
||||
import { IconSettings } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
@ -42,8 +42,8 @@ export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
|
||||
actions: [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit from settings',
|
||||
icon: IconPencil,
|
||||
label: 'Edit from Settings',
|
||||
icon: IconSettings,
|
||||
position: 0,
|
||||
callback: navigateToSelectSettings,
|
||||
},
|
||||
|
||||
@ -33,16 +33,19 @@ export default defineConfig(({ command, mode }) => {
|
||||
};
|
||||
|
||||
if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (VITE_DISABLE_ESLINT_CHECKER === 'true') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`);
|
||||
}
|
||||
|
||||
if (VITE_BUILD_SOURCEMAP === 'true') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user