From 46ad36061e6811b1b12f56210a5d17ba4c922bd6 Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Wed, 27 Sep 2023 14:59:44 +0100 Subject: [PATCH] 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 --- front/src/generated/graphql.tsx | 189 ++++++++++++++++++ .../components/NewCompanyProgressButton.tsx | 6 +- .../hooks/useUpdateCompanyBoardColumns.ts | 6 +- .../ui/board/components/BoardColumn.tsx | 26 +-- .../ui/board/components/BoardColumnMenu.tsx | 58 ++++-- .../ui/board/components/BoardHeader.tsx | 24 ++- .../ui/board/components/EntityBoard.tsx | 19 +- .../ui/board/components/EntityBoardColumn.tsx | 40 ++-- .../ui/board/contexts/BoardColumnContext.ts | 12 ++ .../ui/board/contexts/BoardColumnIdContext.ts | 3 - .../modules/ui/board/hooks/useBoardColumns.ts | 53 +++++ .../ui/board/states/savedBoardColumnsState.ts | 8 + .../canPersistBoardColumnsSelector.ts | 12 ++ .../components/TableColumnDropdownMenu.tsx | 11 +- .../modules/ui/table/hooks/useTableColumns.ts | 55 ++--- .../modules/views/hooks/useBoardViewFields.ts | 2 +- .../src/modules/views/hooks/useBoardViews.ts | 4 + .../modules/views/hooks/useMoveViewColumns.ts | 35 ++++ 18 files changed, 456 insertions(+), 107 deletions(-) create mode 100644 front/src/modules/ui/board/contexts/BoardColumnContext.ts delete mode 100644 front/src/modules/ui/board/contexts/BoardColumnIdContext.ts create mode 100644 front/src/modules/ui/board/hooks/useBoardColumns.ts create mode 100644 front/src/modules/ui/board/states/savedBoardColumnsState.ts create mode 100644 front/src/modules/ui/board/states/selectors/canPersistBoardColumnsSelector.ts create mode 100644 front/src/modules/views/hooks/useMoveViewColumns.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index c5dae4f7d..2719cf673 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -899,6 +899,177 @@ export type CompanyWhereUniqueInput = { id?: InputMaybe; }; +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; gt?: InputMaybe; @@ -942,6 +1113,13 @@ export type EnumColorSchemeFilter = { notIn?: InputMaybe>; }; +export type EnumCurrencyFilter = { + equals?: InputMaybe; + in?: InputMaybe>; + not?: InputMaybe; + notIn?: InputMaybe>; +}; + export type EnumPipelineProgressableTypeFilter = { equals?: InputMaybe; in?: InputMaybe>; @@ -1492,6 +1670,13 @@ export type NestedEnumColorSchemeFilter = { notIn?: InputMaybe>; }; +export type NestedEnumCurrencyFilter = { + equals?: InputMaybe; + in?: InputMaybe>; + not?: InputMaybe; + notIn?: InputMaybe>; +}; + export type NestedEnumPipelineProgressableTypeFilter = { equals?: InputMaybe; in?: InputMaybe>; @@ -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; + currency?: InputMaybe; icon?: InputMaybe; id?: InputMaybe; name?: InputMaybe; @@ -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>; OR?: InputMaybe>; createdAt?: InputMaybe; + currency?: InputMaybe; icon?: InputMaybe; id?: InputMaybe; name?: InputMaybe; diff --git a/front/src/modules/companies/components/NewCompanyProgressButton.tsx b/front/src/modules/companies/components/NewCompanyProgressButton.tsx index 3e5896657..c47fd1352 100644 --- a/front/src/modules/companies/components/NewCompanyProgressButton.tsx +++ b/front/src/modules/companies/components/NewCompanyProgressButton.tsx @@ -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(); diff --git a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts index 67f282418..38d8fb96d 100644 --- a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts +++ b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts @@ -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( diff --git a/front/src/modules/ui/board/components/BoardColumn.tsx b/front/src/modules/ui/board/components/BoardColumn.tsx index 915cb3b47..b1a5dcc36 100644 --- a/front/src/modules/ui/board/components/BoardColumn.tsx +++ b/front/src/modules/ui/board/components/BoardColumn.tsx @@ -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 ( - - + + {!!totalAmount && ${totalAmount}} {numChildren} @@ -107,8 +111,6 @@ export const BoardColumn = ({ onClose={handleClose} onDelete={onDelete} onTitleEdit={onTitleEdit} - title={title} - color={color ?? 'gray'} stageId={stageId} /> )} diff --git a/front/src/modules/ui/board/components/BoardColumnMenu.tsx b/front/src/modules/ui/board/components/BoardColumnMenu.tsx index 9bacf5ea6..949773961 100644 --- a/front/src/modules/ui/board/components/BoardColumnMenu.tsx +++ b/front/src/modules/ui/board/components/BoardColumnMenu.tsx @@ -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 ( @@ -135,6 +159,16 @@ export const BoardColumnMenu = ({ LeftIcon={IconPencil} text="Rename" /> + + )} {currentMenu === 'add' && ( diff --git a/front/src/modules/ui/board/components/BoardHeader.tsx b/front/src/modules/ui/board/components/BoardHeader.tsx index da64d81e0..4bea188a2 100644 --- a/front/src/modules/ui/board/components/BoardHeader.tsx +++ b/front/src/modules/ui/board/components/BoardHeader.tsx @@ -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 ( { - const [boardColumns] = useRecoilState(boardColumnsState); + const boardColumns = useRecoilValue(boardColumnsState); const setCardSelected = useSetCardSelected(); const [updatePipelineProgressStage] = @@ -151,19 +151,26 @@ export const EntityBoard = ({ {sortedBoardColumns.map((column) => ( - + - + ))} diff --git a/front/src/modules/ui/board/components/EntityBoardColumn.tsx b/front/src/modules/ui/board/components/EntityBoardColumn.tsx index 66203ab00..2f4813dae 100644 --- a/front/src/modules/ui/board/components/EntityBoardColumn.tsx +++ b/front/src/modules/ui/board/components/EntityBoardColumn.tsx @@ -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 ( 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 ( {(droppableProvided) => ( diff --git a/front/src/modules/ui/board/contexts/BoardColumnContext.ts b/front/src/modules/ui/board/contexts/BoardColumnContext.ts new file mode 100644 index 000000000..2a01e79de --- /dev/null +++ b/front/src/modules/ui/board/contexts/BoardColumnContext.ts @@ -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(null); diff --git a/front/src/modules/ui/board/contexts/BoardColumnIdContext.ts b/front/src/modules/ui/board/contexts/BoardColumnIdContext.ts deleted file mode 100644 index e6670b9db..000000000 --- a/front/src/modules/ui/board/contexts/BoardColumnIdContext.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const BoardColumnIdContext = createContext(null); diff --git a/front/src/modules/ui/board/hooks/useBoardColumns.ts b/front/src/modules/ui/board/hooks/useBoardColumns.ts new file mode 100644 index 000000000..92a917eb0 --- /dev/null +++ b/front/src/modules/ui/board/hooks/useBoardColumns.ts @@ -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 }; +}; diff --git a/front/src/modules/ui/board/states/savedBoardColumnsState.ts b/front/src/modules/ui/board/states/savedBoardColumnsState.ts new file mode 100644 index 000000000..df5f50fb8 --- /dev/null +++ b/front/src/modules/ui/board/states/savedBoardColumnsState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { BoardColumnDefinition } from '../types/BoardColumnDefinition'; + +export const savedBoardColumnsState = atom({ + key: 'savedBoardColumnsState', + default: [], +}); diff --git a/front/src/modules/ui/board/states/selectors/canPersistBoardColumnsSelector.ts b/front/src/modules/ui/board/states/selectors/canPersistBoardColumnsSelector.ts new file mode 100644 index 000000000..af24d48a2 --- /dev/null +++ b/front/src/modules/ui/board/states/selectors/canPersistBoardColumnsSelector.ts @@ -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({ + key: 'canPersistBoardCardFieldsScopedFamilySelector', + get: ({ get }) => + !isDeeplyEqual(get(boardColumnsState), get(savedBoardColumnsState)), +}); diff --git a/front/src/modules/ui/table/components/TableColumnDropdownMenu.tsx b/front/src/modules/ui/table/components/TableColumnDropdownMenu.tsx index c11a1cf15..591a3cfc1 100644 --- a/front/src/modules/ui/table/components/TableColumnDropdownMenu.tsx +++ b/front/src/modules/ui/table/components/TableColumnDropdownMenu.tsx @@ -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 = () => { diff --git a/front/src/modules/ui/table/hooks/useTableColumns.ts b/front/src/modules/ui/table/hooks/useTableColumns.ts index 4299d2efb..155737233 100644 --- a/front/src/modules/ui/table/hooks/useTableColumns.ts +++ b/front/src/modules/ui/table/hooks/useTableColumns.ts @@ -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[]) => { setSavedTableColumns(columns); @@ -71,54 +74,28 @@ export const useTableColumns = () => { [tableColumnsByKey, tableColumns, handleColumnsChange], ); - const handleColumnMove = useCallback( - async (direction: string, column: ColumnDefinition) => { + const handleMoveTableColumn = useCallback( + ( + direction: 'left' | 'right', + column: ColumnDefinition, + ) => { 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) => { - handleColumnMove('left', column); - }, - [handleColumnMove], - ); - - const handleColumnRightMove = useCallback( - (column: ColumnDefinition) => { - handleColumnMove('right', column); - }, - [handleColumnMove], + [tableColumns, setTableColumns, handleColumnMove], ); return { handleColumnVisibilityChange, - handleColumnLeftMove, - handleColumnRightMove, + handleMoveTableColumn, handleColumnReorder, handleColumnsChange, }; diff --git a/front/src/modules/views/hooks/useBoardViewFields.ts b/front/src/modules/views/hooks/useBoardViewFields.ts index b21066b7b..e77f509b9 100644 --- a/front/src/modules/views/hooks/useBoardViewFields.ts +++ b/front/src/modules/views/hooks/useBoardViewFields.ts @@ -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'; diff --git a/front/src/modules/views/hooks/useBoardViews.ts b/front/src/modules/views/hooks/useBoardViews.ts index a3656b54c..63c612ab3 100644 --- a/front/src/modules/views/hooks/useBoardViews.ts +++ b/front/src/modules/views/hooks/useBoardViews.ts @@ -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(); }; diff --git a/front/src/modules/views/hooks/useMoveViewColumns.ts b/front/src/modules/views/hooks/useMoveViewColumns.ts new file mode 100644 index 000000000..d6759cb83 --- /dev/null +++ b/front/src/modules/views/hooks/useMoveViewColumns.ts @@ -0,0 +1,35 @@ +export const useMoveViewColumns = () => { + const handleColumnMove = ( + 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 }; +};