2358 refactor entityboard to recordboard (#2652)
* renaming * wip * merge BoardColumn and RecordBoardColumn * merge files * remove unnecessary export * Fix lint --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,120 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
|
||||
import { BoardColumnContext } from '../contexts/BoardColumnContext';
|
||||
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
|
||||
|
||||
import { BoardColumnMenu } from './BoardColumnMenu';
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
export type BoardColumnProps = {
|
||||
onDelete?: (id: string) => void;
|
||||
onTitleEdit: (title: string, color: string) => void;
|
||||
totalAmount?: number;
|
||||
children: React.ReactNode;
|
||||
numChildren: number;
|
||||
stageId: string;
|
||||
};
|
||||
|
||||
export const BoardColumn = ({
|
||||
onDelete,
|
||||
onTitleEdit,
|
||||
totalAmount,
|
||||
children,
|
||||
numChildren,
|
||||
stageId,
|
||||
}: BoardColumnProps) => {
|
||||
const boardColumn = useContext(BoardColumnContext);
|
||||
|
||||
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] =
|
||||
React.useState(false);
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const handleTitleClick = () => {
|
||||
setIsBoardColumnMenuOpen(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, {
|
||||
goto: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsBoardColumnMenuOpen(false);
|
||||
};
|
||||
|
||||
if (!boardColumn) return <></>;
|
||||
|
||||
const { isFirstColumn, columnDefinition } = boardColumn;
|
||||
|
||||
return (
|
||||
<StyledColumn isFirstColumn={isFirstColumn}>
|
||||
<StyledHeader>
|
||||
<Tag
|
||||
onClick={handleTitleClick}
|
||||
color={columnDefinition.colorCode ?? 'gray'}
|
||||
text={columnDefinition.title}
|
||||
/>
|
||||
{!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>}
|
||||
<StyledNumChildren>{numChildren}</StyledNumChildren>
|
||||
</StyledHeader>
|
||||
{isBoardColumnMenuOpen && (
|
||||
<BoardColumnMenu
|
||||
onClose={handleClose}
|
||||
onDelete={onDelete}
|
||||
onTitleEdit={onTitleEdit}
|
||||
stageId={stageId}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</StyledColumn>
|
||||
);
|
||||
};
|
||||
@ -1,147 +0,0 @@
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
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 { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
|
||||
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`
|
||||
${textInputStyle}
|
||||
|
||||
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%;
|
||||
`;
|
||||
|
||||
export type BoardColumnEditTitleMenuProps = {
|
||||
onClose: () => void;
|
||||
onDelete?: (id: string) => void;
|
||||
title: string;
|
||||
onTitleEdit: (title: string, color: string) => void;
|
||||
color: ThemeColor;
|
||||
stageId: string;
|
||||
};
|
||||
|
||||
type ColumnColorOption = {
|
||||
name: string;
|
||||
id: ThemeColor;
|
||||
};
|
||||
|
||||
export const COLUMN_COLOR_OPTIONS: ColumnColorOption[] = [
|
||||
{ name: 'Green', id: 'green' },
|
||||
{ name: 'Turquoise', id: 'turquoise' },
|
||||
{ name: 'Sky', id: 'sky' },
|
||||
{ name: 'Blue', id: 'blue' },
|
||||
{ name: 'Purple', id: 'purple' },
|
||||
{ name: 'Pink', id: 'pink' },
|
||||
{ name: 'Red', id: 'red' },
|
||||
{ name: 'Orange', id: 'orange' },
|
||||
{ name: 'Yellow', id: 'yellow' },
|
||||
{ name: 'Gray', id: 'gray' },
|
||||
];
|
||||
|
||||
export const BoardColumnEditTitleMenu = ({
|
||||
onClose,
|
||||
onDelete,
|
||||
stageId,
|
||||
onTitleEdit,
|
||||
title,
|
||||
color,
|
||||
}: BoardColumnEditTitleMenuProps) => {
|
||||
const [internalValue, setInternalValue] = useState(title);
|
||||
const [, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||
const debouncedOnUpdateTitle = debounce(
|
||||
(newTitle) => onTitleEdit(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, 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 />
|
||||
{COLUMN_COLOR_OPTIONS.map((colorOption) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorOption.name}
|
||||
onClick={() => {
|
||||
handleColorChange(colorOption.id);
|
||||
}}
|
||||
color={colorOption.id}
|
||||
selected={colorOption.id === color}
|
||||
text={colorOption.name}
|
||||
/>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem
|
||||
onClick={handleDelete}
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete"
|
||||
accent="danger"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -1,186 +0,0 @@
|
||||
import { useCallback, useContext, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
} from '@/ui/display/icon';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||
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 { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
import { BoardColumnContext } from '../contexts/BoardColumnContext';
|
||||
import { useBoardColumns } from '../hooks/useBoardColumns';
|
||||
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
|
||||
|
||||
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
|
||||
const StyledMenuContainer = styled.div`
|
||||
position: absolute;
|
||||
top: ${({ theme }) => theme.spacing(10)};
|
||||
width: 200px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
type BoardColumnMenuProps = {
|
||||
onClose: () => void;
|
||||
onDelete?: (id: string) => void;
|
||||
onTitleEdit: (title: string, color: string) => void;
|
||||
stageId: string;
|
||||
};
|
||||
|
||||
type Menu = 'actions' | 'add' | 'title';
|
||||
|
||||
export const BoardColumnMenu = ({
|
||||
onClose,
|
||||
onDelete,
|
||||
onTitleEdit,
|
||||
stageId,
|
||||
}: BoardColumnMenuProps) => {
|
||||
const [currentMenu, setCurrentMenu] = useState('actions');
|
||||
const column = useContext(BoardColumnContext);
|
||||
|
||||
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { handleMoveBoardColumn } = useBoardColumns();
|
||||
|
||||
const handleCompanySelected = (
|
||||
selectedCompany: EntityForSelect | null | undefined,
|
||||
) => {
|
||||
if (!selectedCompany?.id) {
|
||||
enqueueSnackBar(
|
||||
'There was a problem with the company selection, please retry.',
|
||||
{
|
||||
variant: 'error',
|
||||
},
|
||||
);
|
||||
|
||||
logError('There was a problem with the company selection, please retry.');
|
||||
return;
|
||||
}
|
||||
|
||||
//createCompanyProgress(selectedCompany.id, stageId);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
onClose();
|
||||
}, [goBackToPreviousHotkeyScope, onClose]);
|
||||
|
||||
const setMenu = (menu: Menu) => {
|
||||
if (menu === 'add') {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
);
|
||||
}
|
||||
setCurrentMenu(menu);
|
||||
};
|
||||
const [relationPickerSearchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
// const companies = useFilteredSearchCompanyQuery({
|
||||
// searchFilter: relationPickerSearchFilter,
|
||||
// });
|
||||
|
||||
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' && (
|
||||
<BoardColumnEditTitleMenu
|
||||
color={columnDefinition.colorCode ?? 'gray'}
|
||||
onClose={closeMenu}
|
||||
onTitleEdit={onTitleEdit}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -1,37 +0,0 @@
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { Dropdown } from '../../dropdown/components/Dropdown';
|
||||
import { DropdownScope } from '../../dropdown/scopes/DropdownScope';
|
||||
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
|
||||
|
||||
import { BoardOptionsDropdownId } from './constants/BoardOptionsDropdownId';
|
||||
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
||||
import {
|
||||
BoardOptionsDropdownContent,
|
||||
BoardOptionsDropdownContentProps,
|
||||
} from './BoardOptionsDropdownContent';
|
||||
|
||||
type BoardOptionsDropdownProps = Pick<
|
||||
BoardOptionsDropdownContentProps,
|
||||
'onStageAdd'
|
||||
>;
|
||||
|
||||
export const BoardOptionsDropdown = ({
|
||||
onStageAdd,
|
||||
}: BoardOptionsDropdownProps) => {
|
||||
const { setViewEditMode } = useView();
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={BoardOptionsDropdownId}>
|
||||
<Dropdown
|
||||
clickableComponent={<BoardOptionsDropdownButton />}
|
||||
dropdownComponents={
|
||||
<BoardOptionsDropdownContent onStageAdd={onStageAdd} />
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
|
||||
onClickOutside={() => setViewEditMode('none')}
|
||||
dropdownMenuWidth={170}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
export const BoardOptionsDropdownButton = () => {
|
||||
const { isDropdownOpen, toggleDropdown } = useDropdown();
|
||||
|
||||
const handleClick = () => {
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownOpen}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Options
|
||||
</StyledHeaderDropdownButton>
|
||||
);
|
||||
};
|
||||
@ -1,245 +0,0 @@
|
||||
import { useCallback, useContext, 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 { BoardContext } from '@/companies/states/contexts/BoardContext';
|
||||
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 { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { useBoardCardFields } from '../hooks/useBoardCardFields';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { isCompactViewEnabledState } from '../states/isCompactViewEnabledState';
|
||||
import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector';
|
||||
import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector';
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
|
||||
|
||||
export type BoardOptionsDropdownContentProps = {
|
||||
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||
};
|
||||
|
||||
type BoardOptionsMenu = 'fields' | 'stage-creation' | 'stages';
|
||||
|
||||
type ColumnForCreate = {
|
||||
id: string;
|
||||
colorCode: ThemeColor;
|
||||
position: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const BoardOptionsDropdownContent = ({
|
||||
onStageAdd,
|
||||
}: BoardOptionsDropdownContentProps) => {
|
||||
const { setViewEditMode, handleViewNameSubmit } = useView();
|
||||
const { viewEditModeState, currentViewSelector } = useViewScopedStates();
|
||||
const { BoardRecoilScopeContext } = useContext(BoardContext);
|
||||
|
||||
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 [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||
const [isCompactViewEnabled, setIsCompactViewEnabled] = useRecoilState(
|
||||
isCompactViewEnabledState,
|
||||
);
|
||||
|
||||
const hiddenBoardCardFields = useRecoilScopedValue(
|
||||
hiddenBoardCardFieldsScopedSelector,
|
||||
BoardRecoilScopeContext,
|
||||
);
|
||||
|
||||
const hasHiddenFields = hiddenBoardCardFields.length > 0;
|
||||
const visibleBoardCardFields = useRecoilScopedValue(
|
||||
visibleBoardCardFieldsScopedSelector,
|
||||
BoardRecoilScopeContext,
|
||||
);
|
||||
const hasVisibleFields = visibleBoardCardFields.length > 0;
|
||||
|
||||
const handleStageSubmit = () => {
|
||||
if (currentMenu !== 'stage-creation' || !stageInputRef?.current?.value)
|
||||
return;
|
||||
|
||||
const columnToCreate: ColumnForCreate = {
|
||||
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 } =
|
||||
useBoardCardFields();
|
||||
|
||||
const { closeDropdown } = useDropdown();
|
||||
|
||||
const handleReorderField: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = [...visibleBoardCardFields];
|
||||
const [removed] = reorderFields.splice(result.source.index - 1, 1);
|
||||
reorderFields.splice(result.destination.index - 1, 0, removed);
|
||||
|
||||
handleFieldsReorder(reorderFields);
|
||||
},
|
||||
[handleFieldsReorder, visibleBoardCardFields],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
setViewEditMode('none');
|
||||
closeDropdown();
|
||||
},
|
||||
BoardOptionsHotkeyScope.Dropdown,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
const name = viewEditInputRef.current?.value;
|
||||
resetMenu();
|
||||
setViewEditMode('none');
|
||||
closeDropdown();
|
||||
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}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={true}
|
||||
onDragEnd={handleReorderField}
|
||||
/>
|
||||
)}
|
||||
{hasVisibleFields && hasHiddenFields && <DropdownMenuSeparator />}
|
||||
{hasHiddenFields && (
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Hidden"
|
||||
fields={hiddenBoardCardFields}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,160 +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 { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
|
||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { StyledBoard } from '@/ui/layout/board/components/StyledBoard';
|
||||
import { BoardColumnContext } from '@/ui/layout/board/contexts/BoardColumnContext';
|
||||
import { useSetCardSelected } from '@/ui/layout/board/hooks/useSetCardSelected';
|
||||
import { useUpdateBoardCardIds } from '@/ui/layout/board/hooks/useUpdateBoardCardIds';
|
||||
import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
import { BoardColumnRecoilScopeContext } from '../states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
import { EntityBoardColumn } from './EntityBoardColumn';
|
||||
|
||||
export type EntityBoardProps = {
|
||||
boardOptions: BoardOptions;
|
||||
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||
onColumnDelete?: (boardColumnId: string) => void;
|
||||
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
|
||||
};
|
||||
|
||||
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 EntityBoard = ({
|
||||
boardOptions,
|
||||
onColumnDelete,
|
||||
onEditColumnTitle,
|
||||
}: EntityBoardProps) => {
|
||||
const boardColumns = useRecoilValue(boardColumnsState);
|
||||
|
||||
const { updateOneObject: updateOneOpportunity } =
|
||||
useUpdateOneObjectRecord<Opportunity>({
|
||||
objectNameSingular: 'opportunity',
|
||||
});
|
||||
|
||||
const { unselectAllActiveCards, setCardSelected } = useSetCardSelected();
|
||||
|
||||
const updatePipelineProgressStageInDB = useCallback(
|
||||
async (pipelineProgressId: string, pipelineStepId: string) => {
|
||||
await updateOneOpportunity?.({
|
||||
idToUpdate: pipelineProgressId,
|
||||
input: {
|
||||
pipelineStepId: pipelineStepId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateOneOpportunity],
|
||||
);
|
||||
|
||||
useListenClickOutsideByClassName({
|
||||
classNames: ['entity-board-card'],
|
||||
excludeClassNames: ['action-bar', 'context-menu'],
|
||||
callback: unselectAllActiveCards,
|
||||
});
|
||||
|
||||
const updateBoardCardIds = useUpdateBoardCardIds();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
async (result) => {
|
||||
if (!boardColumns) return;
|
||||
|
||||
updateBoardCardIds(result);
|
||||
|
||||
try {
|
||||
const draggedEntityId = result.draggableId;
|
||||
const destinationColumnId = result.destination?.droppableId;
|
||||
|
||||
// TODO: abstract
|
||||
if (
|
||||
draggedEntityId &&
|
||||
destinationColumnId &&
|
||||
updatePipelineProgressStageInDB
|
||||
) {
|
||||
await updatePipelineProgressStageInDB(
|
||||
draggedEntityId,
|
||||
destinationColumnId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
},
|
||||
[boardColumns, updatePipelineProgressStageInDB, updateBoardCardIds],
|
||||
);
|
||||
|
||||
const sortedBoardColumns = [...boardColumns].sort((a, b) => {
|
||||
return a.position - b.position;
|
||||
});
|
||||
|
||||
const boardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useScopedHotkeys(
|
||||
'escape',
|
||||
unselectAllActiveCards,
|
||||
PageHotkeyScope.OpportunitiesPage,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledBoardHeader />
|
||||
<ScrollWrapper>
|
||||
<StyledBoard ref={boardRef}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{sortedBoardColumns.map((column) => (
|
||||
<BoardColumnContext.Provider
|
||||
key={column.id}
|
||||
value={{
|
||||
id: column.id,
|
||||
columnDefinition: column,
|
||||
isFirstColumn: column.position === 0,
|
||||
isLastColumn:
|
||||
column.position === sortedBoardColumns.length - 1,
|
||||
}}
|
||||
>
|
||||
<RecoilScope
|
||||
CustomRecoilScopeContext={BoardColumnRecoilScopeContext}
|
||||
key={column.id}
|
||||
>
|
||||
<EntityBoardColumn
|
||||
boardOptions={boardOptions}
|
||||
onDelete={onColumnDelete}
|
||||
onTitleEdit={onEditColumnTitle}
|
||||
/>
|
||||
</RecoilScope>
|
||||
</BoardColumnContext.Provider>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
</ScrollWrapper>
|
||||
<DragSelect
|
||||
dragSelectable={boardRef}
|
||||
onDragSelectionChange={setCardSelected}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
|
||||
|
||||
import { selectedCardIdsSelector } from '../states/selectors/selectedCardIdsSelector';
|
||||
|
||||
export const EntityBoardActionBar = () => {
|
||||
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
|
||||
return <ActionBar selectedIds={selectedCardIds}></ActionBar>;
|
||||
};
|
||||
@ -1,53 +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 { useCurrentCardSelected } from '../hooks/useCurrentCardSelected';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
export const EntityBoardCard = ({
|
||||
boardOptions,
|
||||
cardId,
|
||||
index,
|
||||
}: {
|
||||
boardOptions: BoardOptions;
|
||||
cardId: string;
|
||||
index: number;
|
||||
}) => {
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
||||
|
||||
const { setCurrentCardSelected } = useCurrentCardSelected();
|
||||
|
||||
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}
|
||||
>
|
||||
{<boardOptions.CardComponent />}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
@ -1,123 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { BoardColumn } from '@/ui/layout/board/components/BoardColumn';
|
||||
import { BoardCardIdContext } from '@/ui/layout/board/contexts/BoardCardIdContext';
|
||||
import { BoardColumnContext } from '@/ui/layout/board/contexts/BoardColumnContext';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
|
||||
import { boardColumnTotalsFamilySelector } from '../states/selectors/boardColumnTotalsFamilySelector';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
import { EntityBoardCard } from './EntityBoardCard';
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
type BoardColumnCardsContainerProps = {
|
||||
children: React.ReactNode;
|
||||
droppableProvided: DroppableProvided;
|
||||
};
|
||||
|
||||
type EntityBoardColumnProps = {
|
||||
boardOptions: BoardOptions;
|
||||
onDelete?: (columnId: string) => void;
|
||||
onTitleEdit: (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 EntityBoardColumn = ({
|
||||
boardOptions,
|
||||
onDelete,
|
||||
onTitleEdit,
|
||||
}: EntityBoardColumnProps) => {
|
||||
const column = useContext(BoardColumnContext);
|
||||
|
||||
const boardColumnId = column?.id || '';
|
||||
|
||||
const boardColumnTotal = useRecoilValue(
|
||||
boardColumnTotalsFamilySelector(boardColumnId),
|
||||
);
|
||||
|
||||
const cardIds = useRecoilValue(
|
||||
boardCardIdsByColumnIdFamilyState(boardColumnId),
|
||||
);
|
||||
|
||||
const handleTitleEdit = (title: string, color: string) => {
|
||||
onTitleEdit(boardColumnId, title, color);
|
||||
};
|
||||
|
||||
if (!column) return <></>;
|
||||
|
||||
return (
|
||||
<Droppable droppableId={column.id}>
|
||||
{(droppableProvided) => (
|
||||
<BoardColumn
|
||||
onTitleEdit={handleTitleEdit}
|
||||
onDelete={onDelete}
|
||||
totalAmount={boardColumnTotal}
|
||||
numChildren={cardIds.length}
|
||||
stageId={column.id}
|
||||
>
|
||||
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
|
||||
{cardIds.map((cardId, index) => (
|
||||
<BoardCardIdContext.Provider value={cardId} key={cardId}>
|
||||
<EntityBoardCard
|
||||
index={index}
|
||||
cardId={cardId}
|
||||
boardOptions={boardOptions}
|
||||
/>
|
||||
</BoardCardIdContext.Provider>
|
||||
))}
|
||||
<Draggable
|
||||
draggableId={`new-${column.id}`}
|
||||
index={cardIds.length}
|
||||
isDragDisabled={true}
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
ref={draggableProvided?.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.draggableProps}
|
||||
>
|
||||
<StyledNewCardButtonContainer>
|
||||
<RecoilScope>{boardOptions.newCardComponent}</RecoilScope>
|
||||
</StyledNewCardButtonContainer>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
</BoardColumnCardsContainer>
|
||||
</BoardColumn>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
|
||||
|
||||
import { selectedCardIdsSelector } from '../states/selectors/selectedCardIdsSelector';
|
||||
|
||||
export const EntityBoardContextMenu = () => {
|
||||
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
|
||||
return <ContextMenu selectedIds={selectedCardIds}></ContextMenu>;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export 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)};
|
||||
`;
|
||||
@ -1,26 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import {
|
||||
BoardColumnEditTitleMenu,
|
||||
COLUMN_COLOR_OPTIONS,
|
||||
} from '../BoardColumnEditTitleMenu';
|
||||
|
||||
const meta: Meta<typeof BoardColumnEditTitleMenu> = {
|
||||
title: 'UI/Layout/Board/BoardColumnMenu',
|
||||
component: BoardColumnEditTitleMenu,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
color: {
|
||||
control: 'select',
|
||||
options: COLUMN_COLOR_OPTIONS.map(({ id }) => id),
|
||||
},
|
||||
},
|
||||
args: { color: 'green', title: 'Column title' },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof BoardColumnEditTitleMenu>;
|
||||
|
||||
export const AllTags: Story = {};
|
||||
@ -1,2 +0,0 @@
|
||||
// We should either apply the constant all caps case or maybe define a more general enum to store those ids ?
|
||||
export const BoardOptionsDropdownId = 'board-options';
|
||||
@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardCardIdContext = createContext<string | null>(null);
|
||||
@ -1,12 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
|
||||
type BoardColumn = {
|
||||
id: string;
|
||||
columnDefinition: BoardColumnDefinition;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
};
|
||||
|
||||
export const BoardColumnContext = createContext<BoardColumn | null>(null);
|
||||
@ -1,5 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { BoardOptions } from '@/ui/layout/board/types/BoardOptions';
|
||||
|
||||
export const BoardOptionsContext = createContext<BoardOptions | null>(null);
|
||||
@ -1,27 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { IconTrash } from '@/ui/display/icon';
|
||||
import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSelectedBoardCards';
|
||||
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
|
||||
|
||||
export const useBoardActionBarEntries = () => {
|
||||
const setActionBarEntriesRecoil = useSetRecoilState(actionBarEntriesState);
|
||||
|
||||
const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
|
||||
|
||||
const setActionBarEntries = useCallback(() => {
|
||||
setActionBarEntriesRecoil([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: deleteSelectedBoardCards,
|
||||
},
|
||||
]);
|
||||
}, [deleteSelectedBoardCards, setActionBarEntriesRecoil]);
|
||||
|
||||
return {
|
||||
setActionBarEntries,
|
||||
};
|
||||
};
|
||||
@ -1,61 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { savedBoardCardFieldsFamilyState } from '@/ui/layout/board/states/savedBoardCardFieldsFamilyState';
|
||||
import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition';
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
|
||||
|
||||
import { useBoardContext } from './useBoardContext';
|
||||
|
||||
export const useBoardCardFields = () => {
|
||||
const { BoardRecoilScopeContext, onFieldsChange } = useBoardContext();
|
||||
|
||||
const [, setBoardCardFields] = useRecoilScopedState(
|
||||
boardCardFieldsScopedState,
|
||||
BoardRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setSavedBoardCardFields] = useRecoilScopedState(
|
||||
savedBoardCardFieldsFamilyState,
|
||||
BoardRecoilScopeContext,
|
||||
);
|
||||
|
||||
const handleFieldVisibilityChange = (
|
||||
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => {
|
||||
setBoardCardFields((previousFields) =>
|
||||
previousFields.map((previousField) =>
|
||||
previousField.fieldMetadataId === field.fieldMetadataId
|
||||
? { ...previousField, isVisible: !field.isVisible }
|
||||
: previousField,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFieldsChange = useCallback(
|
||||
async (fields: BoardFieldDefinition<FieldMetadata>[]) => {
|
||||
setSavedBoardCardFields(fields);
|
||||
setBoardCardFields(fields);
|
||||
|
||||
await onFieldsChange?.(fields);
|
||||
},
|
||||
[setBoardCardFields, setSavedBoardCardFields, onFieldsChange],
|
||||
);
|
||||
|
||||
const handleFieldsReorder = useCallback(
|
||||
async (fields: BoardFieldDefinition<FieldMetadata>[]) => {
|
||||
const updatedFields = fields.map((column, index) => ({
|
||||
...column,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
await handleFieldsChange(updatedFields);
|
||||
},
|
||||
[handleFieldsChange],
|
||||
);
|
||||
|
||||
return { handleFieldVisibilityChange, handleFieldsReorder };
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
|
||||
import { PipelineStep } from '@/pipeline/types/PipelineStep';
|
||||
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
|
||||
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
|
||||
export const useBoardColumns = () => {
|
||||
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||
|
||||
const { handleColumnMove } = useMoveViewColumns();
|
||||
|
||||
const { updateOneObject: updateOnePipelineStep } =
|
||||
useUpdateOneObjectRecord<PipelineStep>({
|
||||
objectNameSingular: 'pipelineStep',
|
||||
});
|
||||
|
||||
const updatedPipelineSteps = (stages: BoardColumnDefinition[]) => {
|
||||
if (!stages.length) return;
|
||||
|
||||
return Promise.all(
|
||||
stages.map((stage) =>
|
||||
updateOnePipelineStep?.({
|
||||
idToUpdate: stage.id,
|
||||
input: {
|
||||
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 };
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { BoardContext } from '@/companies/states/contexts/BoardContext';
|
||||
|
||||
export const useBoardContext = () => {
|
||||
return useContext(BoardContext);
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { IconTrash } from '@/ui/display/icon';
|
||||
import { useDeleteSelectedBoardCards } from '@/ui/layout/board/hooks/useDeleteSelectedBoardCards';
|
||||
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
|
||||
|
||||
export const useBoardContextMenuEntries = () => {
|
||||
const setContextMenuEntriesRecoil = useSetRecoilState(
|
||||
contextMenuEntriesState,
|
||||
);
|
||||
|
||||
const deleteSelectedBoardCards = useDeleteSelectedBoardCards();
|
||||
|
||||
const setContextMenuEntries = useCallback(() => {
|
||||
setContextMenuEntriesRecoil([
|
||||
{
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: deleteSelectedBoardCards,
|
||||
},
|
||||
]);
|
||||
}, [deleteSelectedBoardCards, setContextMenuEntriesRecoil]);
|
||||
|
||||
return {
|
||||
setContextMenuEntries,
|
||||
};
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { activeCardIdsState } from '@/ui/layout/board/states/activeCardIdsState';
|
||||
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
||||
|
||||
import { BoardCardIdContext } from '../contexts/BoardCardIdContext';
|
||||
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
||||
|
||||
export const useCurrentCardSelected = () => {
|
||||
const currentCardId = useContext(BoardCardIdContext);
|
||||
|
||||
const isCurrentCardSelected = useRecoilValue(
|
||||
isCardSelectedFamilyState(currentCardId ?? ''),
|
||||
);
|
||||
|
||||
const setActiveCardIds = useSetRecoilState(activeCardIdsState);
|
||||
|
||||
const setCurrentCardSelected = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(selected: boolean) => {
|
||||
if (!currentCardId) return;
|
||||
|
||||
set(isCardSelectedFamilyState(currentCardId), selected);
|
||||
set(actionBarOpenState, selected);
|
||||
|
||||
if (selected) {
|
||||
setActiveCardIds((prevActiveCardIds) => [
|
||||
...prevActiveCardIds,
|
||||
currentCardId,
|
||||
]);
|
||||
} else {
|
||||
setActiveCardIds((prevActiveCardIds) =>
|
||||
prevActiveCardIds.filter((id) => id !== currentCardId),
|
||||
);
|
||||
}
|
||||
},
|
||||
[currentCardId, setActiveCardIds],
|
||||
);
|
||||
|
||||
return {
|
||||
isCurrentCardSelected,
|
||||
setCurrentCardSelected,
|
||||
};
|
||||
};
|
||||
@ -1,41 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord';
|
||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||
|
||||
import { selectedCardIdsSelector } from '../states/selectors/selectedCardIdsSelector';
|
||||
|
||||
import { useRemoveCardIds } from './useRemoveCardIds';
|
||||
|
||||
export const useDeleteSelectedBoardCards = () => {
|
||||
const removeCardIds = useRemoveCardIds();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { deleteOneObject: deleteOneOpportunity } =
|
||||
useDeleteOneObjectRecord<Opportunity>({
|
||||
objectNameSingular: 'opportunity',
|
||||
});
|
||||
|
||||
const deleteSelectedBoardCards = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const selectedCardIds = snapshot
|
||||
.getLoadable(selectedCardIdsSelector)
|
||||
.getValue();
|
||||
|
||||
await Promise.all(
|
||||
selectedCardIds.map(async (id) => {
|
||||
await deleteOneOpportunity?.(id);
|
||||
}),
|
||||
);
|
||||
removeCardIds(selectedCardIds);
|
||||
selectedCardIds.forEach((id) => {
|
||||
apolloClient.cache.evict({ id: `Opportunity:${id}` });
|
||||
});
|
||||
},
|
||||
[apolloClient.cache, removeCardIds, deleteOneOpportunity],
|
||||
);
|
||||
|
||||
return deleteSelectedBoardCards;
|
||||
};
|
||||
@ -1,26 +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 { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
|
||||
export const useRemoveCardIds = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(cardIdToRemove: string[]) => {
|
||||
const boardColumns = snapshot
|
||||
.getLoadable(boardColumnsState)
|
||||
.valueOrThrow();
|
||||
|
||||
boardColumns.forEach((boardColumn) => {
|
||||
const columnCardIds = snapshot
|
||||
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))
|
||||
.valueOrThrow();
|
||||
set(
|
||||
boardCardIdsByColumnIdFamilyState(boardColumn.id),
|
||||
columnCardIds.filter((cardId) => !cardIdToRemove.includes(cardId)),
|
||||
);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -1,47 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
||||
|
||||
import { activeCardIdsState } from '../states/activeCardIdsState';
|
||||
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
||||
|
||||
export const useSetCardSelected = () => {
|
||||
const setCardSelected = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(cardId: string, selected: boolean) => {
|
||||
const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents;
|
||||
|
||||
set(isCardSelectedFamilyState(cardId), selected);
|
||||
set(actionBarOpenState, selected || activeCardIds.length > 0);
|
||||
|
||||
if (selected) {
|
||||
set(activeCardIdsState, [...activeCardIds, cardId]);
|
||||
} else {
|
||||
set(
|
||||
activeCardIdsState,
|
||||
activeCardIds.filter((id: string) => id !== cardId),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const unselectAllActiveCards = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents;
|
||||
|
||||
activeCardIds.forEach((cardId: string) => {
|
||||
set(isCardSelectedFamilyState(cardId), false);
|
||||
});
|
||||
|
||||
set(activeCardIdsState, []);
|
||||
set(actionBarOpenState, false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
setCardSelected,
|
||||
unselectAllActiveCards,
|
||||
};
|
||||
};
|
||||
@ -1,85 +0,0 @@
|
||||
import { DropResult } 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 { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
|
||||
export const useUpdateBoardCardIds = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(result: DropResult) => {
|
||||
const currentBoardColumns = snapshot
|
||||
.getLoadable(boardColumnsState)
|
||||
.valueOrThrow();
|
||||
|
||||
const newBoardColumns = [...currentBoardColumns];
|
||||
|
||||
const { destination, source } = result;
|
||||
|
||||
if (!destination) return;
|
||||
|
||||
const sourceColumnIndex = newBoardColumns.findIndex(
|
||||
(boardColumn: BoardColumnDefinition) =>
|
||||
boardColumn.id === source.droppableId,
|
||||
);
|
||||
|
||||
const sourceColumn = newBoardColumns[sourceColumnIndex];
|
||||
|
||||
const destinationColumnIndex = newBoardColumns.findIndex(
|
||||
(boardColumn: BoardColumnDefinition) =>
|
||||
boardColumn.id === destination.droppableId,
|
||||
);
|
||||
|
||||
const destinationColumn = newBoardColumns[destinationColumnIndex];
|
||||
|
||||
if (!destinationColumn || !sourceColumn) return;
|
||||
|
||||
const sourceCardIds = [
|
||||
...snapshot
|
||||
.getLoadable(boardCardIdsByColumnIdFamilyState(sourceColumn.id))
|
||||
.valueOrThrow(),
|
||||
];
|
||||
|
||||
const destinationCardIds = [
|
||||
...snapshot
|
||||
.getLoadable(
|
||||
boardCardIdsByColumnIdFamilyState(destinationColumn.id),
|
||||
)
|
||||
.valueOrThrow(),
|
||||
];
|
||||
|
||||
const destinationIndex =
|
||||
destination.index >= destinationCardIds.length
|
||||
? destinationCardIds.length - 1
|
||||
: destination.index;
|
||||
|
||||
if (sourceColumn.id === destinationColumn.id) {
|
||||
const [deletedCardId] = sourceCardIds.splice(source.index, 1);
|
||||
|
||||
sourceCardIds.splice(destinationIndex, 0, deletedCardId);
|
||||
|
||||
set(
|
||||
boardCardIdsByColumnIdFamilyState(sourceColumn.id),
|
||||
sourceCardIds,
|
||||
);
|
||||
} else {
|
||||
const [removedCardId] = sourceCardIds.splice(source.index, 1);
|
||||
|
||||
destinationCardIds.splice(destinationIndex, 0, removedCardId);
|
||||
|
||||
set(
|
||||
boardCardIdsByColumnIdFamilyState(sourceColumn.id),
|
||||
sourceCardIds,
|
||||
);
|
||||
|
||||
set(
|
||||
boardCardIdsByColumnIdFamilyState(destinationColumn.id),
|
||||
destinationCardIds,
|
||||
);
|
||||
}
|
||||
|
||||
return newBoardColumns;
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const activeCardIdsState = atom<string[]>({
|
||||
key: 'activeCardIdsState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||
|
||||
export const availableBoardCardFieldsScopedState = createScopedState<
|
||||
BoardFieldDefinition<FieldMetadata>[]
|
||||
>({
|
||||
key: 'availableBoardCardFieldsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||
|
||||
export const boardCardFieldsScopedState = atomFamily<
|
||||
BoardFieldDefinition<FieldMetadata>[],
|
||||
string
|
||||
>({
|
||||
key: 'boardCardFieldsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const boardCardIdsByColumnIdFamilyState = atomFamily<string[], string>({
|
||||
key: 'boardCardIdsByColumnIdFamilyState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefinition';
|
||||
|
||||
export const boardColumnsState = atom<BoardColumnDefinition[]>({
|
||||
key: 'boardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isBoardLoadedState = atom<boolean>({
|
||||
key: 'isBoardLoadedState',
|
||||
default: false,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCardInCompactViewState = atomFamily<boolean, string>({
|
||||
key: 'isCardInCompactViewState',
|
||||
default: true,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCardSelectedFamilyState = atomFamily<boolean, string>({
|
||||
key: 'isCardSelectedFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isCompactViewEnabledState = atom<boolean>({
|
||||
key: 'isCompactViewEnabledState',
|
||||
default: false,
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardColumnRecoilScopeContext = createContext<string | null>(null);
|
||||
@ -1,13 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||
|
||||
export const savedBoardCardFieldsFamilyState = atomFamily<
|
||||
BoardFieldDefinition<FieldMetadata>[],
|
||||
string | undefined
|
||||
>({
|
||||
key: 'savedBoardCardFieldsFamilyState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
|
||||
export const savedBoardColumnsState = atom<BoardColumnDefinition[]>({
|
||||
key: 'savedBoardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
|
||||
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
||||
|
||||
export const boardCardFieldsByKeyScopedSelector = selectorFamily({
|
||||
key: 'boardCardFieldsByKeyScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) =>
|
||||
get(boardCardFieldsScopedState(scopeId)).reduce<
|
||||
Record<string, BoardFieldDefinition<FieldMetadata>>
|
||||
>((result, field) => ({ ...result, [field.fieldMetadataId]: field }), {}),
|
||||
});
|
||||
@ -1,29 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
|
||||
import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFamilyState';
|
||||
|
||||
// TODO: this state should be computed during the synchronization web-hook and put in a generic
|
||||
// boardColumnTotalsFamilyState indexed by columnId.
|
||||
export const boardColumnTotalsFamilySelector = selectorFamily({
|
||||
key: 'boardColumnTotalsFamilySelector',
|
||||
get:
|
||||
(pipelineStepId: string) =>
|
||||
({ get }) => {
|
||||
const cardIds = get(boardCardIdsByColumnIdFamilyState(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 pipelineStepTotal;
|
||||
},
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
||||
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
|
||||
|
||||
export const canPersistBoardCardFieldsScopedFamilySelector = selectorFamily({
|
||||
key: 'canPersistBoardCardFieldsScopedFamilySelector',
|
||||
get:
|
||||
({
|
||||
recoilScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
recoilScopeId: string;
|
||||
viewId: string | undefined;
|
||||
}) =>
|
||||
({ get }) =>
|
||||
!isDeeplyEqual(
|
||||
get(savedBoardCardFieldsFamilyState(viewId)),
|
||||
get(boardCardFieldsScopedState(recoilScopeId)),
|
||||
),
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { boardColumnsState } from '../boardColumnsState';
|
||||
import { savedBoardColumnsState } from '../savedBoardColumnsState';
|
||||
|
||||
export const canPersistBoardColumnsSelector = selector<boolean>({
|
||||
key: 'canPersistBoardCardFieldsScopedFamilySelector',
|
||||
get: ({ get }) =>
|
||||
!isDeeplyEqual(get(boardColumnsState), get(savedBoardColumnsState)),
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { availableBoardCardFieldsScopedState } from '../availableBoardCardFieldsScopedState';
|
||||
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
||||
|
||||
export const hiddenBoardCardFieldsScopedSelector = selectorFamily({
|
||||
key: 'hiddenBoardCardFieldsScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) => {
|
||||
const fields = get(boardCardFieldsScopedState(scopeId));
|
||||
const fieldKeys = fields.map(({ fieldMetadataId }) => fieldMetadataId);
|
||||
const otherAvailableKeys = get(
|
||||
availableBoardCardFieldsScopedState({ scopeId }),
|
||||
).filter(({ fieldMetadataId }) => !fieldKeys.includes(fieldMetadataId));
|
||||
|
||||
return [
|
||||
...fields.filter((field) => !field.isVisible),
|
||||
...otherAvailableKeys,
|
||||
];
|
||||
},
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
|
||||
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
|
||||
|
||||
export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
|
||||
key: 'savedBoardCardFieldsByKeyFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedBoardCardFieldsFamilyState(viewId)).reduce<
|
||||
Record<string, BoardFieldDefinition<FieldMetadata>>
|
||||
>((result, field) => ({ ...result, [field.fieldMetadataId]: field }), {}),
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFamilyState';
|
||||
import { boardColumnsState } from '../boardColumnsState';
|
||||
import { isCardSelectedFamilyState } from '../isCardSelectedFamilyState';
|
||||
|
||||
export const selectedCardIdsSelector = selector<string[]>({
|
||||
key: 'selectedCardIdsSelector',
|
||||
get: ({ get }) => {
|
||||
const boardColumns = get(boardColumnsState);
|
||||
|
||||
const cardIds = boardColumns.flatMap((boardColumn) =>
|
||||
get(boardCardIdsByColumnIdFamilyState(boardColumn.id)),
|
||||
);
|
||||
|
||||
const selectedCardIds = cardIds.filter(
|
||||
(cardId) => get(isCardSelectedFamilyState(cardId)) === true,
|
||||
);
|
||||
|
||||
return selectedCardIds;
|
||||
},
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
||||
|
||||
export const visibleBoardCardFieldsScopedSelector = selectorFamily({
|
||||
key: 'visibleBoardCardFieldsScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) =>
|
||||
get(boardCardFieldsScopedState(scopeId))
|
||||
.filter((field) => field.isVisible)
|
||||
.sort((a, b) => a.position - b.position),
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
export type BoardColumnDefinition = {
|
||||
id: string;
|
||||
title: string;
|
||||
position: number;
|
||||
colorCode?: ThemeColor;
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export enum BoardColumnHotkeyScope {
|
||||
BoardColumn = 'board-column',
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
export type BoardFieldDefinition<T extends FieldMetadata> =
|
||||
FieldDefinition<T> & {
|
||||
position: number;
|
||||
isVisible?: boolean;
|
||||
viewFieldId?: string;
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||
import { FilterDefinitionByEntity } from '@/ui/object/object-filter-dropdown/types/FilterDefinitionByEntity';
|
||||
import { SortDefinition } from '@/ui/object/object-sort-dropdown/types/SortDefinition';
|
||||
|
||||
export type BoardOptions = {
|
||||
newCardComponent: React.ReactNode;
|
||||
CardComponent: ComponentType;
|
||||
filterDefinitions: FilterDefinitionByEntity<Opportunity>[];
|
||||
sortDefinitions: SortDefinition[];
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
export enum BoardOptionsHotkeyScope {
|
||||
Dropdown = 'board-options-dropdown',
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export enum ColumnHotkeyScope {
|
||||
EditColumnName = 'EditColumnNameHotkeyScope',
|
||||
}
|
||||
Reference in New Issue
Block a user