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
109 changed files with 18 additions and 4941 deletions

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';