Deprecate old board (#4352)

* Deprecate old board

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet 2024-03-07 10:02:45 +01:00 committed by GitHub
parent 4f4ce1c655
commit 9190bd8d7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 18 additions and 4941 deletions

View File

@ -1,24 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { CompanyBoard } from '../board/components/CompanyBoard';
const DoNotRenderEffect = () => <></>;
const meta: Meta<typeof CompanyBoard> = {
title: 'Modules/Companies/Board',
component: DoNotRenderEffect,
decorators: [ComponentWithRouterDecorator, SnackBarDecorator],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof CompanyBoard>;
// FIXME: CompanyBoard is re-rendering so much and exceeding the maximum update depth for some reason.
export const OneColumnBoard: Story = {};

View File

@ -1,30 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { CompanyChip } from '../components/CompanyChip';
const meta: Meta<typeof CompanyChip> = {
title: 'Modules/Companies/CompanyChip',
component: CompanyChip,
decorators: [ComponentWithRouterDecorator],
};
export default meta;
type Story = StoryObj<typeof CompanyChip>;
export const SmallName: Story = {
args: {
opportunityId: 'airbnb',
companyName: 'Airbnb',
avatarUrl: 'https://api.faviconkit.com/airbnb.com/144',
},
};
export const BigName: Story = {
args: {
opportunityId: 'google',
companyName: 'Google with a real big name to overflow the cell',
avatarUrl: 'https://api.faviconkit.com/google.com/144',
},
};

View File

@ -1,34 +0,0 @@
import { PipelineStep } from '@/pipeline/types/PipelineStep';
export const pipelineSteps = [
{
id: 'pipeline-stage-1',
name: 'New',
position: 0,
color: 'red',
},
{
id: 'pipeline-stage-2',
name: 'Screening',
position: 1,
color: 'purple',
},
{
id: 'pipeline-stage-3',
name: 'Meeting',
position: 2,
color: 'sky',
},
{
id: 'pipeline-stage-4',
name: 'Proposal',
position: 3,
color: 'turquoise',
},
{
id: 'pipeline-stage-5',
name: 'Customer',
position: 4,
color: 'yellow',
},
] as PipelineStep[];

View File

@ -1,92 +0,0 @@
import { useCallback } from 'react';
import styled from '@emotion/styled';
import { mapBoardFieldDefinitionsToViewFields } from '@/companies/utils/mapBoardFieldDefinitionsToViewFields';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import {
RecordBoardDeprecated,
RecordBoardDeprecatedProps,
} from '@/object-record/record-board-deprecated/components/RecordBoardDeprecated';
import { RecordBoardDeprecatedEffect } from '@/object-record/record-board-deprecated/components/RecordBoardDeprecatedEffect';
import { BOARD_OPTIONS_DROPDOWN_ID } from '@/object-record/record-board-deprecated/constants/BoardOptionsDropdownId';
import { RecordBoardDeprecatedOptionsDropdown } from '@/object-record/record-board-deprecated/options/components/RecordBoardDeprecatedOptionsDropdown';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { ViewBar } from '@/views/components/ViewBar';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { HooksCompanyBoardEffect } from '../../components/HooksCompanyBoardEffect';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
width: 100%;
`;
type CompanyBoardProps = Pick<
RecordBoardDeprecatedProps,
'onColumnAdd' | 'onColumnDelete' | 'onEditColumnTitle'
>;
export const CompanyBoard = ({
onColumnAdd,
onColumnDelete,
onEditColumnTitle,
}: CompanyBoardProps) => {
const viewBarId = 'company-board-view';
const recordBoardId = 'company-board';
const { persistViewFields } = useViewFields(viewBarId);
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: 'pipelineStep',
});
const onStageAdd = useCallback(
(stage: BoardColumnDefinition) => {
createOneRecord({
name: stage.title,
color: stage.colorCode,
position: stage.position,
id: stage.id,
});
},
[createOneRecord],
);
return (
<StyledContainer>
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={
<RecordBoardDeprecatedOptionsDropdown
recordBoardId={recordBoardId}
onStageAdd={onStageAdd}
/>
}
optionsDropdownScopeId={BOARD_OPTIONS_DROPDOWN_ID}
/>
<HooksCompanyBoardEffect
viewBarId={viewBarId}
recordBoardId={recordBoardId}
/>
<RecordBoardDeprecatedEffect
recordBoardId={recordBoardId}
onFieldsChange={(fields) => {
persistViewFields(mapBoardFieldDefinitionsToViewFields(fields));
}}
/>
<RecordBoardDeprecated
recordBoardId={recordBoardId}
boardOptions={opportunitiesBoardOptions}
onColumnAdd={onColumnAdd}
onColumnDelete={onColumnDelete}
onEditColumnTitle={onEditColumnTitle}
/>
</StyledContainer>
);
};

View File

@ -1,255 +0,0 @@
import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { BoardCardIdContext } from '@/object-record/record-board-deprecated/contexts/BoardCardIdContext';
import { useCurrentRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useCurrentRecordBoardDeprecatedCardSelectedInternal';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { isRecordBoardDeprecatedCardInCompactViewFamilyState } from '@/object-record/record-board-deprecated/states/isRecordBoardDeprecatedCardInCompactViewFamilyState';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { EntityChipVariant } from '@/ui/display/chip/components/EntityChip';
import { IconEye } from '@/ui/display/icon/index';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { getLogoUrlFromDomainName } from '~/utils';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
import { CompanyChip } from './CompanyChip';
const StyledBoardCard = styled.div<{ selected: boolean }>`
background-color: ${({ theme, selected }) =>
selected ? theme.accent.quaternary : theme.background.secondary};
border: 1px solid
${({ theme, selected }) =>
selected ? theme.accent.secondary : theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.font.color.primary};
&:hover {
background-color: ${({ theme, selected }) =>
selected && theme.accent.tertiary};
border: 1px solid
${({ theme, selected }) =>
selected ? theme.accent.primary : theme.border.color.medium};
}
cursor: pointer;
.checkbox-container {
transition: all ease-in-out 160ms;
opacity: ${({ selected }) => (selected ? 1 : 0)};
}
&:hover .checkbox-container {
opacity: 1;
}
.compact-icon-container {
transition: all ease-in-out 160ms;
opacity: 0;
}
&:hover .compact-icon-container {
opacity: 1;
}
`;
const StyledBoardCardWrapper = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledBoardCardHeader = styled.div<{
showCompactView: boolean;
}>`
align-items: center;
display: flex;
flex-direction: row;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: 24px;
padding-bottom: ${({ theme, showCompactView }) =>
theme.spacing(showCompactView ? 0 : 1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
transition: padding ease-in-out 160ms;
img {
height: ${({ theme }) => theme.icon.size.md}px;
margin-right: ${({ theme }) => theme.spacing(2)};
object-fit: cover;
width: ${({ theme }) => theme.icon.size.md}px;
}
`;
const StyledBoardCardBody = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(2.5)};
padding-right: ${({ theme }) => theme.spacing(2)};
span {
align-items: center;
display: flex;
flex-direction: row;
svg {
color: ${({ theme }) => theme.font.color.tertiary};
margin-right: ${({ theme }) => theme.spacing(2)};
}
}
`;
const StyledCheckboxContainer = styled.div`
display: flex;
flex: 1;
justify-content: end;
`;
const StyledFieldContainer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
`;
const StyledCompactIconContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
`;
export const CompanyBoardCard = () => {
const { isCurrentCardSelected, setCurrentCardSelected } =
useCurrentRecordBoardDeprecatedCardSelectedInternal();
const boardCardId = useContext(BoardCardIdContext);
const [companyProgress] = useRecoilState(
companyProgressesFamilyState(boardCardId ?? ''),
);
const { isCompactViewEnabledState, visibleBoardCardFieldsSelector } =
useRecordBoardDeprecatedScopedStates();
const [isCompactViewEnabled] = useRecoilState(isCompactViewEnabledState);
const [isCardInCompactView, setIsCardInCompactView] = useRecoilState(
isRecordBoardDeprecatedCardInCompactViewFamilyState(boardCardId ?? ''),
);
const showCompactView = isCompactViewEnabled && isCardInCompactView;
const { opportunity, company } = companyProgress ?? {};
const visibleBoardCardFields = useRecoilValue(visibleBoardCardFieldsSelector);
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
const { updateOneRecord: updateOneOpportunity } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneOpportunity?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
// boardCardId check can be moved to a wrapper to avoid unnecessary logic above
if (!company || !opportunity || !boardCardId) {
return null;
}
const PreventSelectOnClickContainer = ({
children,
}: {
children: ReactNode;
}) => (
<StyledFieldContainer
onClick={(e) => {
e.stopPropagation();
}}
>
{children}
</StyledFieldContainer>
);
const OnMouseLeaveBoard = () => {
setIsCardInCompactView(true);
};
return (
<StyledBoardCardWrapper>
<StyledBoardCard
selected={isCurrentCardSelected}
onMouseLeave={OnMouseLeaveBoard}
onClick={() => setCurrentCardSelected(!isCurrentCardSelected)}
>
<StyledBoardCardHeader showCompactView={showCompactView}>
<CompanyChip
opportunityId={opportunity.id}
companyName={company.name}
avatarUrl={getLogoUrlFromDomainName(company.domainName)}
variant={EntityChipVariant.Transparent}
/>
{showCompactView && (
<StyledCompactIconContainer className="compact-icon-container">
<LightIconButton
Icon={IconEye}
accent="tertiary"
onClick={(e) => {
e.stopPropagation();
setIsCardInCompactView(false);
}}
/>
</StyledCompactIconContainer>
)}
<StyledCheckboxContainer className="checkbox-container">
<Checkbox
checked={isCurrentCardSelected}
onChange={() => setCurrentCardSelected(!isCurrentCardSelected)}
variant={CheckboxVariant.Secondary}
/>
</StyledCheckboxContainer>
</StyledBoardCardHeader>
<StyledBoardCardBody>
<AnimatedEaseInOut isOpen={!showCompactView} initial={false}>
{visibleBoardCardFields.map((viewField) => (
<PreventSelectOnClickContainer key={viewField.fieldMetadataId}>
<FieldContext.Provider
value={{
entityId: boardCardId,
maxWidth: 156,
recoilScopeId: boardCardId + viewField.fieldMetadataId,
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: viewField.fieldMetadataId,
label: viewField.label,
iconName: viewField.iconName,
type: viewField.type,
metadata: viewField.metadata,
},
useUpdateRecord: useUpdateOneRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
</PreventSelectOnClickContainer>
))}
</AnimatedEaseInOut>
</StyledBoardCardBody>
</StyledBoardCard>
</StyledBoardCardWrapper>
);
};

View File

@ -1,27 +0,0 @@
import {
EntityChip,
EntityChipVariant,
} from '@/ui/display/chip/components/EntityChip';
type CompanyChipProps = {
opportunityId: string;
companyName: string;
avatarUrl?: string;
variant?: EntityChipVariant;
};
export const CompanyChip = ({
opportunityId,
companyName,
avatarUrl,
variant = EntityChipVariant.Regular,
}: CompanyChipProps) => (
<EntityChip
entityId={opportunityId}
linkToEntity={`/object/opportunity/${opportunityId}`}
name={companyName}
avatarType="squared"
avatarUrl={avatarUrl}
variant={variant}
/>
);

View File

@ -1,134 +0,0 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { availableRecordBoardDeprecatedCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/availableRecordBoardDeprecatedCardFieldsScopedState';
import { recordBoardCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedCardFieldsScopedState';
import { recordBoardFiltersScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedFiltersScopedState';
import { recordBoardSortsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedSortsScopedState';
import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
import { useViewBar } from '@/views/hooks/useViewBar';
import { mapViewFieldsToBoardFieldDefinitions } from '@/views/utils/mapViewFieldsToBoardFieldDefinitions';
type HooksCompanyBoardEffectProps = {
viewBarId: string;
recordBoardId: string;
};
export const HooksCompanyBoardEffect = ({
viewBarId,
recordBoardId,
}: HooksCompanyBoardEffectProps) => {
const {
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
setAvailableFieldDefinitions,
setViewObjectMetadataId,
} = useViewBar({ viewBarId });
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const { columnDefinitions, filterDefinitions, sortDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const setAvailableBoardCardFields = useSetRecoilScopedStateV2(
availableRecordBoardDeprecatedCardFieldsScopedState,
'company-board',
);
useEffect(() => {
if (!objectMetadataItem) {
return;
}
setAvailableFilterDefinitions?.(filterDefinitions);
setAvailableSortDefinitions?.(sortDefinitions);
setAvailableFieldDefinitions?.(columnDefinitions);
}, [
columnDefinitions,
filterDefinitions,
objectMetadataItem,
setAvailableFieldDefinitions,
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
sortDefinitions,
]);
useEffect(() => {
setAvailableBoardCardFields(columnDefinitions);
}, [columnDefinitions, setAvailableBoardCardFields]);
useEffect(() => {
if (!objectMetadataItem) {
return;
}
setViewObjectMetadataId?.(objectMetadataItem.id);
}, [objectMetadataItem, setViewObjectMetadataId]);
const {
currentViewFieldsState,
currentViewFiltersState,
currentViewSortsState,
} = useViewScopedStates({ viewScopeId: viewBarId });
const currentViewFields = useRecoilValue(currentViewFieldsState);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
const currentViewSorts = useRecoilValue(currentViewSortsState);
//TODO: Modify to use scopeId
const setBoardCardFields = useSetRecoilScopedStateV2(
recordBoardCardFieldsScopedState,
'company-board',
);
const setBoardCardFilters = useSetRecoilScopedStateV2(
recordBoardFiltersScopedState,
'company-board',
);
const setBoardCardSorts = useSetRecoilScopedStateV2(
recordBoardSortsScopedState,
'company-board',
);
useEffect(() => {
if (currentViewFields) {
setBoardCardFields(
mapViewFieldsToBoardFieldDefinitions(
currentViewFields,
columnDefinitions,
),
);
}
}, [columnDefinitions, currentViewFields, setBoardCardFields]);
useEffect(() => {
if (currentViewFilters) {
setBoardCardFilters(currentViewFilters);
}
}, [currentViewFilters, setBoardCardFilters]);
useEffect(() => {
if (currentViewSorts) {
setBoardCardSorts(currentViewSorts);
}
}, [currentViewSorts, setBoardCardSorts]);
const { setEntityCountInCurrentView } = useViewBar({ viewBarId });
const { savedOpportunitiesState } = useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: recordBoardId,
});
const savedOpportunities = useRecoilValue(savedOpportunitiesState);
useEffect(() => {
setEntityCountInCurrentView(savedOpportunities.length);
}, [savedOpportunities.length, setEntityCountInCurrentView]);
return <></>;
};

View File

@ -1,69 +0,0 @@
import { useCallback, useContext, useState } from 'react';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { NewButton } from '@/object-record/record-board-deprecated/components/NewButton';
import { BoardColumnContext } from '@/object-record/record-board-deprecated/contexts/BoardColumnContext';
import { useCreateOpportunity } from '@/object-record/record-board-deprecated/hooks/internal/useCreateOpportunity';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
export const NewOpportunityButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const column = useContext(BoardColumnContext);
const pipelineStepId = column?.columnDefinition.id || '';
const { enqueueSnackBar } = useSnackBar();
const createOpportunity = useCreateOpportunity();
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleEntitySelect = (company: any) => {
setIsCreatingCard(false);
goBackToPreviousHotkeyScope();
if (!pipelineStepId) {
enqueueSnackBar('Pipeline stage id is not defined', {
variant: 'error',
});
throw new Error('Pipeline stage id is not defined');
}
createOpportunity(company.id, pipelineStepId);
};
const handleNewClick = useCallback(() => {
setIsCreatingCard(true);
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}, [setIsCreatingCard, setHotkeyScopeAndMemorizePreviousScope]);
const handleCancel = () => {
goBackToPreviousHotkeyScope();
setIsCreatingCard(false);
};
return (
<>
{isCreatingCard ? (
<SingleEntitySelect
disableBackgroundBlur
onCancel={handleCancel}
onEntitySelected={handleEntitySelect}
relationObjectNameSingular={CoreObjectNameSingular.Company}
relationPickerScopeId="relation-picker"
selectedRelationRecordIds={[]}
/>
) : (
<NewButton onClick={handleNewClick} />
)}
</>
);
};

View File

@ -1,102 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
import { IconChevronDown } from '@/ui/display/icon';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
export type OpportunityPickerProps = {
companyId: string | null;
onSubmit: (
newCompany: EntityForSelect | null,
newPipelineStepId: string | null,
) => void;
onCancel?: () => void;
};
export const OpportunityPicker = ({
onSubmit,
onCancel,
}: OpportunityPickerProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isProgressSelectionUnfolded, setIsProgressSelectionUnfolded] =
useState(false);
const [selectedPipelineStepId, setSelectedPipelineStepId] = useState<
string | null
>(null);
const currentPipelineSteps = useRecoilValue(currentPipelineStepsState);
const handlePipelineStepChange = (newPipelineStepId: string) => {
setSelectedPipelineStepId(newPipelineStepId);
setIsProgressSelectionUnfolded(false);
};
const handleEntitySelected = async (
selectedCompany: EntityForSelect | null | undefined,
) => {
onSubmit(selectedCompany ?? null, selectedPipelineStepId);
};
useEffect(() => {
if (currentPipelineSteps?.[0]?.id) {
setSelectedPipelineStepId(currentPipelineSteps?.[0]?.id);
}
}, [currentPipelineSteps]);
const selectedPipelineStep = useMemo(
() =>
currentPipelineSteps.find(
(pipelineStep: any) => pipelineStep.id === selectedPipelineStepId,
),
[currentPipelineSteps, selectedPipelineStepId],
);
return (
<DropdownMenu
ref={containerRef}
data-testid={`company-progress-dropdown-menu`}
>
{isProgressSelectionUnfolded ? (
<DropdownMenuItemsContainer>
{currentPipelineSteps.map((pipelineStep: any, index: number) => (
<MenuItem
key={pipelineStep.id}
testId={`select-pipeline-stage-${index}`}
onClick={() => {
handlePipelineStepChange(pipelineStep.id);
}}
text={pipelineStep.name}
/>
))}
</DropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
testId="selected-pipeline-stage"
EndIcon={IconChevronDown}
onClick={() => setIsProgressSelectionUnfolded(true)}
>
{selectedPipelineStep?.name}
</DropdownMenuHeader>
<DropdownMenuSeparator />
<SingleEntitySelectMenuItemsWithSearch
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
relationObjectNameSingular={CoreObjectNameSingular.Company}
selectedRelationRecordIds={[]}
/>
</>
)}
</DropdownMenu>
);
};

View File

@ -1,3 +0,0 @@
export enum EditableFieldHotkeyScope {
EditableField = 'editable-field',
}

View File

@ -1,11 +0,0 @@
import { atomFamily } from 'recoil';
import { CompanyProgress } from '@/companies/types/CompanyProgress';
export const companyProgressesFamilyState = atomFamily<
CompanyProgress | undefined,
string
>({
key: 'companyProgressesFamilyState',
default: undefined,
});

View File

@ -1,5 +0,0 @@
import { createContext } from 'react';
export const CompanyBoardViewBarRecoilScopeContext = createContext<
string | null
>(null);

View File

@ -1,13 +0,0 @@
import { Company } from '@/companies/types/Company';
import { Opportunity } from '@/pipeline/types/Opportunity';
export type CompanyForBoard = Pick<Company, 'id' | 'name' | 'domainName'>;
export type CompanyProgress = {
company: CompanyForBoard;
opportunity: Opportunity;
};
export type CompanyProgressDict = {
[key: string]: CompanyProgress;
};

View File

@ -1,31 +0,0 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useObjectRecordBoardDeprecated } from '@/object-record/hooks/useObjectRecordBoardDeprecated';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
const recordBoardId = '783932a0-28c7-4607-b2ce-6543fa2be892';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<RecordBoardDeprecatedScope recordBoardScopeId={recordBoardId}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider addTypename={false}>{children}</MockedProvider>
</SnackBarProviderScope>
</RecordBoardDeprecatedScope>
</RecoilRoot>
);
describe('useObjectRecordBoardDeprecated', () => {
it('should skip fetch if currentWorkspace is undefined', async () => {
const { result } = renderHook(() => useObjectRecordBoardDeprecated(), {
wrapper: Wrapper,
});
expect(result.current.loading).toBe(false);
expect(Array.isArray(result.current.opportunities)).toBe(true);
});
});

View File

@ -1,105 +0,0 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { Company } from '@/companies/types/Company';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordBoardDeprecated = () => {
const objectNameSingular = 'opportunity';
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const {
isBoardLoadedState,
boardFiltersState,
boardSortsState,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
} = useRecordBoardDeprecatedScopedStates();
const setIsBoardLoaded = useSetRecoilState(isBoardLoadedState);
const boardFilters = useRecoilValue(boardFiltersState);
const boardSorts = useRecoilValue(boardSortsState);
const setSavedCompanies = useSetRecoilState(savedCompaniesState);
const [savedOpportunities] = useRecoilState(savedOpportunitiesState);
const [savedPipelineSteps, setSavedPipelineSteps] = useRecoilState(
savedPipelineStepsState,
);
const filter = turnObjectDropdownFilterIntoQueryFilter(
boardFilters,
foundObjectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderBy(
boardSorts,
foundObjectMetadataItem?.fields ?? [],
);
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.PipelineStep,
filter,
onCompleted: useCallback(
(data: ObjectRecordConnection<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
},
[setSavedPipelineSteps],
),
});
const {
records: opportunities,
loading,
fetchMoreRecords: fetchMoreOpportunities,
} = useFindManyRecords<Opportunity>({
skip: !savedPipelineSteps.length,
objectNameSingular: CoreObjectNameSingular.Opportunity,
filter,
orderBy,
onCompleted: useCallback(() => {
setIsBoardLoaded(true);
}, [setIsBoardLoaded]),
});
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({
skip: !savedOpportunities.length,
objectNameSingular: CoreObjectNameSingular.Company,
filter: {
id: {
in: savedOpportunities.map(
(opportunity) => opportunity.companyId || '',
),
},
},
onCompleted: useCallback(
(data: ObjectRecordConnection<Company>) => {
setSavedCompanies(data.edges.map((edge) => edge.node));
},
[setSavedCompanies],
),
});
return {
opportunities,
loading,
fetchMoreOpportunities,
fetchMoreCompanies,
};
};

View File

@ -1,16 +0,0 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
export const RecordBoardDeprecatedActionBar = () => {
const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates();
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
if (!selectedCardIds.length) {
return null;
}
return <ActionBar />;
};

View File

@ -1,36 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlus } from '@/ui/display/icon/index';
const StyledButton = styled.button`
align-items: center;
align-self: baseline;
background-color: ${({ theme }) => theme.background.primary};
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(1)};
&:hover {
background-color: ${({ theme }) => theme.background.tertiary};
}
`;
type NewButtonProps = {
onClick: () => void;
};
export const NewButton = ({ onClick }: NewButtonProps) => {
const theme = useTheme();
return (
<StyledButton onClick={onClick}>
<IconPlus size={theme.icon.size.md} />
New
</StyledButton>
);
};

View File

@ -1,162 +0,0 @@
import { useCallback, useRef } from 'react';
import styled from '@emotion/styled';
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordBoardDeprecatedActionBar } from '@/object-record/record-board-deprecated/action-bar/components/RecordBoardDeprecatedActionBar';
import { RecordBoardDeprecatedInternalEffect } from '@/object-record/record-board-deprecated/components/RecordBoardDeprecatedInternalEffect';
import { RecordBoardDeprecatedContextMenu } from '@/object-record/record-board-deprecated/context-menu/components/RecordBoardDeprecatedContextMenu';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { useSetRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useSetRecordBoardDeprecatedCardSelectedInternal';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { logError } from '~/utils/logError';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptions } from '../types/BoardOptions';
import { RecordBoardDeprecatedColumn } from './RecordBoardDeprecatedColumn';
export type RecordBoardDeprecatedProps = {
recordBoardId: string;
boardOptions: BoardOptions;
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (params: {
columnId: string;
title: string;
color: string;
}) => void;
};
const StyledBoard = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
flex: 1;
flex-direction: row;
margin-left: ${({ theme }) => theme.spacing(2)};
margin-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledBoardHeader = styled.div`
position: relative;
z-index: 1;
`;
export const RecordBoardDeprecated = ({
recordBoardId,
boardOptions,
onColumnDelete,
onEditColumnTitle,
}: RecordBoardDeprecatedProps) => {
const recordBoardScopeId = recordBoardId;
const { boardColumnsState } = useRecordBoardDeprecatedScopedStates({
recordBoardScopeId,
});
const boardColumns = useRecoilValue(boardColumnsState);
const { updateOneRecord: updateOneOpportunity } =
useUpdateOneRecord<Opportunity>({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const { unselectAllActiveCards, setCardSelected } =
useSetRecordBoardDeprecatedCardSelectedInternal({ recordBoardScopeId });
const updatePipelineProgressStageInDB = useCallback(
async (pipelineProgressId: string, pipelineStepId: string) => {
await updateOneOpportunity?.({
idToUpdate: pipelineProgressId,
updateOneRecordInput: {
pipelineStepId: pipelineStepId,
},
});
},
[updateOneOpportunity],
);
useListenClickOutsideByClassName({
classNames: ['entity-board-card'],
excludeClassNames: ['action-bar', 'context-menu'],
callback: unselectAllActiveCards,
});
const onDragEnd: OnDragEndResponder = useCallback(
async (result) => {
if (!boardColumns) return;
try {
const draggedEntityId = result.draggableId;
const destinationColumnId = result.destination?.droppableId;
if (
draggedEntityId &&
destinationColumnId &&
updatePipelineProgressStageInDB
) {
await updatePipelineProgressStageInDB(
draggedEntityId,
destinationColumnId,
);
}
} catch (e) {
logError(e);
}
},
[boardColumns, updatePipelineProgressStageInDB],
);
const sortedBoardColumns = [...boardColumns].sort((a, b) => {
return a.position - b.position;
});
const boardRef = useRef<HTMLDivElement>(null);
return (
<RecordBoardDeprecatedScope recordBoardScopeId={recordBoardId}>
<RecordBoardDeprecatedContextMenu />
<RecordBoardDeprecatedActionBar />
<RecordBoardDeprecatedInternalEffect />
<StyledWrapper>
<StyledBoardHeader />
<ScrollWrapper>
<StyledBoard ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}>
{sortedBoardColumns.map((column) => (
<RecordBoardDeprecatedColumn
key={column.id}
recordBoardColumnId={column.id}
columnDefinition={column}
recordBoardColumnTotal={sortedBoardColumns.length}
recordBoardOptions={boardOptions}
onDelete={onColumnDelete}
onTitleEdit={onEditColumnTitle}
/>
))}
</DragDropContext>
</StyledBoard>
</ScrollWrapper>
<DragSelect
dragSelectable={boardRef}
onDragSelectionChange={setCardSelected}
/>
</StyledWrapper>
</RecordBoardDeprecatedScope>
);
};

View File

@ -1,54 +0,0 @@
import { Draggable } from '@hello-pangea/dnd';
import { useSetRecoilState } from 'recoil';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { useCurrentRecordBoardDeprecatedCardSelectedInternal } from '../hooks/internal/useCurrentRecordBoardDeprecatedCardSelectedInternal';
import { BoardOptions } from '../types/BoardOptions';
export const RecordBoardDeprecatedCard = ({
recordBoardOptions,
cardId,
index,
}: {
recordBoardOptions: BoardOptions;
cardId: string;
index: number;
}) => {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
const { setCurrentCardSelected } =
useCurrentRecordBoardDeprecatedCardSelectedInternal();
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setCurrentCardSelected(true);
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
setContextMenuOpenState(true);
};
return (
<Draggable key={cardId} draggableId={cardId} index={index}>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
className="entity-board-card"
data-selectable-id={cardId}
data-select-disable
onContextMenu={handleContextMenu}
>
{<recordBoardOptions.CardComponent />}
</div>
)}
</Draggable>
);
};

View File

@ -1,143 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { RecordBoardDeprecatedCard } from '@/object-record/record-board-deprecated/components/RecordBoardDeprecatedCard';
import { RecordBoardDeprecatedColumnHeader } from '@/object-record/record-board-deprecated/components/RecordBoardDeprecatedColumnHeader';
import { BoardCardIdContext } from '@/object-record/record-board-deprecated/contexts/BoardCardIdContext';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState';
import { BoardOptions } from '../types/BoardOptions';
const StyledPlaceholder = styled.div`
min-height: 1px;
`;
const StyledNewCardButtonContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledColumnCardsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`;
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
background-color: ${({ theme }) => theme.background.primary};
border-left: 1px solid
${({ theme, isFirstColumn }) =>
isFirstColumn ? 'none' : theme.border.color.light};
display: flex;
flex-direction: column;
max-width: 200px;
min-width: 200px;
padding: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
type BoardColumnCardsContainerProps = {
children: React.ReactNode;
droppableProvided: DroppableProvided;
};
type RecordBoardDeprecatedColumnProps = {
recordBoardColumnId: string;
columnDefinition: BoardColumnDefinition;
recordBoardOptions: BoardOptions;
recordBoardColumnTotal: number;
onDelete?: (columnId: string) => void;
onTitleEdit: (params: {
columnId: string;
title: string;
color: string;
}) => void;
};
const BoardColumnCardsContainer = ({
children,
droppableProvided,
}: BoardColumnCardsContainerProps) => {
return (
<StyledColumnCardsContainer
ref={droppableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided?.droppableProps}
>
{children}
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
</StyledColumnCardsContainer>
);
};
export const RecordBoardDeprecatedColumn = ({
recordBoardColumnId,
columnDefinition,
recordBoardOptions,
recordBoardColumnTotal,
onDelete,
onTitleEdit,
}: RecordBoardDeprecatedColumnProps) => {
const cardIds = useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId),
);
const isFirstColumn = columnDefinition.position === 0;
return (
<BoardColumnContext.Provider
value={{
id: recordBoardColumnId,
columnDefinition: columnDefinition,
isFirstColumn: columnDefinition.position === 0,
isLastColumn: columnDefinition.position === recordBoardColumnTotal - 1,
onTitleEdit: ({ title, color }) =>
onTitleEdit({ columnId: recordBoardColumnId, title, color }),
}}
>
<Droppable droppableId={recordBoardColumnId}>
{(droppableProvided) => (
<StyledColumn isFirstColumn={isFirstColumn}>
<RecordBoardDeprecatedColumnHeader
recordBoardColumnId={recordBoardColumnId}
columnDefinition={columnDefinition}
onDelete={onDelete}
/>
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
{cardIds.map((cardId, index) => (
<BoardCardIdContext.Provider value={cardId} key={cardId}>
<RecordBoardDeprecatedCard
index={index}
cardId={cardId}
recordBoardOptions={recordBoardOptions}
/>
</BoardCardIdContext.Provider>
))}
<Draggable
draggableId={`new-${recordBoardColumnId}`}
index={cardIds.length}
isDragDisabled={true}
>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
>
<StyledNewCardButtonContainer>
{recordBoardOptions.newCardComponent}
</StyledNewCardButtonContainer>
</div>
)}
</Draggable>
</BoardColumnCardsContainer>
</StyledColumn>
)}
</Droppable>
</BoardColumnContext.Provider>
);
};

View File

@ -1,149 +0,0 @@
import { useCallback, useContext, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { IconArrowLeft, IconArrowRight, IconPencil } from '@/ui/display/icon';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { useBoardColumnsInternal } from '../hooks/internal/useRecordBoardDeprecatedColumnsInternal';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { RecordBoardDeprecatedColumnEditTitleMenu } from './RecordBoardDeprecatedColumnEditTitleMenu';
const StyledMenuContainer = styled.div`
position: absolute;
top: ${({ theme }) => theme.spacing(10)};
width: 200px;
z-index: 1;
`;
type RecordBoardDeprecatedColumnDropdownMenuProps = {
onClose: () => void;
onDelete?: (id: string) => void;
stageId: string;
};
type Menu = 'actions' | 'add' | 'title';
export const RecordBoardDeprecatedColumnDropdownMenu = ({
onClose,
onDelete,
stageId,
}: RecordBoardDeprecatedColumnDropdownMenuProps) => {
const [currentMenu, setCurrentMenu] = useState('actions');
const column = useContext(BoardColumnContext);
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
const { handleMoveBoardColumn } = useBoardColumnsInternal();
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const closeMenu = useCallback(() => {
goBackToPreviousHotkeyScope();
onClose();
}, [goBackToPreviousHotkeyScope, onClose]);
const setMenu = (menu: Menu) => {
if (menu === 'add') {
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
}
setCurrentMenu(menu);
};
useListenClickOutside({
refs: [boardColumnMenuRef],
callback: closeMenu,
});
useScopedHotkeys(
[Key.Escape, Key.Enter],
() => {
closeMenu();
},
BoardColumnHotkeyScope.BoardColumn,
[],
);
if (!column) return <></>;
const { isFirstColumn, isLastColumn, columnDefinition } = column;
const handleColumnMoveLeft = () => {
closeMenu();
if (isFirstColumn) {
return;
}
handleMoveBoardColumn('left', columnDefinition);
};
const handleColumnMoveRight = () => {
closeMenu();
if (isLastColumn) {
return;
}
handleMoveBoardColumn('right', columnDefinition);
};
return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<DropdownMenu data-select-disable>
{currentMenu === 'actions' && (
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => setMenu('title')}
LeftIcon={IconPencil}
text="Edit"
/>
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
{/* <MenuItem
onClick={() => setMenu('add')}
LeftIcon={IconPlus}
text="New opportunity"
/> */}
</DropdownMenuItemsContainer>
)}
{currentMenu === 'title' && (
<RecordBoardDeprecatedColumnEditTitleMenu
color={columnDefinition.colorCode ?? 'gray'}
onClose={closeMenu}
title={columnDefinition.title}
onDelete={onDelete}
stageId={stageId}
/>
)}
{currentMenu === 'add' && (
<div>add</div>
// <SingleEntitySelect
// disableBackgroundBlur
// entitiesToSelect={companies.entitiesToSelect}
// loading={companies.loading}
// onCancel={closeMenu}
// onEntitySelected={handleCompanySelected}
// selectedEntity={companies.selectedEntities[0]}
// />
)}
</DropdownMenu>
</StyledMenuContainer>
);
};

View File

@ -1,133 +0,0 @@
import { ChangeEvent, useCallback, useContext, useState } from 'react';
import styled from '@emotion/styled';
import debounce from 'lodash.debounce';
import { IconTrash } from '@/ui/display/icon';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
import {
MAIN_COLOR_NAMES,
ThemeColor,
} from '@/ui/theme/constants/MainColorNames';
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { useRecordBoardDeprecated } from '../hooks/useRecordBoardDeprecated';
const StyledEditTitleContainer = styled.div`
--vertical-padding: ${({ theme }) => theme.spacing(1)};
align-items: center;
display: flex;
flex-direction: row;
height: calc(36px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) 0;
width: calc(100%);
`;
const StyledEditModeInput = styled.input`
${TEXT_INPUT_STYLE}
background: ${({ theme }) => theme.background.transparent.lighter};
border-color: ${({ theme }) => theme.color.blue};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-style: solid;
border-width: 1px;
box-shadow: 0px 0px 0px 3px rgba(25, 97, 237, 0.1);
font-size: ${({ theme }) => theme.font.size.sm};
height: 100%;
width: 100%;
`;
type RecordBoardDeprecatedColumnEditTitleMenuProps = {
onClose: () => void;
onDelete?: (id: string) => void;
title: string;
color: ThemeColor;
stageId: string;
};
export const RecordBoardDeprecatedColumnEditTitleMenu = ({
onClose,
onDelete,
stageId,
title,
color,
}: RecordBoardDeprecatedColumnEditTitleMenuProps) => {
const [internalValue, setInternalValue] = useState(title);
const { onTitleEdit } = useContext(BoardColumnContext) || {};
const { setBoardColumns } = useRecordBoardDeprecated({
recordBoardScopeId: 'company-board',
});
const debouncedOnUpdateTitle = debounce(
(newTitle) => onTitleEdit?.({ title: newTitle, color }),
200,
);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const title = event.target.value;
setInternalValue(title);
debouncedOnUpdateTitle(title);
setBoardColumns((previousBoardColumns) =>
previousBoardColumns.map((column) =>
column.id === stageId ? { ...column, title: title } : column,
),
);
};
const handleColorChange = (newColor: ThemeColor) => {
onTitleEdit?.({ title, color: newColor });
onClose();
setBoardColumns((previousBoardColumns) =>
previousBoardColumns.map((column) =>
column.id === stageId
? { ...column, colorCode: newColor ? newColor : 'gray' }
: column,
),
);
};
const handleDelete = useCallback(() => {
setBoardColumns((previousBoardColumns) =>
previousBoardColumns.filter((column) => column.id !== stageId),
);
onDelete?.(stageId);
onClose();
}, [onClose, onDelete, setBoardColumns, stageId]);
return (
<DropdownMenuItemsContainer>
<StyledEditTitleContainer>
<StyledEditModeInput
value={internalValue}
onChange={handleChange}
autoComplete="off"
autoFocus
/>
</StyledEditTitleContainer>
<DropdownMenuSeparator />
{MAIN_COLOR_NAMES.map((colorName) => (
<MenuItemSelectColor
key={colorName}
onClick={() => handleColorChange(colorName)}
color={colorName}
selected={colorName === color}
variant="pipeline"
/>
))}
<DropdownMenuSeparator />
<MenuItem
onClick={handleDelete}
LeftIcon={IconTrash}
text="Delete"
accent="danger"
/>
</DropdownMenuItemsContainer>
);
};

View File

@ -1,129 +0,0 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { recordBoardColumnTotalsFamilySelector } from '@/object-record/record-board-deprecated/states/selectors/recordBoardDeprecatedColumnTotalsFamilySelector';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { IconDotsVertical } from '@/ui/display/icon';
import { Tag } from '@/ui/display/tag/components/Tag';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { RecordBoardDeprecatedColumnDropdownMenu } from './RecordBoardDeprecatedColumnDropdownMenu';
const StyledHeader = styled.div`
align-items: center;
cursor: pointer;
display: flex;
flex-direction: row;
height: 24px;
justify-content: left;
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledAmount = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
margin-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledNumChildren = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.rounded};
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 20px;
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin-left: auto;
width: 16px;
`;
const StyledHeaderActions = styled.div`
display: flex;
margin-left: auto;
`;
type RecordBoardDeprecatedColumnHeaderProps = {
recordBoardColumnId: string;
columnDefinition: BoardColumnDefinition;
onDelete?: (columnId: string) => void;
};
export const RecordBoardDeprecatedColumnHeader = ({
recordBoardColumnId,
columnDefinition,
onDelete,
}: RecordBoardDeprecatedColumnHeaderProps) => {
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleBoardColumnMenuOpen = () => {
setIsBoardColumnMenuOpen(true);
setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, {
goto: false,
});
};
const handleBoardColumnMenuClose = () => {
goBackToPreviousHotkeyScope();
setIsBoardColumnMenuOpen(false);
};
const boardColumnTotal = useRecoilValue(
recordBoardColumnTotalsFamilySelector(recordBoardColumnId),
);
const cardIds = useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId),
);
return (
<>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
>
<Tag
onClick={handleBoardColumnMenuOpen}
color={columnDefinition.colorCode ?? 'gray'}
text={columnDefinition.title}
/>
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
{!isHeaderHovered && (
<StyledNumChildren>{cardIds.length}</StyledNumChildren>
)}
{isHeaderHovered && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
{/* <LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => {}}
/> */}
</StyledHeaderActions>
)}
</StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardDeprecatedColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
onDelete={onDelete}
stageId={recordBoardColumnId}
/>
)}
</>
);
};

View File

@ -1,25 +0,0 @@
import { useEffect } from 'react';
import { useRecordBoardDeprecated } from '@/object-record/record-board-deprecated/hooks/useRecordBoardDeprecated';
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
type RecordBoardDeprecatedEffectProps = {
recordBoardId: string;
onFieldsChange: (fields: BoardFieldDefinition<FieldMetadata>[]) => void;
};
export const RecordBoardDeprecatedEffect = ({
recordBoardId,
onFieldsChange,
}: RecordBoardDeprecatedEffectProps) => {
const { setOnFieldsChange } = useRecordBoardDeprecated({
recordBoardScopeId: recordBoardId,
});
useEffect(() => {
setOnFieldsChange(() => onFieldsChange);
}, [onFieldsChange, setOnFieldsChange]);
return <></>;
};

View File

@ -1,75 +0,0 @@
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useObjectRecordBoardDeprecated } from '@/object-record/hooks/useObjectRecordBoardDeprecated';
import { useRecordBoardDeprecatedActionBarEntriesInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedActionBarEntriesInternal';
import { useRecordBoardDeprecatedContextMenuEntriesInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedContextMenuEntriesInternal';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { useUpdateCompanyBoardColumnsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useUpdateCompanyBoardColumnsInternal';
import { isNonNullable } from '~/utils/isNonNullable';
export type RecordBoardDeprecatedInternalEffectProps = {
onFieldsChange: (fields: any) => void;
};
export const RecordBoardDeprecatedInternalEffect = () => {
const updateCompanyColumnsBoardInternal =
useUpdateCompanyBoardColumnsInternal();
const { setActionBarEntries } =
useRecordBoardDeprecatedActionBarEntriesInternal();
const { setContextMenuEntries } =
useRecordBoardDeprecatedContextMenuEntriesInternal();
const {
savedPipelineStepsState,
savedOpportunitiesState,
savedCompaniesState,
} = useRecordBoardDeprecatedScopedStates();
const { fetchMoreOpportunities, fetchMoreCompanies, opportunities } =
useObjectRecordBoardDeprecated();
const [savedOpportunities, setSavedOpportunities] = useRecoilState(
savedOpportunitiesState,
);
const savedPipelineSteps = useRecoilValue(savedPipelineStepsState);
const savedCompanies = useRecoilValue(savedCompaniesState);
useEffect(() => {
setSavedOpportunities(opportunities);
}, [opportunities, setSavedOpportunities]);
useEffect(() => {
if (isNonNullable(fetchMoreOpportunities)) {
fetchMoreOpportunities();
}
}, [fetchMoreOpportunities]);
useEffect(() => {
if (isNonNullable(fetchMoreCompanies)) {
fetchMoreCompanies();
}
}, [fetchMoreCompanies]);
useEffect(() => {
if (savedOpportunities && savedCompanies) {
setActionBarEntries();
setContextMenuEntries();
updateCompanyColumnsBoardInternal(
savedPipelineSteps,
savedOpportunities,
savedCompanies,
);
}
}, [
savedCompanies,
savedOpportunities,
savedPipelineSteps,
setActionBarEntries,
setContextMenuEntries,
updateCompanyColumnsBoardInternal,
]);
return <></>;
};

View File

@ -1,17 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { RecordBoardDeprecatedColumnEditTitleMenu } from '../RecordBoardDeprecatedColumnEditTitleMenu';
const meta: Meta<typeof RecordBoardDeprecatedColumnEditTitleMenu> = {
title: 'UI/Layout/Board/BoardColumnMenu',
component: RecordBoardDeprecatedColumnEditTitleMenu,
decorators: [ComponentDecorator],
args: { color: 'green', title: 'Column title' },
};
export default meta;
type Story = StoryObj<typeof RecordBoardDeprecatedColumnEditTitleMenu>;
export const AllTags: Story = {};

View File

@ -1 +0,0 @@
export const BOARD_OPTIONS_DROPDOWN_ID = 'board-options-dropdown-id';

View File

@ -1,14 +0,0 @@
import { useRecoilValue } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
export const RecordBoardDeprecatedContextMenu = () => {
const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates();
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
if (!selectedCardIds.length) {
return null;
}
return <ContextMenu />;
};

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const BoardCardIdContext = createContext<string | null>(null);

View File

@ -1,15 +0,0 @@
import { createContext } from 'react';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
type BoardColumnContextProps = {
id: string;
columnDefinition: BoardColumnDefinition;
isFirstColumn: boolean;
isLastColumn: boolean;
onTitleEdit: (params: { title: string; color: string }) => void;
};
export const BoardColumnContext = createContext<BoardColumnContextProps | null>(
null,
);

View File

@ -1,97 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { useRecordBoardDeprecated } from '@/object-record/record-board-deprecated/hooks/useRecordBoardDeprecated';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider>
<RecoilRoot>{children}</RecoilRoot>
</MockedProvider>
);
const recordBoardScopeId = 'recordBoardScopeId';
const renderHookConfig = {
wrapper: Wrapper,
};
const useRecordBoardDeprecatedHook = () => {
const recordBoard = useRecordBoardDeprecated({ recordBoardScopeId });
const { isBoardLoadedState, boardColumnsState, onFieldsChangeState } =
useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: recordBoardScopeId,
});
const isBoardLoaded = useRecoilValue(isBoardLoadedState);
const boardColumns = useRecoilValue(boardColumnsState);
const onFieldsChange = useRecoilValue(onFieldsChangeState);
return {
recordBoard,
isBoardLoaded,
boardColumns,
onFieldsChange,
};
};
describe('useRecordBoardDeprecated', () => {
it('should set isBoardLoadedState', async () => {
const { result } = renderHook(
() => useRecordBoardDeprecatedHook(),
renderHookConfig,
);
act(() => {
result.current.recordBoard.setIsBoardLoaded(true);
});
await waitFor(() => {
expect(result.current.isBoardLoaded).toBe(true);
});
});
it('should set boardColumnsState', async () => {
const columns = [
{
id: '1',
title: '1',
position: 1,
},
{
id: '1',
title: '1',
position: 1,
},
];
const { result } = renderHook(
() => useRecordBoardDeprecatedHook(),
renderHookConfig,
);
act(() => {
result.current.recordBoard.setBoardColumns(columns);
});
await waitFor(() => {
expect(result.current.boardColumns).toEqual(columns);
});
});
it('should set setOnFieldsChange', async () => {
const onFieldsChangeFunction = () => {};
const onFieldsChange = jest.fn(() => onFieldsChangeFunction);
const { result } = renderHook(
() => useRecordBoardDeprecatedHook(),
renderHookConfig,
);
act(() => {
result.current.recordBoard.setOnFieldsChange(onFieldsChange);
});
await waitFor(() => {
expect(result.current.onFieldsChange).toEqual(onFieldsChangeFunction);
});
});
});

View File

@ -1,78 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import gql from 'graphql-tag';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useCreateOpportunity } from '@/object-record/record-board-deprecated/hooks/internal/useCreateOpportunity';
import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board-deprecated/states/recordBoardCardIdsByColumnIdFamilyState';
const mockedUuid = 'mocked-uuid';
jest.mock('uuid', () => ({
v4: () => mockedUuid,
}));
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: () => () => '\n',
}));
const mocks = [
{
request: {
query: gql`
mutation CreateOneOpportunity($input: OpportunityCreateInput!) {
createOpportunity(data: $input) {
id
}
}
`,
variables: {
input: {
id: mockedUuid,
pipelineStepId: 'pipelineStepId',
companyId: 'New Opportunity',
},
},
},
result: jest.fn(() => ({
data: { createOpportunity: { id: '' } },
})),
},
];
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
<RecoilRoot>{children}</RecoilRoot>
</MockedProvider>
);
const renderHookConfig = {
wrapper: Wrapper,
};
describe('useCreateOpportunity', () => {
it('should create opportunity successfully', () => {
const companyIdname = 'New Opportunity';
const opportunityPipelineStepId = 'pipelineStepId';
const { result } = renderHook(
() => ({
createOpportunity: useCreateOpportunity(),
recordBoardCardIdsByColumnId: useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState(opportunityPipelineStepId),
),
}),
renderHookConfig,
);
act(() => {
result.current.createOpportunity(
companyIdname,
opportunityPipelineStepId,
);
});
expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([
mockedUuid,
]);
});
});

View File

@ -1,57 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { BoardCardIdContext } from '@/object-record/record-board-deprecated/contexts/BoardCardIdContext';
import { useCurrentRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useCurrentRecordBoardDeprecatedCardSelectedInternal';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
const scopeId = 'scopeId';
const boardCardId = 'boardCardId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<BoardCardIdContext.Provider value={boardCardId}>
<RecoilRoot>{children}</RecoilRoot>
</BoardCardIdContext.Provider>
</RecordBoardDeprecatedScope>
);
describe('useCurrentRecordBoardDeprecatedCardSelectedInternal', () => {
it('should update the data when selecting and deselecting the cardId', () => {
const { result } = renderHook(
() => ({
currentCardSelect:
useCurrentRecordBoardDeprecatedCardSelectedInternal(),
activeCardIdsState: useRecoilValue(
useRecordBoardDeprecatedScopedStates().activeCardIdsState,
),
actionBarOpenState: useRecoilValue(actionBarOpenState),
}),
{
wrapper: Wrapper,
},
);
expect(result.current.activeCardIdsState).toStrictEqual([]);
expect(result.current.actionBarOpenState).toBe(false);
expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(false);
act(() => {
result.current.currentCardSelect.setCurrentCardSelected(true);
});
expect(result.current.activeCardIdsState).toStrictEqual([boardCardId]);
expect(result.current.actionBarOpenState).toBe(true);
expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(true);
act(() => {
result.current.currentCardSelect.setCurrentCardSelected(false);
});
expect(result.current.activeCardIdsState).toStrictEqual([]);
expect(result.current.actionBarOpenState).toBe(false);
expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(false);
});
});

View File

@ -1,124 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import gql from 'graphql-tag';
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
import { BoardCardIdContext } from '@/object-record/record-board-deprecated/contexts/BoardCardIdContext';
import { useCreateOpportunity } from '@/object-record/record-board-deprecated/hooks/internal/useCreateOpportunity';
import { useCurrentRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useCurrentRecordBoardDeprecatedCardSelectedInternal';
import { useDeleteSelectedRecordBoardDeprecatedCardsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useDeleteSelectedRecordBoardDeprecatedCardsInternal';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board-deprecated/states/recordBoardCardIdsByColumnIdFamilyState';
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
}));
const mockedUuid = 'mocked-uuid';
jest.mock('uuid', () => ({ v4: () => mockedUuid }));
const mocks = [
{
request: {
query: gql`
mutation DeleteManyOpportunities($filter: OpportunityFilterInput!) {
deleteOpportunities(filter: $filter) {
id
}
}
`,
variables: { filter: { id: { in: [mockedUuid] } } },
},
result: jest.fn(() => ({
data: { deleteOpportunities: { id: '' } },
})),
},
{
request: {
query: gql`
mutation CreateOneOpportunity($input: OpportunityCreateInput!) {
createOpportunity(data: $input) {
id
}
}
`,
variables: {
input: {
id: mockedUuid,
pipelineStepId: 'pipelineStepId',
companyId: 'New Opportunity',
},
},
},
result: jest.fn(() => ({
data: { createOpportunity: { id: '' } },
})),
},
];
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<BoardCardIdContext.Provider value={mockedUuid}>
<RecoilRoot>{children}</RecoilRoot>
</BoardCardIdContext.Provider>
</RecordBoardDeprecatedScope>
</MockedProvider>
);
describe('useDeleteSelectedRecordBoardDeprecatedCardsInternal', () => {
it('should run apollo mutation and update recoil state when delete selected cards', async () => {
const companyIdname = 'New Opportunity';
const opportunityPipelineStepId = 'pipelineStepId';
const { result } = renderHook(
() => ({
createOpportunity: useCreateOpportunity(),
deleteSelectedCards:
useDeleteSelectedRecordBoardDeprecatedCardsInternal(),
setBoardColumns: useSetRecoilState(
useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: scopeId,
}).boardColumnsState,
),
recordBoardCardIdsByColumnId: useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState(opportunityPipelineStepId),
),
currentSelect: useCurrentRecordBoardDeprecatedCardSelectedInternal(),
}),
{
wrapper: Wrapper,
},
);
act(() => {
result.current.currentSelect.setCurrentCardSelected(true);
result.current.setBoardColumns([
{
id: opportunityPipelineStepId,
title: '1',
position: 1,
},
]);
result.current.createOpportunity(
companyIdname,
opportunityPipelineStepId,
);
});
expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([
mockedUuid,
]);
await act(async () => {
await result.current.deleteSelectedCards();
});
await waitFor(() => {
expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([]);
expect(mocks[0].result).toHaveBeenCalled();
});
});
});

View File

@ -1,54 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useDeleteSelectedRecordBoardDeprecatedCardsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useDeleteSelectedRecordBoardDeprecatedCardsInternal';
import { useRecordBoardDeprecatedActionBarEntriesInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedActionBarEntriesInternal';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { IconTrash } from '@/ui/display/icon';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider>
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<RecoilRoot>{children}</RecoilRoot>
</RecordBoardDeprecatedScope>
</MockedProvider>
);
const renderHookConfig = {
wrapper: Wrapper,
};
describe('useRecordBoardDeprecatedActionBarEntriesInternal', () => {
it('should update actionBarEntries', async () => {
const { result } = renderHook(() => {
const deleteSelectedBoardCards =
useDeleteSelectedRecordBoardDeprecatedCardsInternal();
const newActionBarEntry = {
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
};
return {
setActionBarEntries: useRecordBoardDeprecatedActionBarEntriesInternal(),
actionBarEntries: useRecoilValue(actionBarEntriesState),
newActionBarEntry,
};
}, renderHookConfig);
expect(result.current.actionBarEntries).toStrictEqual([]);
act(() => {
result.current.setActionBarEntries.setActionBarEntries();
});
await waitFor(() => {
expect(JSON.stringify(result.current.actionBarEntries)).toBe(
JSON.stringify([result.current.newActionBarEntry]),
);
});
});
});

View File

@ -1,117 +0,0 @@
import { act } from 'react-dom/test-utils';
import { renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { useRecordBoardDeprecatedCardFieldsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedCardFieldsInternal';
import { onFieldsChangeScopedState } from '@/object-record/record-board-deprecated/states/onFieldsChangeScopedState';
import { recordBoardCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedCardFieldsScopedState';
import { savedRecordBoardDeprecatedCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/savedRecordBoardDeprecatedCardFieldsScopedState';
import { FieldType } from '@/object-record/record-field/types/FieldType';
const recordBoardScopeId = 'recordBoardScopeId';
const renderHookConfig = {
wrapper: RecoilRoot,
};
describe('useRecordBoardDeprecatedCardFieldsInternal', () => {
it('should toggle field visibility', async () => {
const { result } = renderHook(() => {
const [cardFieldsList, setCardFieldsList] = useRecoilState(
recordBoardCardFieldsScopedState({ scopeId: recordBoardScopeId }),
);
return {
boardCardFields: useRecordBoardDeprecatedCardFieldsInternal({
recordBoardScopeId,
}),
cardFieldsList,
setCardFieldsList,
};
}, renderHookConfig);
const field = {
position: 0,
fieldMetadataId: 'id',
label: 'label',
iconName: 'icon',
type: 'TEXT' as FieldType,
metadata: {
fieldName: 'fieldName',
},
isVisible: true,
};
act(() => {
result.current.setCardFieldsList([field]);
});
expect(result.current.cardFieldsList[0].isVisible).toBe(true);
act(() => {
result.current.boardCardFields.handleFieldVisibilityChange({
...field,
isVisible: true,
});
});
waitFor(() => {
expect(result.current.cardFieldsList[0].isVisible).toBe(false);
});
act(() => {
result.current.boardCardFields.handleFieldVisibilityChange({
...field,
isVisible: false,
});
});
waitFor(() => {
expect(result.current.cardFieldsList[0].isVisible).toBe(true);
});
});
it('should call the onFieldsChange callback and update board card states', async () => {
const { result } = renderHook(() => {
const [onFieldsChange, setOnFieldsChange] = useRecoilState(
onFieldsChangeScopedState({ scopeId: recordBoardScopeId }),
);
return {
boardCardFieldsHook: useRecordBoardDeprecatedCardFieldsInternal({
recordBoardScopeId,
}),
boardCardFieldsList: useRecoilValue(
recordBoardCardFieldsScopedState({ scopeId: recordBoardScopeId }),
),
savedBoardCardFieldsList: useRecoilValue(
savedRecordBoardDeprecatedCardFieldsScopedState({
scopeId: recordBoardScopeId,
}),
),
onFieldsChange,
setOnFieldsChange,
};
}, renderHookConfig);
const field = {
position: 0,
fieldMetadataId: 'id',
label: 'label',
iconName: 'icon',
type: 'TEXT' as FieldType,
metadata: {
fieldName: 'fieldName',
},
isVisible: true,
};
const onChangeFunction = jest.fn();
await act(async () => {
result.current.setOnFieldsChange(() => onChangeFunction);
result.current.boardCardFieldsHook.handleFieldsReorder([field]);
});
expect(onChangeFunction).toHaveBeenCalledWith([field]);
expect(result.current.savedBoardCardFieldsList).toStrictEqual([field]);
expect(result.current.boardCardFieldsList).toStrictEqual([field]);
});
});

View File

@ -1,128 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import gql from 'graphql-tag';
import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
import { useBoardColumnsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedColumnsInternal';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
}));
const mocks = [
{
request: {
query: gql`
mutation UpdateOnePipelineStep(
$idToUpdate: ID!
$input: PipelineStepUpdateInput!
) {
updatePipelineStep(id: $idToUpdate, data: $input) {
id
}
}
`,
variables: { idToUpdate: '1', input: { position: 0 } },
},
result: jest.fn(() => ({
data: { updatePipelineStep: { id: '' } },
})),
},
];
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<RecoilRoot>{children}</RecoilRoot>
</RecordBoardDeprecatedScope>
</MockedProvider>
);
const renderHookConfig = {
wrapper: Wrapper,
};
describe('useBoardColumnsInternal', () => {
it('should update boardColumns state when moving to left and right', async () => {
const { result } = renderHook(() => {
const [boardColumnsList, setBoardColumnsList] = useRecoilState(
useRecordBoardDeprecatedScopedStates().boardColumnsState,
);
return {
boardColumns: useBoardColumnsInternal(),
boardColumnsList,
setBoardColumnsList,
};
}, renderHookConfig);
const columns: BoardColumnDefinition[] = [
{
id: '1',
title: '1',
position: 0,
},
{
id: '2',
title: '2',
position: 1,
},
];
act(() => {
result.current.setBoardColumnsList(columns);
});
act(() => {
result.current.boardColumns.handleMoveBoardColumn('right', columns[0]);
});
await waitFor(() => {
expect(result.current.boardColumnsList).toStrictEqual([
{ id: '2', title: '2', position: 0, index: 0 },
{ id: '1', title: '1', position: 1, index: 1 },
]);
});
act(() => {
result.current.boardColumns.handleMoveBoardColumn('left', columns[0]);
});
await waitFor(() => {
expect(result.current.boardColumnsList).toStrictEqual([
{ id: '1', title: '1', position: 0, index: 0 },
{ id: '2', title: '2', position: 1, index: 1 },
]);
});
});
it('should call apollo mutation after persistBoardColumns', async () => {
const { result } = renderHook(() => {
return {
boardColumns: useBoardColumnsInternal(),
setBoardColumnsList: useSetRecoilState(
useRecordBoardDeprecatedScopedStates().boardColumnsState,
),
};
}, renderHookConfig);
const columns: BoardColumnDefinition[] = [
{
id: '1',
title: '1',
position: 0,
},
];
act(() => {
result.current.setBoardColumnsList(columns);
});
act(() => {
result.current.boardColumns.persistBoardColumns();
});
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();
});
});
});

View File

@ -1,56 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useDeleteSelectedRecordBoardDeprecatedCardsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useDeleteSelectedRecordBoardDeprecatedCardsInternal';
import { useRecordBoardDeprecatedContextMenuEntriesInternal } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedContextMenuEntriesInternal';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { IconTrash } from '@/ui/display/icon';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MockedProvider>
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<RecoilRoot>{children}</RecoilRoot>
</RecordBoardDeprecatedScope>
</MockedProvider>
);
describe('useRecordBoardDeprecatedContextMenuEntriesInternal', () => {
it('should update contextEntries', async () => {
const { result } = renderHook(
() => {
const deleteSelectedBoardCards =
useDeleteSelectedRecordBoardDeprecatedCardsInternal();
const newContextEntry = {
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
};
return {
setContextEntries:
useRecordBoardDeprecatedContextMenuEntriesInternal(),
contextEntries: useRecoilValue(contextMenuEntriesState),
newContextEntry,
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.contextEntries).toStrictEqual([]);
act(() => {
result.current.setContextEntries.setContextMenuEntries();
});
await waitFor(() => {
expect(JSON.stringify(result.current.contextEntries)).toBe(
JSON.stringify([result.current.newContextEntry]),
);
});
});
});

View File

@ -1,58 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useSetRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useSetRecordBoardDeprecatedCardSelectedInternal';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { isRecordBoardDeprecatedCardSelectedFamilyState } from '@/object-record/record-board-deprecated/states/isRecordBoardDeprecatedCardSelectedFamilyState';
const scopeId = 'scopeId';
const boardCardId = 'boardCardId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<RecoilRoot>{children}</RecoilRoot>
</RecordBoardDeprecatedScope>
);
const recordBoardScopeId = 'recordBoardScopeId';
describe('useSetRecordBoardDeprecatedCardSelectedInternal', () => {
it('should update the data when selecting and deselecting the cardId', async () => {
const { result } = renderHook(
() => {
return {
cardSelect: useSetRecordBoardDeprecatedCardSelectedInternal({
recordBoardScopeId,
}),
isSelected: useRecoilValue(
isRecordBoardDeprecatedCardSelectedFamilyState(boardCardId),
),
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.isSelected).toBe(false);
act(() => {
result.current.cardSelect.setCardSelected(boardCardId, true);
});
expect(result.current.isSelected).toBe(true);
act(() => {
result.current.cardSelect.setCardSelected(boardCardId, false);
});
expect(result.current.isSelected).toBe(false);
act(() => {
result.current.cardSelect.setCardSelected(boardCardId, true);
result.current.cardSelect.unselectAllActiveCards();
});
expect(result.current.isSelected).toBe(false);
});
});

View File

@ -1,113 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CompanyForBoard } from '@/companies/types/CompanyProgress';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { useUpdateCompanyBoardColumnsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useUpdateCompanyBoardColumnsInternal';
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board-deprecated/states/recordBoardCardIdsByColumnIdFamilyState';
import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
const scopeId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordBoardDeprecatedScope recordBoardScopeId={scopeId}>
<RecoilRoot>{children}</RecoilRoot>
</RecordBoardDeprecatedScope>
);
describe('useUpdateCompanyBoardColumnsInternal', () => {
it('should update recoil state after updateCompanyBoardColumns call ', async () => {
const { result } = renderHook(
() => {
return {
updateCompanyBoardColumns: useUpdateCompanyBoardColumnsInternal(),
currentPipeline: useRecoilValue(currentPipelineStepsState),
boardColumns: useRecoilValue(
useRecordBoardDeprecatedScopedStates().boardColumnsState,
),
savedBoardColumns: useRecoilValue(
useRecordBoardDeprecatedScopedStates().savedBoardColumnsState,
),
idsByColumnId: useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState('1'),
),
};
},
{
wrapper: Wrapper,
},
);
const pipelineSteps: PipelineStep[] = [
{
id: '1',
name: 'Step 1',
color: 'red',
position: 1,
createdAt: '2024-01-12',
updatedAt: '2024-01-12',
},
{
id: '2',
name: 'Step 2',
color: 'blue',
position: 1,
createdAt: '2024-01-12',
updatedAt: '2024-01-12',
},
];
const opportunity: Opportunity = {
id: '123',
amount: {
amountMicros: 1000000,
currencyCode: 'USD',
},
closeDate: new Date('2024-02-01'),
probability: 0.75,
pipelineStepId: '1',
pipelineStep: pipelineSteps[0],
pointOfContactId: '456',
pointOfContact: {
id: '456',
name: {
firstName: 'John',
lastName: 'Doe',
},
avatarUrl: 'https://example.com/avatar.jpg',
},
};
const companyForBoard: CompanyForBoard = {
id: '789',
name: 'Acme Inc.',
domainName: 'acme.com',
};
expect(result.current.currentPipeline).toStrictEqual([]);
expect(result.current.savedBoardColumns).toStrictEqual([]);
expect(result.current.boardColumns).toStrictEqual([]);
expect(result.current.idsByColumnId).toStrictEqual([]);
act(() => {
result.current.updateCompanyBoardColumns(
pipelineSteps,
[opportunity],
[companyForBoard],
);
});
const expectedBoardColumns = [
{ id: '1', title: 'Step 1', colorCode: 'red', position: 1 },
{ id: '2', title: 'Step 2', colorCode: 'blue', position: 1 },
];
expect(result.current.currentPipeline).toStrictEqual(pipelineSteps);
expect(result.current.savedBoardColumns).toStrictEqual(
expectedBoardColumns,
);
expect(result.current.boardColumns).toStrictEqual(expectedBoardColumns);
expect(result.current.idsByColumnId).toStrictEqual([opportunity.id]);
});
});

View File

@ -1,36 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board-deprecated/states/recordBoardCardIdsByColumnIdFamilyState';
import { Opportunity } from '@/pipeline/types/Opportunity';
export const useCreateOpportunity = () => {
const { createOneRecord: createOneOpportunity } =
useCreateOneRecord<Opportunity>({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const createOpportunity = useRecoilCallback(
({ set }) =>
async (companyId: string, pipelineStepId: string) => {
const newUuid = v4();
set(
recordBoardCardIdsByColumnIdFamilyState(pipelineStepId),
(oldValue) => [...oldValue, newUuid],
);
await createOneOpportunity?.({
id: newUuid,
name: 'Opportunity',
pipelineStepId,
companyId: companyId,
});
},
[createOneOpportunity],
);
return createOpportunity;
};

View File

@ -1,50 +0,0 @@
import { useContext } from 'react';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
import { BoardCardIdContext } from '../../contexts/BoardCardIdContext';
import { isRecordBoardDeprecatedCardSelectedFamilyState } from '../../states/isRecordBoardDeprecatedCardSelectedFamilyState';
export const useCurrentRecordBoardDeprecatedCardSelectedInternal = () => {
const currentCardId = useContext(BoardCardIdContext);
const isCurrentCardSelected = useRecoilValue(
isRecordBoardDeprecatedCardSelectedFamilyState(currentCardId ?? ''),
);
const { activeCardIdsState } = useRecordBoardDeprecatedScopedStates();
const setActiveCardIds = useSetRecoilState(activeCardIdsState);
const setCurrentCardSelected = useRecoilCallback(
({ set }) =>
(selected: boolean) => {
if (!currentCardId) return;
set(
isRecordBoardDeprecatedCardSelectedFamilyState(currentCardId),
selected,
);
set(actionBarOpenState, selected);
if (selected) {
setActiveCardIds((prevActiveCardIds) => [
...prevActiveCardIds,
currentCardId,
]);
} else {
setActiveCardIds((prevActiveCardIds) =>
prevActiveCardIds.filter((id) => id !== currentCardId),
);
}
},
[currentCardId, setActiveCardIds],
);
return {
isCurrentCardSelected,
setCurrentCardSelected,
};
};

View File

@ -1,42 +0,0 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { useRemoveRecordBoardDeprecatedCardIdsInternal } from './useRemoveRecordBoardDeprecatedCardIdsInternal';
export const useDeleteSelectedRecordBoardDeprecatedCardsInternal = () => {
const removeCardIds = useRemoveRecordBoardDeprecatedCardIdsInternal();
const apolloClient = useApolloClient();
const { deleteManyRecords: deleteManyOpportunities } = useDeleteManyRecords({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates();
const deleteSelectedBoardCards = useRecoilCallback(
({ snapshot }) =>
async () => {
const selectedCardIds = snapshot
.getLoadable(selectedCardIdsSelector)
.getValue();
await deleteManyOpportunities?.(selectedCardIds);
removeCardIds(selectedCardIds);
selectedCardIds.forEach((id) => {
apolloClient.cache.evict({ id: `Opportunity:${id}` });
});
},
[
selectedCardIdsSelector,
removeCardIds,
deleteManyOpportunities,
apolloClient.cache,
],
);
return deleteSelectedBoardCards;
};

View File

@ -1,28 +0,0 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useDeleteSelectedRecordBoardDeprecatedCardsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useDeleteSelectedRecordBoardDeprecatedCardsInternal';
import { IconTrash } from '@/ui/display/icon';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
export const useRecordBoardDeprecatedActionBarEntriesInternal = () => {
const setActionBarEntriesRecoil = useSetRecoilState(actionBarEntriesState);
const deleteSelectedBoardCards =
useDeleteSelectedRecordBoardDeprecatedCardsInternal();
const setActionBarEntries = useCallback(() => {
setActionBarEntriesRecoil([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
},
]);
}, [deleteSelectedBoardCards, setActionBarEntriesRecoil]);
return {
setActionBarEntries,
};
};

View File

@ -1,97 +0,0 @@
import { useCallback } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { RecordBoardDeprecatedScopeInternalContext } from '@/object-record/record-board-deprecated/scopes/scope-internal-context/RecordBoardDeprecatedScopeInternalContext';
import { onFieldsChangeScopedState } from '@/object-record/record-board-deprecated/states/onFieldsChangeScopedState';
import { recordBoardCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedCardFieldsScopedState';
import { savedRecordBoardDeprecatedCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/savedRecordBoardDeprecatedCardFieldsScopedState';
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRecordBoardDeprecatedCardFieldsInternalProps = {
recordBoardScopeId?: string;
};
export const useRecordBoardDeprecatedCardFieldsInternal = (
props?: useRecordBoardDeprecatedCardFieldsInternalProps,
) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordBoardDeprecatedScopeInternalContext,
props?.recordBoardScopeId,
);
const setBoardCardFields = useSetRecoilState(
recordBoardCardFieldsScopedState({ scopeId }),
);
const setSavedBoardCardFields = useSetRecoilState(
savedRecordBoardDeprecatedCardFieldsScopedState({ scopeId }),
);
const handleFieldVisibilityChange = useRecoilCallback(
({ snapshot }) =>
async (
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
) => {
const existingFields = snapshot
.getLoadable(recordBoardCardFieldsScopedState({ scopeId }))
.getValue();
const fieldIndex = existingFields.findIndex(
({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId,
);
const fields = [...existingFields];
if (fieldIndex === -1) {
fields.push({ ...field, position: existingFields.length });
} else {
fields[fieldIndex] = {
...field,
isVisible: !field.isVisible,
position: existingFields.length,
};
}
setSavedBoardCardFields(fields);
setBoardCardFields(fields);
const onFieldsChange = snapshot
.getLoadable(onFieldsChangeScopedState({ scopeId }))
.getValue();
onFieldsChange?.(fields);
},
[scopeId, setBoardCardFields, setSavedBoardCardFields],
);
const handleFieldsChange = useRecoilCallback(
({ snapshot }) =>
async (fields: BoardFieldDefinition<FieldMetadata>[]) => {
setSavedBoardCardFields(fields);
setBoardCardFields(fields);
const onFieldsChange = snapshot
.getLoadable(onFieldsChangeScopedState({ scopeId }))
.getValue();
await onFieldsChange?.(fields);
},
[scopeId, setBoardCardFields, setSavedBoardCardFields],
);
const handleFieldsReorder = useCallback(
async (fields: BoardFieldDefinition<FieldMetadata>[]) => {
const updatedFields = fields.map((column, index) => ({
...column,
position: index,
}));
await handleFieldsChange(updatedFields);
},
[handleFieldsChange],
);
return { handleFieldVisibilityChange, handleFieldsReorder };
};

View File

@ -1,58 +0,0 @@
import { useRecoilState } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { BoardColumnDefinition } from '../../types/BoardColumnDefinition';
export const useBoardColumnsInternal = () => {
const { boardColumnsState } = useRecordBoardDeprecatedScopedStates();
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const { handleColumnMove } = useMoveViewColumns();
const { updateOneRecord: updateOnePipelineStep } =
useUpdateOneRecord<PipelineStep>({
objectNameSingular: CoreObjectNameSingular.PipelineStep,
});
const updatedPipelineSteps = (stages: BoardColumnDefinition[]) => {
if (!stages.length) return;
return Promise.all(
stages.map(
(stage) =>
updateOnePipelineStep?.({
idToUpdate: stage.id,
updateOneRecordInput: {
position: stage.position,
},
}),
),
);
};
const persistBoardColumns = async () => {
await updatedPipelineSteps(boardColumns);
};
const handleMoveBoardColumn = (
direction: 'left' | 'right',
column: BoardColumnDefinition,
) => {
const currentColumnArrayIndex = boardColumns.findIndex(
(tableColumn) => tableColumn.id === column.id,
);
const columns = handleColumnMove(
direction,
currentColumnArrayIndex,
boardColumns,
);
setBoardColumns(columns);
};
return { handleMoveBoardColumn, persistBoardColumns };
};

View File

@ -1,30 +0,0 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useDeleteSelectedRecordBoardDeprecatedCardsInternal } from '@/object-record/record-board-deprecated/hooks/internal/useDeleteSelectedRecordBoardDeprecatedCardsInternal';
import { IconTrash } from '@/ui/display/icon';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
export const useRecordBoardDeprecatedContextMenuEntriesInternal = () => {
const setContextMenuEntriesRecoil = useSetRecoilState(
contextMenuEntriesState,
);
const deleteSelectedBoardCards =
useDeleteSelectedRecordBoardDeprecatedCardsInternal();
const setContextMenuEntries = useCallback(() => {
setContextMenuEntriesRecoil([
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: deleteSelectedBoardCards,
},
]);
}, [deleteSelectedBoardCards, setContextMenuEntriesRecoil]);
return {
setContextMenuEntries,
};
};

View File

@ -1,59 +0,0 @@
import { RecordBoardDeprecatedScopeInternalContext } from '@/object-record/record-board-deprecated/scopes/scope-internal-context/RecordBoardDeprecatedScopeInternalContext';
import { getRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/utils/getRecordBoardDeprecatedScopedStates';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRecordBoardDeprecatedScopedStatesProps = {
recordBoardScopeId?: string;
};
export const useRecordBoardDeprecatedScopedStates = (
args?: useRecordBoardDeprecatedScopedStatesProps,
) => {
const { recordBoardScopeId } = args ?? {};
const scopeId = useAvailableScopeIdOrThrow(
RecordBoardDeprecatedScopeInternalContext,
recordBoardScopeId,
);
const {
activeCardIdsState,
availableBoardCardFieldsState,
boardColumnsState,
isBoardLoadedState,
isCompactViewEnabledState,
savedBoardColumnsState,
boardFiltersState,
boardSortsState,
onFieldsChangeState,
boardCardFieldsByKeySelector,
hiddenBoardCardFieldsSelector,
selectedCardIdsSelector,
visibleBoardCardFieldsSelector,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
} = getRecordBoardDeprecatedScopedStates({
recordBoardScopeId: scopeId,
});
return {
scopeId,
activeCardIdsState,
availableBoardCardFieldsState,
boardColumnsState,
isBoardLoadedState,
isCompactViewEnabledState,
savedBoardColumnsState,
boardFiltersState,
boardSortsState,
onFieldsChangeState,
boardCardFieldsByKeySelector,
hiddenBoardCardFieldsSelector,
selectedCardIdsSelector,
visibleBoardCardFieldsSelector,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
};
};

View File

@ -1,30 +0,0 @@
// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useRecoilCallback } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { recordBoardCardIdsByColumnIdFamilyState } from '../../states/recordBoardCardIdsByColumnIdFamilyState';
export const useRemoveRecordBoardDeprecatedCardIdsInternal = () => {
const { boardColumnsState } = useRecordBoardDeprecatedScopedStates();
return useRecoilCallback(
({ snapshot, set }) =>
(cardIdToRemove: string[]) => {
const boardColumns = snapshot.getLoadable(boardColumnsState).getValue();
boardColumns.forEach((boardColumn) => {
const columnCardIds = snapshot
.getLoadable(
recordBoardCardIdsByColumnIdFamilyState(boardColumn.id),
)
.getValue();
set(
recordBoardCardIdsByColumnIdFamilyState(boardColumn.id),
columnCardIds.filter((cardId) => !cardIdToRemove.includes(cardId)),
);
});
},
[boardColumnsState],
);
};

View File

@ -1,62 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { RecordBoardDeprecatedScopeInternalContext } from '@/object-record/record-board-deprecated/scopes/scope-internal-context/RecordBoardDeprecatedScopeInternalContext';
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { isRecordBoardDeprecatedCardSelectedFamilyState } from '../../states/isRecordBoardDeprecatedCardSelectedFamilyState';
export const useSetRecordBoardDeprecatedCardSelectedInternal = (props: any) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordBoardDeprecatedScopeInternalContext,
props?.recordBoardScopeId,
);
const { activeCardIdsState } = useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: scopeId,
});
const setCardSelected = useRecoilCallback(
({ set, snapshot }) =>
(cardId: string, selected: boolean) => {
const activeCardIds = snapshot
.getLoadable(activeCardIdsState)
.getValue();
set(isRecordBoardDeprecatedCardSelectedFamilyState(cardId), selected);
set(actionBarOpenState, selected || activeCardIds.length > 0);
if (selected) {
set(activeCardIdsState, [...activeCardIds, cardId]);
} else {
set(
activeCardIdsState,
activeCardIds.filter((id: string) => id !== cardId),
);
}
},
[activeCardIdsState],
);
const unselectAllActiveCards = useRecoilCallback(
({ set, snapshot }) =>
() => {
const activeCardIds = snapshot
.getLoadable(activeCardIdsState)
.getValue();
activeCardIds.forEach((cardId: string) => {
set(isRecordBoardDeprecatedCardSelectedFamilyState(cardId), false);
});
set(activeCardIdsState, []);
set(actionBarOpenState, false);
},
[activeCardIdsState],
);
return {
setCardSelected,
unselectAllActiveCards,
};
};

View File

@ -1,148 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board-deprecated/states/recordBoardCardIdsByColumnIdFamilyState';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { logError } from '~/utils/logError';
import { companyProgressesFamilyState } from '../../../../companies/states/companyProgressesFamilyState';
import {
CompanyForBoard,
CompanyProgressDict,
} from '../../../../companies/types/CompanyProgress';
export const useUpdateCompanyBoardColumnsInternal = () => {
const { boardColumnsState, savedBoardColumnsState } =
useRecordBoardDeprecatedScopedStates();
return useRecoilCallback(
({ set, snapshot }) =>
(
pipelineSteps: PipelineStep[],
opportunities: Opportunity[],
companies: CompanyForBoard[],
) => {
const indexCompanyByIdReducer = (
acc: { [key: string]: CompanyForBoard },
company: CompanyForBoard,
) => ({
...acc,
[company.id]: company,
});
const companiesDict =
companies.reduce(
indexCompanyByIdReducer,
{} as { [key: string]: CompanyForBoard },
) ?? {};
const indexOpportunityByIdReducer = (
acc: CompanyProgressDict,
opportunity: Opportunity,
) => {
const company =
opportunity.companyId && companiesDict[opportunity.companyId];
if (!company) return acc;
return {
...acc,
[opportunity.id]: {
opportunity,
company,
},
};
};
const companyBoardIndex = opportunities.reduce(
indexOpportunityByIdReducer,
{} as CompanyProgressDict,
);
for (const [id, companyProgress] of Object.entries(companyBoardIndex)) {
const currentCompanyProgress = snapshot
.getLoadable(companyProgressesFamilyState(id))
.getValue();
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
set(companyProgressesFamilyState(id), companyProgress);
set(recordStoreFamilyState(id), companyProgress.opportunity);
}
}
const currentPipelineSteps = snapshot
.getLoadable(currentPipelineStepsState)
.getValue();
const currentBoardColumns = snapshot
.getLoadable(boardColumnsState)
.getValue();
if (!isDeeplyEqual(pipelineSteps, currentPipelineSteps)) {
set(currentPipelineStepsState, pipelineSteps);
}
const orderedPipelineSteps = [...pipelineSteps].sort((a, b) => {
if (!a.position || !b.position) return 0;
return a.position - b.position;
});
const newBoardColumns: BoardColumnDefinition[] =
orderedPipelineSteps?.map((pipelineStep) => {
const colorValidationResult = themeColorSchema.safeParse(
pipelineStep.color,
);
if (!colorValidationResult.success) {
logError(
`Color ${pipelineStep.color} is not recognized in useUpdateCompanyBoard.`,
);
}
return {
id: pipelineStep.id,
title: pipelineStep.name,
colorCode: colorValidationResult.success
? colorValidationResult.data
: undefined,
position: pipelineStep.position ?? 0,
};
});
if (
currentBoardColumns.length === 0 &&
!isDeeplyEqual(newBoardColumns, currentBoardColumns)
) {
set(boardColumnsState, newBoardColumns);
set(savedBoardColumnsState, newBoardColumns);
}
for (const boardColumn of newBoardColumns) {
const boardCardIds = opportunities
.filter(
(opportunityToFilter) =>
opportunityToFilter.pipelineStepId === boardColumn.id,
)
.map((opportunity) => opportunity.id);
const currentBoardCardIds = snapshot
.getLoadable(
recordBoardCardIdsByColumnIdFamilyState(boardColumn.id),
)
.getValue();
if (!isDeeplyEqual(currentBoardCardIds, boardCardIds)) {
set(
recordBoardCardIdsByColumnIdFamilyState(boardColumn.id),
boardCardIds,
);
}
}
},
[boardColumnsState, savedBoardColumnsState],
);
};

View File

@ -1,39 +0,0 @@
import { useSetRecoilState } from 'recoil';
import { useCreateOpportunity } from '@/object-record/record-board-deprecated/hooks/internal/useCreateOpportunity';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import { RecordBoardDeprecatedScopeInternalContext } from '@/object-record/record-board-deprecated/scopes/scope-internal-context/RecordBoardDeprecatedScopeInternalContext';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRecordBoardDeprecatedProps = {
recordBoardScopeId?: string;
};
export const useRecordBoardDeprecated = (
props?: useRecordBoardDeprecatedProps,
) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordBoardDeprecatedScopeInternalContext,
props?.recordBoardScopeId,
);
const { isBoardLoadedState, boardColumnsState, onFieldsChangeState } =
useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: scopeId,
});
const setIsBoardLoaded = useSetRecoilState(isBoardLoadedState);
const setBoardColumns = useSetRecoilState(boardColumnsState);
const createOpportunity = useCreateOpportunity();
const setOnFieldsChange = useSetRecoilState(onFieldsChangeState);
return {
scopeId,
setIsBoardLoaded,
setBoardColumns,
createOpportunity,
setOnFieldsChange,
};
};

View File

@ -1,39 +0,0 @@
import { BOARD_OPTIONS_DROPDOWN_ID } from '@/object-record/record-board-deprecated/constants/BoardOptionsDropdownId';
import { useViewBar } from '@/views/hooks/useViewBar';
import { Dropdown } from '../../../../ui/layout/dropdown/components/Dropdown';
import { BoardOptionsHotkeyScope } from '../../types/BoardOptionsHotkeyScope';
import { RecordBoardDeprecatedOptionsDropdownButton } from './RecordBoardDeprecatedOptionsDropdownButton';
import {
RecordBoardDeprecatedOptionsDropdownContent,
RecordBoardDeprecatedOptionsDropdownContentProps,
} from './RecordBoardDeprecatedOptionsDropdownContent';
type RecordBoardDeprecatedOptionsDropdownProps = Pick<
RecordBoardDeprecatedOptionsDropdownContentProps,
'onStageAdd' | 'recordBoardId'
>;
export const RecordBoardDeprecatedOptionsDropdown = ({
onStageAdd,
recordBoardId,
}: RecordBoardDeprecatedOptionsDropdownProps) => {
const { setViewEditMode } = useViewBar();
return (
<Dropdown
dropdownId={BOARD_OPTIONS_DROPDOWN_ID}
clickableComponent={<RecordBoardDeprecatedOptionsDropdownButton />}
dropdownComponents={
<RecordBoardDeprecatedOptionsDropdownContent
onStageAdd={onStageAdd}
recordBoardId={recordBoardId}
/>
}
dropdownHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
onClickOutside={() => setViewEditMode('none')}
dropdownMenuWidth={170}
/>
);
};

View File

@ -1,22 +0,0 @@
import { BOARD_OPTIONS_DROPDOWN_ID } from '@/object-record/record-board-deprecated/constants/BoardOptionsDropdownId';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const RecordBoardDeprecatedOptionsDropdownButton = () => {
const { isDropdownOpen, toggleDropdown } = useDropdown(
BOARD_OPTIONS_DROPDOWN_ID,
);
const handleClick = () => {
toggleDropdown();
};
return (
<StyledHeaderDropdownButton
isUnfolded={isDropdownOpen}
onClick={handleClick}
>
Options
</StyledHeaderDropdownButton>
);
};

View File

@ -1,240 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { v4 } from 'uuid';
import { BOARD_OPTIONS_DROPDOWN_ID } from '@/object-record/record-board-deprecated/constants/BoardOptionsDropdownId';
import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-board-deprecated/hooks/internal/useRecordBoardDeprecatedScopedStates';
import {
IconBaselineDensitySmall,
IconChevronLeft,
IconLayoutKanban,
IconPlus,
IconTag,
} from '@/ui/display/icon';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate';
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
import { useViewBar } from '@/views/hooks/useViewBar';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { useRecordBoardDeprecatedCardFieldsInternal } from '../../hooks/internal/useRecordBoardDeprecatedCardFieldsInternal';
import { BoardColumnDefinition } from '../../types/BoardColumnDefinition';
import { BoardOptionsHotkeyScope } from '../../types/BoardOptionsHotkeyScope';
export type RecordBoardDeprecatedOptionsDropdownContentProps = {
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
recordBoardId: string;
};
type BoardOptionsMenu = 'fields' | 'stage-creation' | 'stages';
export const RecordBoardDeprecatedOptionsDropdownContent = ({
onStageAdd,
recordBoardId,
}: RecordBoardDeprecatedOptionsDropdownContentProps) => {
const { setViewEditMode, handleViewNameSubmit } = useViewBar();
const { viewEditModeState, currentViewSelector } = useViewScopedStates();
const viewEditMode = useRecoilValue(viewEditModeState);
const currentView = useRecoilValue(currentViewSelector);
const stageInputRef = useRef<HTMLInputElement>(null);
const viewEditInputRef = useRef<HTMLInputElement>(null);
const [currentMenu, setCurrentMenu] = useState<
BoardOptionsMenu | undefined
>();
const {
boardColumnsState,
isCompactViewEnabledState,
hiddenBoardCardFieldsSelector,
visibleBoardCardFieldsSelector,
} = useRecordBoardDeprecatedScopedStates({
recordBoardScopeId: recordBoardId,
});
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const [isCompactViewEnabled, setIsCompactViewEnabled] = useRecoilState(
isCompactViewEnabledState,
);
const hiddenBoardCardFields = useRecoilValue(hiddenBoardCardFieldsSelector);
const hasHiddenFields = hiddenBoardCardFields.length > 0;
const visibleBoardCardFields = useRecoilValue(visibleBoardCardFieldsSelector);
const hasVisibleFields = visibleBoardCardFields.length > 0;
const handleStageSubmit = () => {
if (currentMenu !== 'stage-creation' || !stageInputRef?.current?.value)
return;
const columnToCreate: BoardColumnDefinition = {
id: v4(),
colorCode: 'gray',
position: boardColumns.length,
title: stageInputRef.current.value,
};
setBoardColumns((previousBoardColumns) => [
...previousBoardColumns,
columnToCreate,
]);
onStageAdd?.(columnToCreate);
};
const resetMenu = () => setCurrentMenu(undefined);
const handleMenuNavigate = (menu: BoardOptionsMenu) => {
handleViewNameSubmit();
setCurrentMenu(menu);
};
const { handleFieldVisibilityChange, handleFieldsReorder } =
useRecordBoardDeprecatedCardFieldsInternal({
recordBoardScopeId: recordBoardId,
});
const { closeDropdown } = useDropdown(BOARD_OPTIONS_DROPDOWN_ID);
const handleReorderField: OnDragEndResponder = useCallback(
(result) => {
if (!result.destination) {
return;
}
const reorderedFields = moveArrayItem(visibleBoardCardFields, {
fromIndex: result.source.index - 1,
toIndex: result.destination.index - 1,
});
handleFieldsReorder(reorderedFields);
},
[handleFieldsReorder, visibleBoardCardFields],
);
useScopedHotkeys(
[Key.Escape],
() => {
setViewEditMode('none');
closeDropdown();
},
BoardOptionsHotkeyScope.Dropdown,
);
useScopedHotkeys(
Key.Enter,
() => {
const name = viewEditInputRef.current?.value;
resetMenu();
setViewEditMode('none');
handleStageSubmit();
handleViewNameSubmit(name);
closeDropdown();
},
BoardOptionsHotkeyScope.Dropdown,
);
return (
<>
{!currentMenu && (
<>
<DropdownMenuInput
ref={viewEditInputRef}
autoFocus={viewEditMode !== 'none'}
placeholder={
viewEditMode === 'create'
? 'New view'
: viewEditMode === 'edit'
? 'View name'
: ''
}
defaultValue={viewEditMode === 'create' ? '' : currentView?.name}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemNavigate
onClick={() => handleMenuNavigate('fields')}
LeftIcon={IconTag}
text="Fields"
/>
<MenuItemNavigate
onClick={() => handleMenuNavigate('stages')}
LeftIcon={IconLayoutKanban}
text="Stages"
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconBaselineDensitySmall}
onToggleChange={setIsCompactViewEnabled}
toggled={isCompactViewEnabled}
text="Compact view"
toggleSize="small"
/>
</DropdownMenuItemsContainer>
</>
)}
{currentMenu === 'stages' && (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Stages
</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => setCurrentMenu('stage-creation')}
LeftIcon={IconPlus}
text="Add stage"
/>
</DropdownMenuItemsContainer>
</>
)}
{currentMenu === 'stage-creation' && (
<DropdownMenuSearchInput
autoFocus
placeholder="New stage"
ref={stageInputRef}
/>
)}
{currentMenu === 'fields' && (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Fields
</DropdownMenuHeader>
<DropdownMenuSeparator />
{hasVisibleFields && (
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleBoardCardFields}
isDraggable
onDragEnd={handleReorderField}
onVisibilityChange={handleFieldVisibilityChange}
/>
)}
{hasVisibleFields && hasHiddenFields && <DropdownMenuSeparator />}
{hasHiddenFields && (
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenBoardCardFields}
isDraggable={false}
onVisibilityChange={handleFieldVisibilityChange}
/>
)}
</>
)}
</>
);
};

View File

@ -1,23 +0,0 @@
import { ReactNode } from 'react';
import { RecordBoardDeprecatedScopeInternalContext } from '@/object-record/record-board-deprecated/scopes/scope-internal-context/RecordBoardDeprecatedScopeInternalContext';
type RecordBoardDeprecatedScopeProps = {
children: ReactNode;
recordBoardScopeId: string;
};
export const RecordBoardDeprecatedScope = ({
children,
recordBoardScopeId,
}: RecordBoardDeprecatedScopeProps) => {
return (
<RecordBoardDeprecatedScopeInternalContext.Provider
value={{
scopeId: recordBoardScopeId,
}}
>
{children}
</RecordBoardDeprecatedScopeInternalContext.Provider>
);
};

View File

@ -1,7 +0,0 @@
import { StateScopeMapKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/StateScopeMapKey';
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
type RecordBoardDeprecatedScopeInternalContextProps = StateScopeMapKey;
export const RecordBoardDeprecatedScopeInternalContext =
createScopeInternalContext<RecordBoardDeprecatedScopeInternalContextProps>();

View File

@ -1,7 +0,0 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const activeRecordBoardDeprecatedCardIdsScopedState =
createStateScopeMap<string[]>({
key: 'activeRecordBoardDeprecatedCardIdsScopedState',
defaultValue: [],
});

View File

@ -1,10 +0,0 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const availableRecordBoardDeprecatedCardFieldsScopedState =
createStateScopeMap<BoardFieldDefinition<FieldMetadata>[]>({
key: 'availableRecordBoardDeprecatedCardFieldsScopedState',
defaultValue: [],
});

View File

@ -1,6 +0,0 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const isCompactViewEnabledScopedState = createStateScopeMap<boolean>({
key: 'isCompactViewEnabledScopedState',
defaultValue: false,
});

View File

@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const isRecordBoardDeprecatedCardInCompactViewFamilyState = atomFamily<
boolean,
string
>({
key: 'isRecordBoardDeprecatedCardInCompactViewFamilyState',
default: true,
});

View File

@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const isRecordBoardDeprecatedCardSelectedFamilyState = atomFamily<
boolean,
string
>({
key: 'isRecordBoardDeprecatedCardSelectedFamilyState',
default: false,
});

View File

@ -1,7 +0,0 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const isRecordBoardDeprecatedLoadedScopedState =
createStateScopeMap<boolean>({
key: 'isRecordBoardDeprecatedLoadedScopedState',
defaultValue: false,
});

View File

@ -1,10 +0,0 @@
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const onFieldsChangeScopedState = createStateScopeMap<
(fields: BoardFieldDefinition<FieldMetadata>[]) => void
>({
key: 'onFieldsChangeScopedState',
defaultValue: () => {},
});

View File

@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const recordBoardCardIdsByColumnIdFamilyState = atomFamily<
string[],
string
>({
key: 'recordBoardCardIdsByColumnIdFamilyState',
default: [],
});

View File

@ -1,9 +0,0 @@
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const recordBoardColumnsScopedState = createStateScopeMap<
BoardColumnDefinition[]
>({
key: 'recordBoardColumnsScopedState',
defaultValue: [],
});

View File

@ -1,11 +0,0 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const recordBoardCardFieldsScopedState = createStateScopeMap<
BoardFieldDefinition<FieldMetadata>[]
>({
key: 'recordBoardCardFieldsScopedState',
defaultValue: [],
});

View File

@ -1,7 +0,0 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const recordBoardFiltersScopedState = createStateScopeMap<Filter[]>({
key: 'recordBoardFiltersScopedState',
defaultValue: [],
});

View File

@ -1,8 +0,0 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
import { Sort } from '../../object-sort-dropdown/types/Sort';
export const recordBoardSortsScopedState = createStateScopeMap<Sort[]>({
key: 'recordBoardSortsScopedState',
defaultValue: [],
});

View File

@ -1,9 +0,0 @@
import { Opportunity } from '@/pipeline/types/Opportunity';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const savedOpportunitiesScopedState = createStateScopeMap<Opportunity[]>(
{
key: 'savedOpportunitiesScopedState',
defaultValue: [],
},
);

View File

@ -1,9 +0,0 @@
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const savedPipelineStepsScopedState = createStateScopeMap<
PipelineStep[]
>({
key: 'savedPipelineStepsScopedState',
defaultValue: [],
});

View File

@ -1,10 +0,0 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const savedRecordBoardDeprecatedCardFieldsScopedState =
createStateScopeMap<BoardFieldDefinition<FieldMetadata>[]>({
key: 'savedRecordBoardDeprecatedCardFieldsScopedState',
defaultValue: [],
});

View File

@ -1,10 +0,0 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
export const savedRecordBoardDeprecatedColumnsScopedState = createStateScopeMap<
BoardColumnDefinition[]
>({
key: 'savedRecordBoardDeprecatedColumnsScopedState',
defaultValue: [],
});

View File

@ -1,7 +0,0 @@
import { Company } from '@/companies/types/Company';
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const savedRecordsScopedState = createStateScopeMap<Company[]>({
key: 'savedRecordsScopedState',
defaultValue: [],
});

View File

@ -1,24 +0,0 @@
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
import { availableRecordBoardDeprecatedCardFieldsScopedState } from '../availableRecordBoardDeprecatedCardFieldsScopedState';
import { recordBoardCardFieldsScopedState } from '../recordBoardDeprecatedCardFieldsScopedState';
export const hiddenRecordBoardDeprecatedCardFieldsScopedSelector =
createSelectorReadOnlyScopeMap({
key: 'hiddenRecordBoardDeprecatedCardFieldsScopedSelector',
get:
({ scopeId }) =>
({ get }) => {
const fields = get(recordBoardCardFieldsScopedState({ scopeId }));
const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId);
const otherAvailableKeys = get(
availableRecordBoardDeprecatedCardFieldsScopedState({ scopeId }),
).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId));
return [
...fields.filter((field) => !field.isVisible),
...otherAvailableKeys,
];
},
});

View File

@ -1,16 +0,0 @@
import { selectorFamily } from 'recoil';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
import { recordBoardCardFieldsScopedState } from '../recordBoardDeprecatedCardFieldsScopedState';
export const recordBoardCardFieldsByKeyScopedSelector = selectorFamily({
key: 'recordBoardCardFieldsByKeyScopedSelector',
get:
(scopeId: string) =>
({ get }) =>
get(recordBoardCardFieldsScopedState({ scopeId })).reduce<
Record<string, BoardFieldDefinition<FieldMetadata>>
>((result, field) => ({ ...result, [field.fieldMetadataId]: field }), {}),
});

View File

@ -1,32 +0,0 @@
import { selectorFamily } from 'recoil';
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
import { amountFormat } from '~/utils/format/amountFormat';
import { recordBoardCardIdsByColumnIdFamilyState } from '../recordBoardCardIdsByColumnIdFamilyState';
// TODO: this state should be computed during the synchronization web-hook and put in a generic
// boardColumnTotalsFamilyState indexed by columnId.
export const recordBoardColumnTotalsFamilySelector = selectorFamily({
key: 'recordBoardColumnTotalsFamilySelector',
get:
(pipelineStepId: string) =>
({ get }) => {
const cardIds = get(
recordBoardCardIdsByColumnIdFamilyState(pipelineStepId),
);
const opportunities = cardIds.map((opportunityId: string) =>
get(companyProgressesFamilyState(opportunityId)),
);
const pipelineStepTotal: number =
opportunities?.reduce(
(acc: number, curr: any) =>
acc + curr?.opportunity.amount.amountMicros / 1000000,
0,
) || 0;
return amountFormat(pipelineStepTotal);
},
});

View File

@ -1,27 +0,0 @@
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
import { isRecordBoardDeprecatedCardSelectedFamilyState } from '../isRecordBoardDeprecatedCardSelectedFamilyState';
import { recordBoardCardIdsByColumnIdFamilyState } from '../recordBoardCardIdsByColumnIdFamilyState';
import { recordBoardColumnsScopedState } from '../recordBoardColumnsScopedState';
export const selectedRecordBoardDeprecatedCardIdsScopedSelector =
createSelectorReadOnlyScopeMap<string[]>({
key: 'selectedRecordBoardDeprecatedCardIdsScopedSelector',
get:
({ scopeId }) =>
({ get }) => {
const boardColumns = get(recordBoardColumnsScopedState({ scopeId }));
const cardIds = boardColumns.flatMap((boardColumn) =>
get(recordBoardCardIdsByColumnIdFamilyState(boardColumn.id)),
);
const selectedCardIds = cardIds.filter(
(cardId) =>
get(isRecordBoardDeprecatedCardSelectedFamilyState(cardId)) ===
true,
);
return selectedCardIds;
},
});

View File

@ -1,14 +0,0 @@
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
import { recordBoardCardFieldsScopedState } from '../recordBoardDeprecatedCardFieldsScopedState';
export const visibleRecordBoardDeprecatedCardFieldsScopedSelector =
createSelectorReadOnlyScopeMap({
key: 'visibleRecordBoardDeprecatedCardFieldsScopedSelector',
get:
({ scopeId }) =>
({ get }) =>
get(recordBoardCardFieldsScopedState({ scopeId }))
.filter((field) => field.isVisible)
.sort((a, b) => a.position - b.position),
});

View File

@ -1,8 +0,0 @@
import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
export type BoardColumnDefinition = {
id: string;
title: string;
position: number;
colorCode?: ThemeColor;
};

View File

@ -1,3 +0,0 @@
export enum BoardColumnHotkeyScope {
BoardColumn = 'board-column',
}

View File

@ -1,9 +0,0 @@
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
export type BoardFieldDefinition<T extends FieldMetadata> =
FieldDefinition<T> & {
position: number;
isVisible?: boolean;
viewFieldId?: string;
};

View File

@ -1,6 +0,0 @@
import { ComponentType } from 'react';
export type BoardOptions = {
newCardComponent: React.ReactNode;
CardComponent: ComponentType;
};

View File

@ -1,3 +0,0 @@
export enum BoardOptionsHotkeyScope {
Dropdown = 'board-options-dropdown',
}

View File

@ -1,3 +0,0 @@
export enum ColumnHotkeyScope {
EditColumnName = 'EditColumnNameHotkeyScope',
}

View File

@ -1,121 +0,0 @@
import { activeRecordBoardDeprecatedCardIdsScopedState } from '@/object-record/record-board-deprecated/states/activeRecordBoardDeprecatedCardIdsScopedState';
import { availableRecordBoardDeprecatedCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/availableRecordBoardDeprecatedCardFieldsScopedState';
import { isCompactViewEnabledScopedState } from '@/object-record/record-board-deprecated/states/isCompactViewEnabledScopedState';
import { isRecordBoardDeprecatedLoadedScopedState } from '@/object-record/record-board-deprecated/states/isRecordBoardDeprecatedLoadedScopedState';
import { onFieldsChangeScopedState } from '@/object-record/record-board-deprecated/states/onFieldsChangeScopedState';
import { recordBoardColumnsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardColumnsScopedState';
import { recordBoardFiltersScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedFiltersScopedState';
import { recordBoardSortsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedSortsScopedState';
import { savedOpportunitiesScopedState } from '@/object-record/record-board-deprecated/states/savedOpportunitiesScopedState';
import { savedPipelineStepsScopedState } from '@/object-record/record-board-deprecated/states/savedPipelineStepsScopedState';
import { savedRecordBoardDeprecatedColumnsScopedState } from '@/object-record/record-board-deprecated/states/savedRecordBoardDeprecatedColumnsScopedState';
import { savedRecordsScopedState } from '@/object-record/record-board-deprecated/states/savedRecordsScopedState';
import { hiddenRecordBoardDeprecatedCardFieldsScopedSelector } from '@/object-record/record-board-deprecated/states/selectors/hiddenRecordBoardDeprecatedCardFieldsScopedSelector';
import { recordBoardCardFieldsByKeyScopedSelector } from '@/object-record/record-board-deprecated/states/selectors/recordBoardDeprecatedCardFieldsByKeyScopedSelector';
import { selectedRecordBoardDeprecatedCardIdsScopedSelector } from '@/object-record/record-board-deprecated/states/selectors/selectedRecordBoardDeprecatedCardIdsScopedSelector';
import { visibleRecordBoardDeprecatedCardFieldsScopedSelector } from '@/object-record/record-board-deprecated/states/selectors/visibleRecordBoardDeprecatedCardFieldsScopedSelector';
import { getScopedStateDeprecated } from '@/ui/utilities/recoil-scope/utils/getScopedStateDeprecated';
export const getRecordBoardDeprecatedScopedStates = ({
recordBoardScopeId,
}: {
recordBoardScopeId: string;
}) => {
const activeCardIdsState = getScopedStateDeprecated(
activeRecordBoardDeprecatedCardIdsScopedState,
recordBoardScopeId,
);
const availableBoardCardFieldsState = getScopedStateDeprecated(
availableRecordBoardDeprecatedCardFieldsScopedState,
recordBoardScopeId,
);
const boardColumnsState = getScopedStateDeprecated(
recordBoardColumnsScopedState,
recordBoardScopeId,
);
const isBoardLoadedState = getScopedStateDeprecated(
isRecordBoardDeprecatedLoadedScopedState,
recordBoardScopeId,
);
const isCompactViewEnabledState = getScopedStateDeprecated(
isCompactViewEnabledScopedState,
recordBoardScopeId,
);
const savedBoardColumnsState = getScopedStateDeprecated(
savedRecordBoardDeprecatedColumnsScopedState,
recordBoardScopeId,
);
const boardFiltersState = getScopedStateDeprecated(
recordBoardFiltersScopedState,
recordBoardScopeId,
);
const boardSortsState = getScopedStateDeprecated(
recordBoardSortsScopedState,
recordBoardScopeId,
);
const savedCompaniesState = getScopedStateDeprecated(
savedRecordsScopedState,
recordBoardScopeId,
);
const savedOpportunitiesState = getScopedStateDeprecated(
savedOpportunitiesScopedState,
recordBoardScopeId,
);
const savedPipelineStepsState = getScopedStateDeprecated(
savedPipelineStepsScopedState,
recordBoardScopeId,
);
const onFieldsChangeState = getScopedStateDeprecated(
onFieldsChangeScopedState,
recordBoardScopeId,
);
// TODO: Family scoped selector
const boardCardFieldsByKeySelector =
recordBoardCardFieldsByKeyScopedSelector(recordBoardScopeId);
const hiddenBoardCardFieldsSelector =
hiddenRecordBoardDeprecatedCardFieldsScopedSelector({
scopeId: recordBoardScopeId,
});
const selectedCardIdsSelector =
selectedRecordBoardDeprecatedCardIdsScopedSelector({
scopeId: recordBoardScopeId,
});
const visibleBoardCardFieldsSelector =
visibleRecordBoardDeprecatedCardFieldsScopedSelector({
scopeId: recordBoardScopeId,
});
return {
activeCardIdsState,
availableBoardCardFieldsState,
boardColumnsState,
isBoardLoadedState,
isCompactViewEnabledState,
savedBoardColumnsState,
boardFiltersState,
boardSortsState,
onFieldsChangeState,
boardCardFieldsByKeySelector,
hiddenBoardCardFieldsSelector,
selectedCardIdsSelector,
visibleBoardCardFieldsSelector,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
};
};

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
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 { BoardColumnHotkeyScope } from '@/object-record/record-board-deprecated/types/BoardColumnHotkeyScope';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { IconDotsVertical } from '@/ui/display/icon';
import { Tag } from '@/ui/display/tag/components/Tag';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
@ -57,9 +57,12 @@ export const RecordBoardColumnHeader = () => {
const handleBoardColumnMenuOpen = () => {
setIsBoardColumnMenuOpen(true);
setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, {
goto: false,
});
setHotkeyScopeAndMemorizePreviousScope(
RecordBoardColumnHotkeyScope.BoardColumn,
{
goto: false,
},
);
};
const handleBoardColumnMenuClose = () => {

View File

@ -0,0 +1,3 @@
export enum RecordBoardColumnHotkeyScope {
BoardColumn = 'board-column',
}

View File

@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilState } from 'recoil';
import { mapBoardFieldDefinitionsToViewFields } from '@/companies/utils/mapBoardFieldDefinitionsToViewFields';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
@ -12,6 +11,7 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { useViews } from '@/views/hooks/internal/useViews';
import { GraphQLView } from '@/views/types/GraphQLView';
import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';

View File

@ -1,78 +0,0 @@
import { OpportunityPicker } from '@/companies/components/OpportunityPicker';
import { useCreateOpportunity } from '@/object-record/record-board-deprecated/hooks/internal/useCreateOpportunity';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { IconPlus } from '@/ui/display/icon/index';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { logError } from '~/utils/logError';
export const PipelineAddButton = () => {
const { enqueueSnackBar } = useSnackBar();
const { closeDropdown, toggleDropdown } = useDropdown(
'add-pipeline-progress',
);
const createOpportunity = useCreateOpportunity();
const handleCompanySelected = (
selectedCompany: EntityForSelect | null,
selectedPipelineStepId: string | null,
) => {
if (!selectedCompany?.id) {
enqueueSnackBar(
'There was a problem with the company selection, please retry.',
{ variant: 'error' },
);
logError('There was a problem with the company selection, please retry.');
return;
}
if (!selectedPipelineStepId) {
enqueueSnackBar(
'There was a problem with the pipeline stage selection, please retry.',
{ variant: 'error' },
);
logError('There was a problem with the pipeline step selection.');
return;
}
closeDropdown();
createOpportunity(selectedCompany.id, selectedPipelineStepId);
};
return (
<Dropdown
dropdownId="add-pipeline-progress"
clickableComponent={
<IconButton
Icon={IconPlus}
size="medium"
dataTestId="add-company-progress-button"
accent="default"
variant="secondary"
onClick={toggleDropdown}
/>
}
dropdownComponents={
<OpportunityPicker
companyId={null}
onSubmit={handleCompanySelected}
onCancel={closeDropdown}
/>
}
hotkey={{
key: 'c',
scope: PageHotkeyScope.OpportunitiesPage,
}}
dropdownHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
/>
);
};

View File

@ -1,60 +0,0 @@
import { gql } from '@apollo/client';
export const query = gql`
mutation CreateOnePipelineStep($input: PipelineStepCreateInput!) {
createPipelineStep(data: $input) {
id
name
id
createdAt
opportunities {
edges {
node {
__typename
id
}
}
}
position
color
updatedAt
}
}
`;
export const deleteQuery = gql`
mutation DeleteOnePipelineStep($idToDelete: ID!) {
deletePipelineStep(id: $idToDelete) {
id
}
}
`;
export const mockId = '8f3b2121-f194-4ba4-9fbf-2d5a37126806';
export const currentPipelineId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33';
const data = {
color: 'yellow',
id: mockId,
position: 1,
name: 'Column Title',
};
export const variables = {
input: data,
};
export const deleteVariables = { idToDelete: 'columnId' };
export const responseData = {
...data,
createdAt: '',
opportunities: {
edges: [],
},
updatedAt: '',
};
export const deleteResponseData = {
id: 'columnId',
};

View File

@ -1,104 +0,0 @@
import { ReactNode } from 'react';
import { act } from 'react-dom/test-utils';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import {
currentPipelineId,
deleteQuery,
deleteResponseData,
deleteVariables,
mockId,
query,
responseData,
variables,
} from '../__mocks__/usePipelineSteps';
import { usePipelineSteps } from '../usePipelineSteps';
const mocks = [
{
request: {
query,
variables,
},
result: jest.fn(() => ({
data: {
createPipelineStep: responseData,
},
})),
},
{
request: {
query: deleteQuery,
variables: deleteVariables,
},
result: jest.fn(() => ({
data: {
deletePipelineStep: deleteResponseData,
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
</RecoilRoot>
);
jest.mock('uuid', () => ({
v4: jest.fn(() => mockId),
}));
describe('usePipelineSteps', () => {
it('should handlePipelineStepAdd successfully', async () => {
const { result } = renderHook(
() => {
const setCurrentPipeline = useSetRecoilState(currentPipelineState);
setCurrentPipeline({ id: currentPipelineId });
return usePipelineSteps();
},
{
wrapper: Wrapper,
},
);
const boardColumn: BoardColumnDefinition = {
id: mockId,
title: 'Column Title',
colorCode: 'yellow',
position: 1,
};
await act(async () => {
const res = await result.current.handlePipelineStepAdd(boardColumn);
expect(res).toEqual(responseData);
});
});
it('should handlePipelineStepDelete successfully', async () => {
const { result } = renderHook(
() => {
const setCurrentPipeline = useSetRecoilState(currentPipelineState);
setCurrentPipeline({ id: currentPipelineId });
return usePipelineSteps();
},
{
wrapper: Wrapper,
},
);
const boardColumnId = 'columnId';
await act(async () => {
const res = await result.current.handlePipelineStepDelete(boardColumnId);
expect(res).toEqual(deleteResponseData);
});
});
});

View File

@ -1,52 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
export const usePipelineSteps = () => {
const { createOneRecord: createOnePipelineStep } =
useCreateOneRecord<PipelineStep>({
objectNameSingular: CoreObjectNameSingular.PipelineStep,
});
const { deleteOneRecord: deleteOnePipelineStep } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.PipelineStep,
});
const handlePipelineStepAdd = useRecoilCallback(
({ snapshot }) =>
(boardColumn: BoardColumnDefinition) => {
const currentPipeline = snapshot
.getLoadable(currentPipelineState)
.getValue();
if (!currentPipeline?.id) return;
return createOnePipelineStep?.({
color: boardColumn.colorCode ?? 'gray',
id: boardColumn.id,
position: boardColumn.position,
name: boardColumn.title,
});
},
[createOnePipelineStep],
);
const handlePipelineStepDelete = useRecoilCallback(
({ snapshot }) =>
(boardColumnId: string) => {
const currentPipeline = snapshot
.getLoadable(currentPipelineState)
.getValue();
if (!currentPipeline?.id) return;
return deleteOnePipelineStep?.(boardColumnId);
},
[deleteOnePipelineStep],
);
return { handlePipelineStepAdd, handlePipelineStepDelete };
};

View File

@ -1,7 +0,0 @@
import { atom } from 'recoil';
import { undefined } from 'zod';
export const currentPipelineState = atom<any | undefined>({
key: 'currentPipelineState',
default: undefined,
});

View File

@ -1,8 +0,0 @@
import { atom } from 'recoil';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
export const currentPipelineStepsState = atom<PipelineStep[]>({
key: 'currentPipelineStepsState',
default: [],
});

View File

@ -1,17 +0,0 @@
import { Person } from '@/people/types/Person';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
export type Opportunity = {
[key: string]: any;
id: string;
amount: {
amountMicros: number;
currencyCode: string;
};
closeDate: Date;
probability: number;
pipelineStepId: string;
pipelineStep: PipelineStep;
pointOfContactId: string;
pointOfContact: Pick<Person, 'id' | 'name' | 'avatarUrl'>;
};

View File

@ -1,8 +0,0 @@
export type PipelineStep = {
id: string;
name: string;
color: string;
position: number;
createdAt: string;
updatedAt: string;
};

View File

@ -1,4 +1,4 @@
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
@ -10,5 +10,5 @@ export type ViewField = {
size: number;
definition:
| ColumnDefinition<FieldMetadata>
| BoardFieldDefinition<FieldMetadata>;
| RecordBoardFieldDefinition<FieldMetadata>;
};

View File

@ -1,10 +1,10 @@
import { mapBoardFieldDefinitionsToViewFields } from '@/companies/utils/mapBoardFieldDefinitionsToViewFields';
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields';
describe('mapBoardFieldDefinitionsToViewFields', () => {
it('should map board field definitions to view fields', () => {
const fieldDefinitions: BoardFieldDefinition<FieldMetadata>[] = [
const fieldDefinitions: RecordBoardFieldDefinition<FieldMetadata>[] = [
{
fieldMetadataId: 'fieldMetadataId',
label: 'label',

View File

@ -1,6 +1,5 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ViewField } from '@/views/types/ViewField';
@ -8,7 +7,6 @@ import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { ViewSort } from '@/views/types/ViewSort';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { mapViewFieldsToBoardFieldDefinitions } from '@/views/utils/mapViewFieldsToBoardFieldDefinitions';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
@ -192,84 +190,6 @@ describe('mapViewFieldsToColumnDefinitions', () => {
});
});
describe('mapViewFieldsToBoardFieldDefinitions', () => {
it('should map visible ViewFields to BoardFieldDefinitions and filter out missing fieldMetadata', () => {
const viewFields = [
{
id: 1,
fieldMetadataId: 1,
position: 1,
isVisible: true,
},
{
id: 2,
fieldMetadataId: 2,
position: 2,
isVisible: false,
},
{
id: 3,
fieldMetadataId: 3,
position: 3,
isVisible: true,
},
];
const fieldsMetadata = [
{
fieldMetadataId: 1,
label: 'Field 1',
metadata: {},
position: 1,
infoTooltipContent: 'Tooltip content for Field 1',
iconName: 'icon-field-1',
type: 'string',
},
{
fieldMetadataId: 3,
label: 'Field 3',
metadata: {},
position: 3,
infoTooltipContent: 'Tooltip for Field 3',
iconName: 'icon-field-3',
type: 'number',
},
];
const expectedBoardFieldDefinitions = [
{
fieldMetadataId: 1,
label: 'Field 1',
metadata: {},
position: 1,
infoTooltipContent: 'Tooltip content for Field 1',
iconName: 'icon-field-1',
type: 'string',
isVisible: true,
viewFieldId: 1,
},
{
fieldMetadataId: 3,
label: 'Field 3',
metadata: {},
position: 3,
infoTooltipContent: 'Tooltip for Field 3',
iconName: 'icon-field-3',
type: 'number',
isVisible: true,
viewFieldId: 3,
},
];
const actualBoardFieldDefinitions = mapViewFieldsToBoardFieldDefinitions(
viewFields as unknown as ViewField[],
fieldsMetadata as unknown as BoardFieldDefinition<FieldMetadata>[],
);
expect(actualBoardFieldDefinitions).toEqual(expectedBoardFieldDefinitions);
});
});
describe('mapColumnDefinitionsToViewFields', () => {
it('should map ColumnDefinitions to ViewFields, setting defaults and using viewFieldId if present', () => {
const columnDefinitions = [

Some files were not shown because too many files have changed in this diff Show More