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']>; 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 = { export type DateTimeFilter = {
equals?: InputMaybe<Scalars['DateTime']>; equals?: InputMaybe<Scalars['DateTime']>;
gt?: InputMaybe<Scalars['DateTime']>; gt?: InputMaybe<Scalars['DateTime']>;
@ -942,6 +1113,13 @@ export type EnumColorSchemeFilter = {
notIn?: InputMaybe<Array<ColorScheme>>; notIn?: InputMaybe<Array<ColorScheme>>;
}; };
export type EnumCurrencyFilter = {
equals?: InputMaybe<Currency>;
in?: InputMaybe<Array<Currency>>;
not?: InputMaybe<NestedEnumCurrencyFilter>;
notIn?: InputMaybe<Array<Currency>>;
};
export type EnumPipelineProgressableTypeFilter = { export type EnumPipelineProgressableTypeFilter = {
equals?: InputMaybe<PipelineProgressableType>; equals?: InputMaybe<PipelineProgressableType>;
in?: InputMaybe<Array<PipelineProgressableType>>; in?: InputMaybe<Array<PipelineProgressableType>>;
@ -1492,6 +1670,13 @@ export type NestedEnumColorSchemeFilter = {
notIn?: InputMaybe<Array<ColorScheme>>; notIn?: InputMaybe<Array<ColorScheme>>;
}; };
export type NestedEnumCurrencyFilter = {
equals?: InputMaybe<Currency>;
in?: InputMaybe<Array<Currency>>;
not?: InputMaybe<NestedEnumCurrencyFilter>;
notIn?: InputMaybe<Array<Currency>>;
};
export type NestedEnumPipelineProgressableTypeFilter = { export type NestedEnumPipelineProgressableTypeFilter = {
equals?: InputMaybe<PipelineProgressableType>; equals?: InputMaybe<PipelineProgressableType>;
in?: InputMaybe<Array<PipelineProgressableType>>; in?: InputMaybe<Array<PipelineProgressableType>>;
@ -1795,6 +1980,7 @@ export type PersonWhereUniqueInput = {
export type Pipeline = { export type Pipeline = {
__typename?: 'Pipeline'; __typename?: 'Pipeline';
createdAt: Scalars['DateTime']; createdAt: Scalars['DateTime'];
currency: Currency;
icon: Scalars['String']; icon: Scalars['String'];
id: Scalars['ID']; id: Scalars['ID'];
name: Scalars['String']; name: Scalars['String'];
@ -1814,6 +2000,7 @@ export type PipelineCreateNestedOneWithoutPipelineStagesInput = {
export type PipelineOrderByWithRelationInput = { export type PipelineOrderByWithRelationInput = {
createdAt?: InputMaybe<SortOrder>; createdAt?: InputMaybe<SortOrder>;
currency?: InputMaybe<SortOrder>;
icon?: InputMaybe<SortOrder>; icon?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>; id?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>; name?: InputMaybe<SortOrder>;
@ -2000,6 +2187,7 @@ export type PipelineRelationFilter = {
export enum PipelineScalarFieldEnum { export enum PipelineScalarFieldEnum {
CreatedAt = 'createdAt', CreatedAt = 'createdAt',
Currency = 'currency',
DeletedAt = 'deletedAt', DeletedAt = 'deletedAt',
Icon = 'icon', Icon = 'icon',
Id = 'id', Id = 'id',
@ -2141,6 +2329,7 @@ export type PipelineWhereInput = {
NOT?: InputMaybe<Array<PipelineWhereInput>>; NOT?: InputMaybe<Array<PipelineWhereInput>>;
OR?: InputMaybe<Array<PipelineWhereInput>>; OR?: InputMaybe<Array<PipelineWhereInput>>;
createdAt?: InputMaybe<DateTimeFilter>; createdAt?: InputMaybe<DateTimeFilter>;
currency?: InputMaybe<EnumCurrencyFilter>;
icon?: InputMaybe<StringFilter>; icon?: InputMaybe<StringFilter>;
id?: InputMaybe<StringFilter>; id?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>; name?: InputMaybe<StringFilter>;

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import React from 'react'; import React, { useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Tag } from '@/ui/tag/components/Tag'; import { Tag } from '@/ui/tag/components/Tag';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope'; import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnMenu } from './BoardColumnMenu'; import { BoardColumnMenu } from './BoardColumnMenu';
@ -53,28 +53,24 @@ const StyledNumChildren = styled.div`
`; `;
export type BoardColumnProps = { export type BoardColumnProps = {
color?: ThemeColor;
title: string;
onDelete?: (id: string) => void; onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void; onTitleEdit: (title: string, color: string) => void;
totalAmount?: number; totalAmount?: number;
children: React.ReactNode; children: React.ReactNode;
isFirstColumn: boolean;
numChildren: number; numChildren: number;
stageId: string; stageId: string;
}; };
export const BoardColumn = ({ export const BoardColumn = ({
color,
title,
onDelete, onDelete,
onTitleEdit, onTitleEdit,
totalAmount, totalAmount,
children, children,
isFirstColumn,
numChildren, numChildren,
stageId, stageId,
}: BoardColumnProps) => { }: BoardColumnProps) => {
const boardColumn = useContext(BoardColumnContext);
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] =
React.useState(false); React.useState(false);
@ -95,10 +91,18 @@ export const BoardColumn = ({
setIsBoardColumnMenuOpen(false); setIsBoardColumnMenuOpen(false);
}; };
if (!boardColumn) return <></>;
const { isFirstColumn, columnDefinition } = boardColumn;
return ( return (
<StyledColumn isFirstColumn={isFirstColumn}> <StyledColumn isFirstColumn={isFirstColumn}>
<StyledHeader onClick={handleTitleClick}> <StyledHeader>
<Tag color={color ?? 'gray'} text={title} /> <Tag
onClick={handleTitleClick}
color={columnDefinition.colorCode ?? 'gray'}
text={columnDefinition.title}
/>
{!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>} {!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>}
<StyledNumChildren>{numChildren}</StyledNumChildren> <StyledNumChildren>{numChildren}</StyledNumChildren>
</StyledHeader> </StyledHeader>
@ -107,8 +111,6 @@ export const BoardColumn = ({
onClose={handleClose} onClose={handleClose}
onDelete={onDelete} onDelete={onDelete}
onTitleEdit={onTitleEdit} onTitleEdit={onTitleEdit}
title={title}
color={color ?? 'gray'}
stageId={stageId} 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 styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
@ -7,50 +7,53 @@ import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProg
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery'; import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; 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 { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; 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 { boardColumnsState } from '../states/boardColumnsState';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope'; import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu'; import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
const StyledMenuContainer = styled.div` const StyledMenuContainer = styled.div`
position: absolute; position: absolute;
width: 200px; width: 200px;
z-index: 1; z-index: 1;
`; `;
type OwnProps = { type BoardColumnMenuProps = {
color: ThemeColor;
onClose: () => void; onClose: () => void;
onDelete?: (id: string) => void; onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void; onTitleEdit: (title: string, color: string) => void;
stageId: string; stageId: string;
title: string;
}; };
type Menu = 'actions' | 'add' | 'title'; type Menu = 'actions' | 'add' | 'title';
export const BoardColumnMenu = ({ export const BoardColumnMenu = ({
color,
onClose, onClose,
onDelete, onDelete,
onTitleEdit, onTitleEdit,
stageId, stageId,
title, }: BoardColumnMenuProps) => {
}: OwnProps) => {
const [currentMenu, setCurrentMenu] = useState('actions'); const [currentMenu, setCurrentMenu] = useState('actions');
const column = useContext(BoardColumnContext);
const [, setBoardColumns] = useRecoilState(boardColumnsState); const [, setBoardColumns] = useRecoilState(boardColumnsState);
@ -58,6 +61,7 @@ export const BoardColumnMenu = ({
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const createCompanyProgress = useCreateCompanyProgress(); const createCompanyProgress = useCreateCompanyProgress();
const { handleMoveBoardColumn } = useBoardColumns();
const handleCompanySelected = ( const handleCompanySelected = (
selectedCompany: EntityForSelect | null | undefined, 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 ( return (
<StyledMenuContainer ref={boardColumnMenuRef}> <StyledMenuContainer ref={boardColumnMenuRef}>
<StyledDropdownMenu data-select-disable> <StyledDropdownMenu data-select-disable>
@ -135,6 +159,16 @@ export const BoardColumnMenu = ({
LeftIcon={IconPencil} LeftIcon={IconPencil}
text="Rename" text="Rename"
/> />
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
<MenuItem <MenuItem
onClick={handleDelete} onClick={handleDelete}
LeftIcon={IconTrash} LeftIcon={IconTrash}
@ -149,10 +183,10 @@ export const BoardColumnMenu = ({
)} )}
{currentMenu === 'title' && ( {currentMenu === 'title' && (
<BoardColumnEditTitleMenu <BoardColumnEditTitleMenu
color={color} color={columnDefinition.colorCode ?? 'gray'}
onClose={closeMenu} onClose={closeMenu}
onTitleEdit={onTitleEdit} onTitleEdit={onTitleEdit}
title={title} title={columnDefinition.title}
/> />
)} )}
{currentMenu === 'add' && ( {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 { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState'; import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardColumnsState } from '../states/boardColumnsState';
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState'; import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
import { savedBoardColumnsState } from '../states/savedBoardColumnsState';
import { canPersistBoardCardFieldsScopedFamilySelector } from '../states/selectors/canPersistBoardCardFieldsScopedFamilySelector'; 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 { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope'; import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
@ -47,6 +50,8 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
viewId: currentViewId, viewId: currentViewId,
}), }),
); );
const canPersistBoardColumns = useRecoilValue(canPersistBoardColumnsSelector);
const [boardCardFields, setBoardCardFields] = useRecoilScopedState( const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState, boardCardFieldsScopedState,
BoardRecoilScopeContext, BoardRecoilScopeContext,
@ -55,7 +60,15 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
savedBoardCardFieldsFamilyState(currentViewId), 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( const handleViewSelect = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
@ -75,16 +88,21 @@ export const BoardHeader = ({ className, onStageAdd }: BoardHeaderProps) => {
if (canPersistBoardCardFields) { if (canPersistBoardCardFields) {
setSavedBoardCardFields(boardCardFields); setSavedBoardCardFields(boardCardFields);
} }
if (canPersistBoardColumns) {
setSavedBoardColumns(boardColumns);
}
await onCurrentViewSubmit?.(); await onCurrentViewSubmit?.();
}; };
const canPersistView = canPersistBoardCardFields || canPersistBoardColumns;
return ( return (
<RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}> <RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}>
<ViewBarContext.Provider <ViewBarContext.Provider
value={{ value={{
...viewBarContextProps, ...viewBarContextProps,
canPersistViewFields: canPersistBoardCardFields, canPersistViewFields: canPersistView,
onCurrentViewSubmit: handleCurrentViewSubmit, onCurrentViewSubmit: handleCurrentViewSubmit,
onViewBarReset: handleViewBarReset, onViewBarReset: handleViewBarReset,
onViewSelect: handleViewSelect, onViewSelect: handleViewSelect,

View File

@ -2,13 +2,13 @@ import { useCallback, useRef } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; 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 { 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 { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { BoardHeader } from '@/ui/board/components/BoardHeader'; import { BoardHeader } from '@/ui/board/components/BoardHeader';
import { StyledBoard } from '@/ui/board/components/StyledBoard'; 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 { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -54,7 +54,7 @@ export const EntityBoard = ({
onColumnDelete, onColumnDelete,
onEditColumnTitle, onEditColumnTitle,
}: EntityBoardProps) => { }: EntityBoardProps) => {
const [boardColumns] = useRecoilState(boardColumnsState); const boardColumns = useRecoilValue(boardColumnsState);
const setCardSelected = useSetCardSelected(); const setCardSelected = useSetCardSelected();
const [updatePipelineProgressStage] = const [updatePipelineProgressStage] =
@ -151,19 +151,26 @@ export const EntityBoard = ({
<StyledBoard ref={boardRef}> <StyledBoard ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
{sortedBoardColumns.map((column) => ( {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 <RecoilScope
CustomRecoilScopeContext={BoardColumnRecoilScopeContext} CustomRecoilScopeContext={BoardColumnRecoilScopeContext}
key={column.id} key={column.id}
> >
<EntityBoardColumn <EntityBoardColumn
boardOptions={boardOptions} boardOptions={boardOptions}
column={column}
onDelete={onColumnDelete} onDelete={onColumnDelete}
onTitleEdit={onEditColumnTitle} onTitleEdit={onEditColumnTitle}
/> />
</RecoilScope> </RecoilScope>
</BoardColumnIdContext.Provider> </BoardColumnContext.Provider>
))} ))}
</DragDropContext> </DragDropContext>
</StyledBoard> </StyledBoard>

View File

@ -5,8 +5,7 @@ import { useRecoilValue } from 'recoil';
import { BoardColumn } from '@/ui/board/components/BoardColumn'; import { BoardColumn } from '@/ui/board/components/BoardColumn';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext'; import { BoardColumnContext } from '@/ui/board/contexts/BoardColumnContext';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState'; import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState';
@ -29,13 +28,21 @@ const StyledColumnCardsContainer = styled.div`
flex-direction: column; 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 = ({ const BoardColumnCardsContainer = ({
children, children,
droppableProvided, droppableProvided,
}: { }: BoardColumnCardsContainerProps) => {
children: React.ReactNode;
droppableProvided: DroppableProvided;
}) => {
return ( return (
<StyledColumnCardsContainer <StyledColumnCardsContainer
ref={droppableProvided?.innerRef} ref={droppableProvided?.innerRef}
@ -49,39 +56,34 @@ const BoardColumnCardsContainer = ({
export const EntityBoardColumn = ({ export const EntityBoardColumn = ({
boardOptions, boardOptions,
column,
onDelete, onDelete,
onTitleEdit, onTitleEdit,
}: { }: EntityBoardColumnProps) => {
boardOptions: BoardOptions; const column = useContext(BoardColumnContext);
column: BoardColumnDefinition;
onDelete?: (columnId: string) => void; const boardColumnId = column?.id || '';
onTitleEdit: (columnId: string, title: string, color: string) => void;
}) => {
const boardColumnId = useContext(BoardColumnIdContext) ?? '';
const boardColumnTotal = useRecoilValue( const boardColumnTotal = useRecoilValue(
boardColumnTotalsFamilySelector(column.id), boardColumnTotalsFamilySelector(boardColumnId),
); );
const cardIds = useRecoilValue( const cardIds = useRecoilValue(
boardCardIdsByColumnIdFamilyState(boardColumnId ?? ''), boardCardIdsByColumnIdFamilyState(boardColumnId),
); );
const handleTitleEdit = (title: string, color: string) => { const handleTitleEdit = (title: string, color: string) => {
onTitleEdit(boardColumnId, title, color); onTitleEdit(boardColumnId, title, color);
}; };
if (!column) return <></>;
return ( return (
<Droppable droppableId={column.id}> <Droppable droppableId={column.id}>
{(droppableProvided) => ( {(droppableProvided) => (
<BoardColumn <BoardColumn
onTitleEdit={handleTitleEdit} onTitleEdit={handleTitleEdit}
onDelete={onDelete} onDelete={onDelete}
title={column.title}
color={column.colorCode}
totalAmount={boardColumnTotal} totalAmount={boardColumnTotal}
isFirstColumn={column.index === 0}
numChildren={cardIds.length} numChildren={cardIds.length}
stageId={column.id} 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, isLastColumn,
primaryColumnKey, primaryColumnKey,
}: EntityTableHeaderOptionsProps) => { }: EntityTableHeaderOptionsProps) => {
const { const { handleColumnVisibilityChange, handleMoveTableColumn } =
handleColumnVisibilityChange, useTableColumns();
handleColumnLeftMove,
handleColumnRightMove,
} = useTableColumns();
const { closeDropdownButton } = useDropdownButton({ const { closeDropdownButton } = useDropdownButton({
dropdownId: ColumnHeadDropdownId, dropdownId: ColumnHeadDropdownId,
@ -37,7 +34,7 @@ export const TableColumnDropdownMenu = ({
if (isFirstColumn) { if (isFirstColumn) {
return; return;
} }
handleColumnLeftMove(column); handleMoveTableColumn('left', column);
}; };
const handleColumnMoveRight = () => { const handleColumnMoveRight = () => {
@ -45,7 +42,7 @@ export const TableColumnDropdownMenu = ({
if (isLastColumn) { if (isLastColumn) {
return; return;
} }
handleColumnRightMove(column); handleMoveTableColumn('right', column);
}; };
const handleColumnVisibility = () => { 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 { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { TableContext } from '../contexts/TableContext'; import { TableContext } from '../contexts/TableContext';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
@ -32,6 +33,8 @@ export const useTableColumns = () => {
TableRecoilScopeContext, TableRecoilScopeContext,
); );
const { handleColumnMove } = useMoveViewColumns();
const handleColumnsChange = useCallback( const handleColumnsChange = useCallback(
async (columns: ColumnDefinition<ViewFieldMetadata>[]) => { async (columns: ColumnDefinition<ViewFieldMetadata>[]) => {
setSavedTableColumns(columns); setSavedTableColumns(columns);
@ -71,54 +74,28 @@ export const useTableColumns = () => {
[tableColumnsByKey, tableColumns, handleColumnsChange], [tableColumnsByKey, tableColumns, handleColumnsChange],
); );
const handleColumnMove = useCallback( const handleMoveTableColumn = useCallback(
async (direction: string, column: ColumnDefinition<ViewFieldMetadata>) => { (
direction: 'left' | 'right',
column: ColumnDefinition<ViewFieldMetadata>,
) => {
const currentColumnArrayIndex = tableColumns.findIndex( const currentColumnArrayIndex = tableColumns.findIndex(
(tableColumn) => tableColumn.key === column.key, (tableColumn) => tableColumn.key === column.key,
); );
const targetColumnArrayIndex = const columns = handleColumnMove(
direction === 'left' direction,
? currentColumnArrayIndex - 1 currentColumnArrayIndex,
: currentColumnArrayIndex + 1; tableColumns,
);
if (currentColumnArrayIndex >= 0) { setTableColumns(columns);
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);
}
}, },
[tableColumns, handleColumnsChange], [tableColumns, setTableColumns, handleColumnMove],
);
const handleColumnLeftMove = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
handleColumnMove('left', column);
},
[handleColumnMove],
);
const handleColumnRightMove = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
handleColumnMove('right', column);
},
[handleColumnMove],
); );
return { return {
handleColumnVisibilityChange, handleColumnVisibilityChange,
handleColumnLeftMove, handleMoveTableColumn,
handleColumnRightMove,
handleColumnReorder, handleColumnReorder,
handleColumnsChange, handleColumnsChange,
}; };

View File

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

View File

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