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:
@ -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>;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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' && (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
12
front/src/modules/ui/board/contexts/BoardColumnContext.ts
Normal file
12
front/src/modules/ui/board/contexts/BoardColumnContext.ts
Normal 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);
|
||||
@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardColumnIdContext = createContext<string | null>(null);
|
||||
53
front/src/modules/ui/board/hooks/useBoardColumns.ts
Normal file
53
front/src/modules/ui/board/hooks/useBoardColumns.ts
Normal 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 };
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||
|
||||
export const savedBoardColumnsState = atom<BoardColumnDefinition[]>({
|
||||
key: 'savedBoardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
@ -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)),
|
||||
});
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
35
front/src/modules/views/hooks/useMoveViewColumns.ts
Normal file
35
front/src/modules/views/hooks/useMoveViewColumns.ts
Normal 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user