feat: reorder kanban columns (#1699)

* kaban header options

* gql codegn

* moveColumn hook refactor

* BoardColumnContext addition

* saved board columns state

* db call hook update

* lint fix

* state change first db call second

* handleMoveTableColumn call

* codegen lint fix

* useMoveViewColumns hook

* useBoardColumns db call addition

* boardColumns state change from BoardHeader

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Aditya Pimpalkar
2023-09-27 14:59:44 +01:00
committed by GitHub
parent 1e716bf6d6
commit 46ad36061e
18 changed files with 456 additions and 107 deletions

View File

@ -899,6 +899,177 @@ export type CompanyWhereUniqueInput = {
id?: InputMaybe<Scalars['String']>;
};
export enum Currency {
Aed = 'AED',
Afn = 'AFN',
All = 'ALL',
Amd = 'AMD',
Ang = 'ANG',
Aoa = 'AOA',
Ars = 'ARS',
Aud = 'AUD',
Awg = 'AWG',
Azn = 'AZN',
Bam = 'BAM',
Bbd = 'BBD',
Bdt = 'BDT',
Bgn = 'BGN',
Bhd = 'BHD',
Bif = 'BIF',
Bmd = 'BMD',
Bnd = 'BND',
Bob = 'BOB',
Bov = 'BOV',
Brl = 'BRL',
Bsd = 'BSD',
Btn = 'BTN',
Bwp = 'BWP',
Byn = 'BYN',
Bzd = 'BZD',
Cad = 'CAD',
Cdf = 'CDF',
Chf = 'CHF',
Clf = 'CLF',
Clp = 'CLP',
Cny = 'CNY',
Cop = 'COP',
Cou = 'COU',
Crc = 'CRC',
Cuc = 'CUC',
Cup = 'CUP',
Cve = 'CVE',
Czk = 'CZK',
Djf = 'DJF',
Dkk = 'DKK',
Dop = 'DOP',
Dzd = 'DZD',
Egp = 'EGP',
Ern = 'ERN',
Etb = 'ETB',
Eur = 'EUR',
Fjd = 'FJD',
Fkp = 'FKP',
Gbp = 'GBP',
Gel = 'GEL',
Ghs = 'GHS',
Gip = 'GIP',
Gmd = 'GMD',
Gnf = 'GNF',
Gtq = 'GTQ',
Gyd = 'GYD',
Hkd = 'HKD',
Hnl = 'HNL',
Hrk = 'HRK',
Htg = 'HTG',
Huf = 'HUF',
Idr = 'IDR',
Ils = 'ILS',
Inr = 'INR',
Iqd = 'IQD',
Irr = 'IRR',
Isk = 'ISK',
Jmd = 'JMD',
Jod = 'JOD',
Jpy = 'JPY',
Kes = 'KES',
Kgs = 'KGS',
Khr = 'KHR',
Kmf = 'KMF',
Kpw = 'KPW',
Krw = 'KRW',
Kwd = 'KWD',
Kyd = 'KYD',
Kzt = 'KZT',
Lak = 'LAK',
Lbp = 'LBP',
Lkr = 'LKR',
Lrd = 'LRD',
Lsl = 'LSL',
Lyd = 'LYD',
Mad = 'MAD',
Mdl = 'MDL',
Mga = 'MGA',
Mkd = 'MKD',
Mmk = 'MMK',
Mnt = 'MNT',
Mop = 'MOP',
Mro = 'MRO',
Mru = 'MRU',
Mur = 'MUR',
Mvr = 'MVR',
Mwk = 'MWK',
Mxn = 'MXN',
Mxv = 'MXV',
Myr = 'MYR',
Mzn = 'MZN',
Nad = 'NAD',
Ngn = 'NGN',
Nio = 'NIO',
Nok = 'NOK',
Npr = 'NPR',
Nzd = 'NZD',
Omr = 'OMR',
Pab = 'PAB',
Pen = 'PEN',
Pgk = 'PGK',
Php = 'PHP',
Pkr = 'PKR',
Pln = 'PLN',
Pyg = 'PYG',
Qar = 'QAR',
Ron = 'RON',
Rsd = 'RSD',
Rub = 'RUB',
Rwf = 'RWF',
Sar = 'SAR',
Sbd = 'SBD',
Scr = 'SCR',
Sdd = 'SDD',
Sdg = 'SDG',
Sek = 'SEK',
Sgd = 'SGD',
Shp = 'SHP',
Sll = 'SLL',
Sos = 'SOS',
Srd = 'SRD',
Ssp = 'SSP',
Std = 'STD',
Stn = 'STN',
Svc = 'SVC',
Syp = 'SYP',
Szl = 'SZL',
Thb = 'THB',
Tjs = 'TJS',
Tmm = 'TMM',
Tmt = 'TMT',
Tnd = 'TND',
Top = 'TOP',
Try = 'TRY',
Ttd = 'TTD',
Twd = 'TWD',
Tzs = 'TZS',
Uah = 'UAH',
Ugx = 'UGX',
Usd = 'USD',
Uyu = 'UYU',
Uzs = 'UZS',
Vef = 'VEF',
Ves = 'VES',
Vnd = 'VND',
Vuv = 'VUV',
Wst = 'WST',
Xaf = 'XAF',
Xcd = 'XCD',
Xof = 'XOF',
Xpf = 'XPF',
Xsu = 'XSU',
Xua = 'XUA',
Yer = 'YER',
Zar = 'ZAR',
Zmw = 'ZMW',
Zwl = 'ZWL'
}
export type DateTimeFilter = {
equals?: InputMaybe<Scalars['DateTime']>;
gt?: InputMaybe<Scalars['DateTime']>;
@ -942,6 +1113,13 @@ export type EnumColorSchemeFilter = {
notIn?: InputMaybe<Array<ColorScheme>>;
};
export type EnumCurrencyFilter = {
equals?: InputMaybe<Currency>;
in?: InputMaybe<Array<Currency>>;
not?: InputMaybe<NestedEnumCurrencyFilter>;
notIn?: InputMaybe<Array<Currency>>;
};
export type EnumPipelineProgressableTypeFilter = {
equals?: InputMaybe<PipelineProgressableType>;
in?: InputMaybe<Array<PipelineProgressableType>>;
@ -1492,6 +1670,13 @@ export type NestedEnumColorSchemeFilter = {
notIn?: InputMaybe<Array<ColorScheme>>;
};
export type NestedEnumCurrencyFilter = {
equals?: InputMaybe<Currency>;
in?: InputMaybe<Array<Currency>>;
not?: InputMaybe<NestedEnumCurrencyFilter>;
notIn?: InputMaybe<Array<Currency>>;
};
export type NestedEnumPipelineProgressableTypeFilter = {
equals?: InputMaybe<PipelineProgressableType>;
in?: InputMaybe<Array<PipelineProgressableType>>;
@ -1795,6 +1980,7 @@ export type PersonWhereUniqueInput = {
export type Pipeline = {
__typename?: 'Pipeline';
createdAt: Scalars['DateTime'];
currency: Currency;
icon: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
@ -1814,6 +2000,7 @@ export type PipelineCreateNestedOneWithoutPipelineStagesInput = {
export type PipelineOrderByWithRelationInput = {
createdAt?: InputMaybe<SortOrder>;
currency?: InputMaybe<SortOrder>;
icon?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
@ -2000,6 +2187,7 @@ export type PipelineRelationFilter = {
export enum PipelineScalarFieldEnum {
CreatedAt = 'createdAt',
Currency = 'currency',
DeletedAt = 'deletedAt',
Icon = 'icon',
Id = 'id',
@ -2141,6 +2329,7 @@ export type PipelineWhereInput = {
NOT?: InputMaybe<Array<PipelineWhereInput>>;
OR?: InputMaybe<Array<PipelineWhereInput>>;
createdAt?: InputMaybe<DateTimeFilter>;
currency?: InputMaybe<EnumCurrencyFilter>;
icon?: InputMaybe<StringFilter>;
id?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>;

View File

@ -1,7 +1,7 @@
import { useCallback, useContext, useState } from 'react';
import { NewButton } from '@/ui/board/components/NewButton';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
import { BoardColumnContext } from '@/ui/board/contexts/BoardColumnContext';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
@ -14,7 +14,9 @@ import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompany
export const NewCompanyProgressButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false);
const pipelineStageId = useContext(BoardColumnIdContext);
const column = useContext(BoardColumnContext);
const pipelineStageId = column?.columnDefinition.id || '';
const { enqueueSnackBar } = useSnackBar();

View File

@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { savedBoardColumnsState } from '@/ui/board/states/savedBoardColumnsState';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor';
@ -114,11 +115,10 @@ export const useUpdateCompanyBoard = () =>
index: pipelineStage.index ?? 0,
};
});
if (!isDeeplyEqual(currentBoardColumns, newBoardColumns)) {
if (currentBoardColumns.length === 0) {
set(boardColumnsState, newBoardColumns);
set(savedBoardColumnsState, newBoardColumns);
}
for (const boardColumn of newBoardColumns) {
const boardCardIds = pipelineProgresses
.filter(

View File

@ -1,10 +1,10 @@
import React from 'react';
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import { Tag } from '@/ui/tag/components/Tag';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnMenu } from './BoardColumnMenu';
@ -53,28 +53,24 @@ const StyledNumChildren = styled.div`
`;
export type BoardColumnProps = {
color?: ThemeColor;
title: string;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void;
totalAmount?: number;
children: React.ReactNode;
isFirstColumn: boolean;
numChildren: number;
stageId: string;
};
export const BoardColumn = ({
color,
title,
onDelete,
onTitleEdit,
totalAmount,
children,
isFirstColumn,
numChildren,
stageId,
}: BoardColumnProps) => {
const boardColumn = useContext(BoardColumnContext);
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] =
React.useState(false);
@ -95,10 +91,18 @@ export const BoardColumn = ({
setIsBoardColumnMenuOpen(false);
};
if (!boardColumn) return <></>;
const { isFirstColumn, columnDefinition } = boardColumn;
return (
<StyledColumn isFirstColumn={isFirstColumn}>
<StyledHeader onClick={handleTitleClick}>
<Tag color={color ?? 'gray'} text={title} />
<StyledHeader>
<Tag
onClick={handleTitleClick}
color={columnDefinition.colorCode ?? 'gray'}
text={columnDefinition.title}
/>
{!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>}
<StyledNumChildren>{numChildren}</StyledNumChildren>
</StyledHeader>
@ -107,8 +111,6 @@ export const BoardColumn = ({
onClose={handleClose}
onDelete={onDelete}
onTitleEdit={onTitleEdit}
title={title}
color={color ?? 'gray'}
stageId={stageId}
/>
)}

View File

@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useContext, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
@ -7,50 +7,53 @@ import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProg
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconPencil, IconPlus, IconTrash } from '@/ui/icon';
import {
IconArrowLeft,
IconArrowRight,
IconPencil,
IconPlus,
IconTrash,
} from '@/ui/icon';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
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 { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { ThemeColor } from '@/ui/theme/constants/colors';
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 { BoardColumnContext } from '../contexts/BoardColumnContext';
import { useBoardColumns } from '../hooks/useBoardColumns';
import { boardColumnsState } from '../states/boardColumnsState';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
const StyledMenuContainer = styled.div`
position: absolute;
width: 200px;
z-index: 1;
`;
type OwnProps = {
color: ThemeColor;
type BoardColumnMenuProps = {
onClose: () => void;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void;
stageId: string;
title: string;
};
type Menu = 'actions' | 'add' | 'title';
export const BoardColumnMenu = ({
color,
onClose,
onDelete,
onTitleEdit,
stageId,
title,
}: OwnProps) => {
}: BoardColumnMenuProps) => {
const [currentMenu, setCurrentMenu] = useState('actions');
const column = useContext(BoardColumnContext);
const [, setBoardColumns] = useRecoilState(boardColumnsState);
@ -58,6 +61,7 @@ export const BoardColumnMenu = ({
const { enqueueSnackBar } = useSnackBar();
const createCompanyProgress = useCreateCompanyProgress();
const { handleMoveBoardColumn } = useBoardColumns();
const handleCompanySelected = (
selectedCompany: EntityForSelect | null | undefined,
@ -125,6 +129,26 @@ export const BoardColumnMenu = ({
[],
);
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}>
<StyledDropdownMenu data-select-disable>
@ -135,6 +159,16 @@ export const BoardColumnMenu = ({
LeftIcon={IconPencil}
text="Rename"
/>
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
<MenuItem
onClick={handleDelete}
LeftIcon={IconTrash}
@ -149,10 +183,10 @@ export const BoardColumnMenu = ({
)}
{currentMenu === 'title' && (
<BoardColumnEditTitleMenu
color={color}
color={columnDefinition.colorCode ?? 'gray'}
onClose={closeMenu}
onTitleEdit={onTitleEdit}
title={title}
title={columnDefinition.title}
/>
)}
{currentMenu === 'add' && (

View File

@ -12,9 +12,12 @@ import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardColumnsState } from '../states/boardColumnsState';
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
import { savedBoardColumnsState } from '../states/savedBoardColumnsState';
import { canPersistBoardCardFieldsScopedFamilySelector } from '../states/selectors/canPersistBoardCardFieldsScopedFamilySelector';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { canPersistBoardColumnsSelector } from '../states/selectors/canPersistBoardColumnsSelector';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
@ -47,6 +50,8 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
viewId: currentViewId,
}),
);
const canPersistBoardColumns = useRecoilValue(canPersistBoardColumnsSelector);
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
BoardRecoilScopeContext,
@ -55,7 +60,15 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
savedBoardCardFieldsFamilyState(currentViewId),
);
const handleViewBarReset = () => setBoardCardFields(savedBoardCardFields);
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const [, setSavedBoardColumns] = useRecoilState(savedBoardColumnsState);
const savedBoardColumns = useRecoilValue(savedBoardColumnsState);
const handleViewBarReset = () => {
setBoardCardFields(savedBoardCardFields);
setBoardColumns(savedBoardColumns);
};
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
@ -75,16 +88,21 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
if (canPersistBoardCardFields) {
setSavedBoardCardFields(boardCardFields);
}
if (canPersistBoardColumns) {
setSavedBoardColumns(boardColumns);
}
await onCurrentViewSubmit?.();
};
const canPersistView = canPersistBoardCardFields || canPersistBoardColumns;
return (
<RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}>
<ViewBarContext.Provider
value={{
...viewBarContextProps,
canPersistViewFields: canPersistBoardCardFields,
canPersistViewFields: canPersistView,
onCurrentViewSubmit: handleCurrentViewSubmit,
onViewBarReset: handleViewBarReset,
onViewSelect: handleViewSelect,

View File

@ -2,13 +2,13 @@ import { useCallback, useRef } from 'react';
import { getOperationName } from '@apollo/client/utilities';
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 { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { BoardHeader } from '@/ui/board/components/BoardHeader';
import { StyledBoard } from '@/ui/board/components/StyledBoard';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
import { BoardColumnContext } from '@/ui/board/contexts/BoardColumnContext';
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';
@ -54,7 +54,7 @@ export const EntityBoard = ({
onColumnDelete,
onEditColumnTitle,
}: EntityBoardProps) => {
const [boardColumns] = useRecoilState(boardColumnsState);
const boardColumns = useRecoilValue(boardColumnsState);
const setCardSelected = useSetCardSelected();
const [updatePipelineProgressStage] =
@ -151,19 +151,26 @@ export const EntityBoard = ({
<StyledBoard ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}>
{sortedBoardColumns.map((column) => (
<BoardColumnIdContext.Provider value={column.id} key={column.id}>
<BoardColumnContext.Provider
key={column.id}
value={{
id: column.id,
columnDefinition: column,
isFirstColumn: column.index === 0,
isLastColumn: column.index === sortedBoardColumns.length - 1,
}}
>
<RecoilScope
CustomRecoilScopeContext={BoardColumnRecoilScopeContext}
key={column.id}
>
<EntityBoardColumn
boardOptions={boardOptions}
column={column}
onDelete={onColumnDelete}
onTitleEdit={onEditColumnTitle}
/>
</RecoilScope>
</BoardColumnIdContext.Provider>
</BoardColumnContext.Provider>
))}
</DragDropContext>
</StyledBoard>

View File

@ -5,8 +5,7 @@ import { useRecoilValue } from 'recoil';
import { BoardColumn } from '@/ui/board/components/BoardColumn';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { BoardColumnContext } from '@/ui/board/contexts/BoardColumnContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
@ -29,13 +28,21 @@ const StyledColumnCardsContainer = styled.div`
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,
}: {
children: React.ReactNode;
droppableProvided: DroppableProvided;
}) => {
}: BoardColumnCardsContainerProps) => {
return (
<StyledColumnCardsContainer
ref={droppableProvided?.innerRef}
@ -49,39 +56,34 @@ const BoardColumnCardsContainer = ({
export const EntityBoardColumn = ({
boardOptions,
column,
onDelete,
onTitleEdit,
}: {
boardOptions: BoardOptions;
column: BoardColumnDefinition;
onDelete?: (columnId: string) => void;
onTitleEdit: (columnId: string, title: string, color: string) => void;
}) => {
const boardColumnId = useContext(BoardColumnIdContext) ?? '';
}: EntityBoardColumnProps) => {
const column = useContext(BoardColumnContext);
const boardColumnId = column?.id || '';
const boardColumnTotal = useRecoilValue(
boardColumnTotalsFamilySelector(column.id),
boardColumnTotalsFamilySelector(boardColumnId),
);
const cardIds = useRecoilValue(
boardCardIdsByColumnIdFamilyState(boardColumnId ?? ''),
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}
title={column.title}
color={column.colorCode}
totalAmount={boardColumnTotal}
isFirstColumn={column.index === 0}
numChildren={cardIds.length}
stageId={column.id}
>

View File

@ -0,0 +1,12 @@
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);

View File

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

View File

@ -0,0 +1,53 @@
import { useRecoilState } from 'recoil';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { useUpdatePipelineStageMutation } from '~/generated/graphql';
import { boardColumnsState } from '../states/boardColumnsState';
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
export const useBoardColumns = () => {
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const { handleColumnMove } = useMoveViewColumns();
const [updatePipelineStageMutation] = useUpdatePipelineStageMutation();
const updatedPipelineStages = (stages: BoardColumnDefinition[]) => {
if (!stages.length) return;
return Promise.all(
stages.map((stage) =>
updatePipelineStageMutation({
variables: {
data: {
index: stage.index,
},
id: stage.id,
},
}),
),
);
};
const persistBoardColumns = async () => {
await updatedPipelineStages(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

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

View File

@ -0,0 +1,12 @@
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)),
});

View File

@ -22,11 +22,8 @@ export const TableColumnDropdownMenu = ({
isLastColumn,
primaryColumnKey,
}: EntityTableHeaderOptionsProps) => {
const {
handleColumnVisibilityChange,
handleColumnLeftMove,
handleColumnRightMove,
} = useTableColumns();
const { handleColumnVisibilityChange, handleMoveTableColumn } =
useTableColumns();
const { closeDropdownButton } = useDropdownButton({
dropdownId: ColumnHeadDropdownId,
@ -37,7 +34,7 @@ export const TableColumnDropdownMenu = ({
if (isFirstColumn) {
return;
}
handleColumnLeftMove(column);
handleMoveTableColumn('left', column);
};
const handleColumnMoveRight = () => {
@ -45,7 +42,7 @@ export const TableColumnDropdownMenu = ({
if (isLastColumn) {
return;
}
handleColumnRightMove(column);
handleMoveTableColumn('right', column);
};
const handleColumnVisibility = () => {

View File

@ -5,6 +5,7 @@ import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { TableContext } from '../contexts/TableContext';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
@ -32,6 +33,8 @@ export const useTableColumns = () => {
TableRecoilScopeContext,
);
const { handleColumnMove } = useMoveViewColumns();
const handleColumnsChange = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
setSavedTableColumns(columns);
@ -71,54 +74,28 @@ export const useTableColumns = () => {
[tableColumnsByKey, tableColumns, handleColumnsChange],
);
const handleColumnMove = useCallback(
async (direction: string, column: ColumnDefinition<ViewFieldMetadata>) => {
const handleMoveTableColumn = useCallback(
(
direction: 'left' | 'right',
column: ColumnDefinition<ViewFieldMetadata>,
) => {
const currentColumnArrayIndex = tableColumns.findIndex(
(tableColumn) => tableColumn.key === column.key,
);
const targetColumnArrayIndex =
direction === 'left'
? currentColumnArrayIndex - 1
: currentColumnArrayIndex + 1;
const columns = handleColumnMove(
direction,
currentColumnArrayIndex,
tableColumns,
);
if (currentColumnArrayIndex >= 0) {
const currentColumn = tableColumns[currentColumnArrayIndex];
const targetColumn = tableColumns[targetColumnArrayIndex];
const newTableColumns = [...tableColumns];
newTableColumns[currentColumnArrayIndex] = {
...targetColumn,
index: currentColumn.index,
};
newTableColumns[targetColumnArrayIndex] = {
...currentColumn,
index: targetColumn.index,
};
await handleColumnsChange(newTableColumns);
}
setTableColumns(columns);
},
[tableColumns, handleColumnsChange],
);
const handleColumnLeftMove = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
handleColumnMove('left', column);
},
[handleColumnMove],
);
const handleColumnRightMove = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
handleColumnMove('right', column);
},
[handleColumnMove],
[tableColumns, setTableColumns, handleColumnMove],
);
return {
handleColumnVisibilityChange,
handleColumnLeftMove,
handleColumnRightMove,
handleMoveTableColumn,
handleColumnReorder,
handleColumnsChange,
};

View File

@ -5,7 +5,7 @@ import { availableBoardCardFieldsScopedState } from '@/ui/board/states/available
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import { savedBoardCardFieldsFamilyState } from '@/ui/board/states/savedBoardCardFieldsFamilyState';
import { savedBoardCardFieldsByKeyFamilySelector } from '@/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector';
import {
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';

View File

@ -1,4 +1,5 @@
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { useBoardColumns } from '@/ui/board/hooks/useBoardColumns';
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import {
ViewFieldDefinition,
@ -50,6 +51,8 @@ export const useBoardViews = ({
RecoilScopeContext,
});
const { persistBoardColumns } = useBoardColumns();
const { createViewFilters, persistFilters } = useViewFilters({
skipFetch: isFetchingViews,
RecoilScopeContext,
@ -62,6 +65,7 @@ export const useBoardViews = ({
const submitCurrentView = async () => {
await persistCardFields();
await persistBoardColumns();
await persistFilters();
await persistSorts();
};

View File

@ -0,0 +1,35 @@
export const useMoveViewColumns = () => {
const handleColumnMove = <T extends { index: number }>(
direction: 'left' | 'right',
currentArrayindex: number,
targetArray: T[],
) => {
const targetArrayIndex =
direction === 'left' ? currentArrayindex - 1 : currentArrayindex + 1;
const targetArraySize = targetArray.length - 1;
if (
currentArrayindex >= 0 &&
targetArrayIndex >= 0 &&
currentArrayindex <= targetArraySize &&
targetArrayIndex <= targetArraySize
) {
const currentEntity = targetArray[currentArrayindex];
const targetEntity = targetArray[targetArrayIndex];
const newArray = [...targetArray];
newArray[currentArrayindex] = {
...targetEntity,
index: currentEntity.index,
};
newArray[targetArrayIndex] = {
...currentEntity,
index: targetEntity.index,
};
return newArray;
}
return targetArray;
};
return { handleColumnMove };
};