diff --git a/front/src/modules/companies/board/components/CompanyBoard.tsx b/front/src/modules/companies/board/components/CompanyBoard.tsx index c851f0e26..145dca41f 100644 --- a/front/src/modules/companies/board/components/CompanyBoard.tsx +++ b/front/src/modules/companies/board/components/CompanyBoard.tsx @@ -5,6 +5,7 @@ import { } from '@/ui/board/components/EntityBoard'; import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; import { useBoardViews } from '@/views/hooks/useBoardViews'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; @@ -16,25 +17,38 @@ type CompanyBoardProps = Pick< 'onColumnAdd' | 'onColumnDelete' | 'onEditColumnTitle' >; -export const CompanyBoard = ({ ...props }: CompanyBoardProps) => { - const { handleViewsChange, handleViewSubmit } = useBoardViews({ - objectId: 'company', - scopeContext: CompanyBoardRecoilScopeContext, - fieldDefinitions: pipelineAvailableFieldDefinitions, - }); +export const CompanyBoard = ({ + onColumnAdd, + onColumnDelete, + onEditColumnTitle, +}: CompanyBoardProps) => { + const { createView, deleteView, submitCurrentView, updateView } = + useBoardViews({ + objectId: 'company', + scopeContext: CompanyBoardRecoilScopeContext, + fieldDefinitions: pipelineAvailableFieldDefinitions, + }); return ( <> - + + + diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index 974891a72..1e34f2ae0 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -8,6 +8,7 @@ import { EntityTableEffect } from '@/ui/table/components/EntityTableEffect'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useTableViews } from '@/views/hooks/useTableViews'; @@ -32,10 +33,11 @@ export function CompanyTable() { const [updateEntityMutation] = useUpdateOneCompanyMutation(); const upsertEntityTableItem = useUpsertEntityTableItem(); - const { handleViewsChange, handleViewSubmit } = useTableViews({ - objectId: 'company', - columnDefinitions: companiesAvailableColumnDefinitions, - }); + const { createView, deleteView, submitCurrentView, updateView } = + useTableViews({ + objectId: 'company', + columnDefinitions: companiesAvailableColumnDefinitions, + }); const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); @@ -61,27 +63,34 @@ export function CompanyTable() { setContextMenuEntries={setContextMenuEntries} setActionBarEntries={setActionBarEntries} /> - - updateEntityMutation({ + + { - if (!data.updateOneCompany) { - return; - } - upsertEntityTableItem(data.updateOneCompany); - }, - }) - } - /> + }: { + variables: UpdateOneCompanyMutationVariables; + }) => + updateEntityMutation({ + variables, + onCompleted: (data) => { + if (!data.updateOneCompany) { + return; + } + upsertEntityTableItem(data.updateOneCompany); + }, + }) + } + /> + ); } diff --git a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx index 0d699bb43..e52f77546 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx @@ -1,4 +1,5 @@ import { EntityTable } from '@/ui/table/components/EntityTable'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; import { useUpdateOneCompanyMutation } from '~/generated/graphql'; import { CompanyTableMockData } from './CompanyTableMockData'; @@ -7,10 +8,9 @@ export function CompanyTableMockMode() { return ( <> - + + + ); } diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index 8ef195a5e..980b65913 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -8,6 +8,7 @@ import { EntityTableEffect } from '@/ui/table/components/EntityTableEffect'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useTableViews } from '@/views/hooks/useTableViews'; @@ -33,10 +34,11 @@ export function PeopleTable() { const upsertEntityTableItem = useUpsertEntityTableItem(); const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); - const { handleViewsChange, handleViewSubmit } = useTableViews({ - objectId: 'person', - columnDefinitions: peopleAvailableColumnDefinitions, - }); + const { createView, deleteView, submitCurrentView, updateView } = + useTableViews({ + objectId: 'person', + columnDefinitions: peopleAvailableColumnDefinitions, + }); const { setContextMenuEntries } = usePersonTableContextMenuEntries(); const { setActionBarEntries } = usePersonTableActionBarEntries(); @@ -60,27 +62,34 @@ export function PeopleTable() { setActionBarEntries={setActionBarEntries} sortDefinitionArray={peopleAvailableSorts} /> - - updateEntityMutation({ + + { - if (!data.updateOnePerson) { - return; - } - upsertEntityTableItem(data.updateOnePerson); - }, - }) - } - /> + }: { + variables: UpdateOnePersonMutationVariables; + }) => + updateEntityMutation({ + variables, + onCompleted: (data) => { + if (!data.updateOnePerson) { + return; + } + upsertEntityTableItem(data.updateOnePerson); + }, + }) + } + /> + ); } diff --git a/front/src/modules/ui/board/components/BoardHeader.tsx b/front/src/modules/ui/board/components/BoardHeader.tsx index 7b2169975..3141a4edc 100644 --- a/front/src/modules/ui/board/components/BoardHeader.tsx +++ b/front/src/modules/ui/board/components/BoardHeader.tsx @@ -1,46 +1,101 @@ -import type { ComponentProps } from 'react'; +import { useContext } from 'react'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; +import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { ViewBar, type ViewBarProps } from '@/ui/view-bar/components/ViewBar'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; +import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; +import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState'; +import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState'; +import { canPersistBoardCardFieldsScopedFamilySelector } from '../states/selectors/canPersistBoardCardFieldsScopedFamilySelector'; import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope'; import { BoardOptionsDropdown } from './BoardOptionsDropdown'; -export type BoardHeaderProps = ComponentProps<'div'> & { +export type BoardHeaderProps = { + className?: string; onStageAdd?: (boardColumn: BoardColumnDefinition) => void; -} & Pick< - ViewBarProps, - 'defaultViewName' | 'onViewsChange' | 'onViewSubmit' | 'scopeContext' - >; +} & Pick; export function BoardHeader({ + className, onStageAdd, - onViewsChange, - onViewSubmit, scopeContext, - defaultViewName, }: BoardHeaderProps) { + const { onCurrentViewSubmit, ...viewBarContextProps } = + useContext(ViewBarContext); + const tableScopeId = useContextScopeId(scopeContext); + + const currentViewId = useRecoilScopedValue( + currentViewIdScopedState, + scopeContext, + ); + const canPersistBoardCardFields = useRecoilValue( + canPersistBoardCardFieldsScopedFamilySelector([ + tableScopeId, + currentViewId, + ]), + ); + const [boardCardFields, setBoardCardFields] = useRecoilScopedState( + boardCardFieldsScopedState, + scopeContext, + ); + const [savedBoardCardFields, setSavedBoardCardFields] = useRecoilState( + savedBoardCardFieldsFamilyState(currentViewId), + ); + + const handleViewBarReset = () => setBoardCardFields(savedBoardCardFields); + + const handleViewSelect = useRecoilCallback( + ({ set, snapshot }) => + async (viewId: string) => { + const savedBoardCardFields = await snapshot.getPromise( + savedBoardCardFieldsFamilyState(viewId), + ); + set(boardCardFieldsScopedState(tableScopeId), savedBoardCardFields); + }, + [tableScopeId], + ); + + const handleCurrentViewSubmit = async () => { + if (canPersistBoardCardFields) { + setSavedBoardCardFields(boardCardFields); + } + + await onCurrentViewSubmit?.(); + }; + return ( - - } - optionsDropdownKey={BoardOptionsDropdownKey} - scopeContext={scopeContext} - /> + + + } + optionsDropdownKey={BoardOptionsDropdownKey} + scopeContext={scopeContext} + /> + ); } diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx index 6a44b2f2b..016b2523a 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdown.tsx @@ -10,20 +10,22 @@ import { type BoardOptionsDropdownProps = Pick< BoardOptionsDropdownContentProps, - 'customHotkeyScope' | 'onStageAdd' | 'onViewsChange' | 'scopeContext' + 'customHotkeyScope' | 'onStageAdd' | 'scopeContext' >; export function BoardOptionsDropdown({ customHotkeyScope, - ...props + onStageAdd, + scopeContext, }: BoardOptionsDropdownProps) { return ( } dropdownComponents={ } dropdownHotkeyScope={customHotkeyScope} diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx index 662db6129..08c403437 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx @@ -1,7 +1,7 @@ import { type Context, useRef, useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; import { v4 } from 'uuid'; @@ -23,15 +23,17 @@ import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate'; import { ThemeColor } from '@/ui/theme/constants/colors'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection'; import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView'; import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; -import type { View } from '@/ui/view-bar/types/View'; import { useBoardCardFields } from '../hooks/useBoardCardFields'; +import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState'; import { boardColumnsState } from '../states/boardColumnsState'; +import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState'; import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector'; import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector'; import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; @@ -40,7 +42,6 @@ import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; export type BoardOptionsDropdownContentProps = { customHotkeyScope: HotkeyScope; onStageAdd?: (boardColumn: BoardColumnDefinition) => void; - onViewsChange?: (views: View[]) => void | Promise; scopeContext: Context; }; @@ -60,10 +61,10 @@ type ColumnForCreate = { export function BoardOptionsDropdownContent({ customHotkeyScope, onStageAdd, - onViewsChange, scopeContext, }: BoardOptionsDropdownContentProps) { const theme = useTheme(); + const scopeId = useContextScopeId(scopeContext); const stageInputRef = useRef(null); const viewEditInputRef = useRef(null); @@ -106,15 +107,24 @@ export function BoardOptionsDropdownContent({ onStageAdd?.(columnToCreate); }; - const { upsertView } = useUpsertView({ - onViewsChange, - scopeContext, - }); + const { upsertView } = useUpsertView({ scopeContext }); - const handleViewNameSubmit = async () => { - const name = viewEditInputRef.current?.value; - await upsertView(name); - }; + const handleViewNameSubmit = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const boardCardFields = await snapshot.getPromise( + boardCardFieldsScopedState(scopeId), + ); + const isCreateMode = viewEditMode.mode === 'create'; + const name = viewEditInputRef.current?.value; + const view = await upsertView(name); + + if (view && isCreateMode) { + set(savedBoardCardFieldsFamilyState(view.id), boardCardFields); + } + }, + [scopeId, upsertView, viewEditMode.mode], + ); const resetMenu = () => setCurrentMenu(undefined); diff --git a/front/src/modules/ui/board/components/EntityBoard.tsx b/front/src/modules/ui/board/components/EntityBoard.tsx index 8f883996c..fb7d08d49 100644 --- a/front/src/modules/ui/board/components/EntityBoard.tsx +++ b/front/src/modules/ui/board/components/EntityBoard.tsx @@ -6,10 +6,7 @@ import { useRecoilState } from 'recoil'; import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { - BoardHeader, - BoardHeaderProps, -} from '@/ui/board/components/BoardHeader'; +import { BoardHeader } from '@/ui/board/components/BoardHeader'; import { StyledBoard } from '@/ui/board/components/StyledBoard'; import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; @@ -39,10 +36,7 @@ export type EntityBoardProps = { onColumnDelete?: (boardColumnId: string) => void; onEditColumnTitle: (columnId: string, title: string, color: string) => void; scopeContext: Context; -} & Pick< - BoardHeaderProps, - 'defaultViewName' | 'onViewsChange' | 'onViewSubmit' ->; +}; const StyledWrapper = styled.div` display: flex; @@ -57,12 +51,9 @@ const StyledBoardHeader = styled(BoardHeader)` export function EntityBoard({ boardOptions, - defaultViewName, onColumnAdd, onColumnDelete, onEditColumnTitle, - onViewsChange, - onViewSubmit, scopeContext, }: EntityBoardProps) { const [boardColumns] = useRecoilState(boardColumnsState); @@ -139,13 +130,7 @@ export function EntityBoard({ return (boardColumns?.length ?? 0) > 0 ? ( - + diff --git a/front/src/modules/ui/board/states/savedBoardCardFieldsFamilyState.ts b/front/src/modules/ui/board/states/savedBoardCardFieldsFamilyState.ts new file mode 100644 index 000000000..17506cb3b --- /dev/null +++ b/front/src/modules/ui/board/states/savedBoardCardFieldsFamilyState.ts @@ -0,0 +1,14 @@ +import { atomFamily } from 'recoil'; + +import type { + ViewFieldDefinition, + ViewFieldMetadata, +} from '@/ui/editable-field/types/ViewField'; + +export const savedBoardCardFieldsFamilyState = atomFamily< + ViewFieldDefinition[], + string | undefined +>({ + key: 'savedBoardCardFieldsFamilyState', + default: [], +}); diff --git a/front/src/modules/ui/board/states/selectors/canPersistBoardCardFieldsScopedFamilySelector.ts b/front/src/modules/ui/board/states/selectors/canPersistBoardCardFieldsScopedFamilySelector.ts new file mode 100644 index 000000000..620e0802c --- /dev/null +++ b/front/src/modules/ui/board/states/selectors/canPersistBoardCardFieldsScopedFamilySelector.ts @@ -0,0 +1,17 @@ +import { selectorFamily } from 'recoil'; + +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState'; +import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState'; + +export const canPersistBoardCardFieldsScopedFamilySelector = selectorFamily({ + key: 'canPersistBoardCardFieldsScopedFamilySelector', + get: + ([scopeId, viewId]: [string, string | undefined]) => + ({ get }) => + !isDeeplyEqual( + get(savedBoardCardFieldsFamilyState(viewId)), + get(boardCardFieldsScopedState(scopeId)), + ), +}); diff --git a/front/src/modules/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector.ts b/front/src/modules/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector.ts new file mode 100644 index 000000000..34db430d1 --- /dev/null +++ b/front/src/modules/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector.ts @@ -0,0 +1,18 @@ +import { selectorFamily } from 'recoil'; + +import type { + ViewFieldDefinition, + ViewFieldMetadata, +} from '@/ui/editable-field/types/ViewField'; + +import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState'; + +export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({ + key: 'savedBoardCardFieldsByKeyFamilySelector', + get: + (viewId: string | undefined) => + ({ get }) => + get(savedBoardCardFieldsFamilyState(viewId)).reduce< + Record> + >((result, field) => ({ ...result, [field.key]: field }), {}), +}); diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index 8da25cbc6..a36b56fc4 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -87,18 +87,9 @@ const StyledTableContainer = styled.div` type OwnProps = { updateEntityMutation: any; -} & Pick< - TableHeaderProps, - 'defaultViewName' | 'onImport' | 'onViewsChange' | 'onViewSubmit' ->; +} & Pick; -export function EntityTable({ - defaultViewName, - onImport, - onViewsChange, - onViewSubmit, - updateEntityMutation, -}: OwnProps) { +export function EntityTable({ onImport, updateEntityMutation }: OwnProps) { const tableBodyRef = useRef(null); const setRowSelectedState = useSetRowSelectedState(); @@ -135,12 +126,7 @@ export function EntityTable({ - +
diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx index 4a74a845f..2f1bead26 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx @@ -1,6 +1,5 @@ import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import type { View } from '@/ui/view-bar/types/View'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; @@ -8,13 +7,11 @@ import { TableOptionsDropdownButton } from './TableOptionsDropdownButton'; import { TableOptionsDropdownContent } from './TableOptionsDropdownContent'; type TableOptionsDropdownProps = { - onViewsChange?: (views: View[]) => void; onImport?: () => void; customHotkeyScope: HotkeyScope; }; export function TableOptionsDropdown({ - onViewsChange, onImport, customHotkeyScope, }: TableOptionsDropdownProps) { @@ -23,12 +20,7 @@ export function TableOptionsDropdown({ buttonComponents={} dropdownHotkeyScope={customHotkeyScope} dropdownId={TableOptionsDropdownId} - dropdownComponents={ - - } + dropdownComponents={} /> ); } diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx index 39fcc462b..031676a00 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; @@ -11,31 +11,33 @@ import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/icon'; import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection'; import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView'; import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; -import type { View } from '@/ui/view-bar/types/View'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; import { useTableColumns } from '../../hooks/useTableColumns'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState'; import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector'; import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector'; +import { tableColumnsScopedState } from '../../states/tableColumnsScopedState'; import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; type TableOptionsDropdownButtonProps = { - onViewsChange?: (views: View[]) => void | Promise; onImport?: () => void; }; type TableOptionsMenu = 'fields'; export function TableOptionsDropdownContent({ - onViewsChange, onImport, }: TableOptionsDropdownButtonProps) { + const scopeId = useContextScopeId(TableRecoilScopeContext); + const { closeDropdownButton } = useDropdownButton({ dropdownId: TableOptionsDropdownId, }); @@ -60,17 +62,28 @@ export function TableOptionsDropdownContent({ TableRecoilScopeContext, ); + const { handleColumnVisibilityChange } = useTableColumns(); + const { upsertView } = useUpsertView({ - onViewsChange, scopeContext: TableRecoilScopeContext, }); - const { handleColumnVisibilityChange } = useTableColumns(); + const handleViewNameSubmit = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const tableColumns = await snapshot.getPromise( + tableColumnsScopedState(scopeId), + ); + const isCreateMode = viewEditMode.mode === 'create'; + const name = viewEditInputRef.current?.value; + const view = await upsertView(name); - const handleViewNameSubmit = async () => { - const name = viewEditInputRef.current?.value; - await upsertView(name); - }; + if (view && isCreateMode) { + set(savedTableColumnsFamilyState(view.id), tableColumns); + } + }, + [scopeId, upsertView, viewEditMode.mode], + ); const handleSelectMenu = (option: TableOptionsMenu) => { handleViewNameSubmit(); diff --git a/front/src/modules/ui/table/table-header/components/TableHeader.tsx b/front/src/modules/ui/table/table-header/components/TableHeader.tsx index ef374ace4..1611bdfe7 100644 --- a/front/src/modules/ui/table/table-header/components/TableHeader.tsx +++ b/front/src/modules/ui/table/table-header/components/TableHeader.tsx @@ -1,3 +1,4 @@ +import { useContext } from 'react'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; @@ -5,7 +6,8 @@ import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope' import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { ViewBar, ViewBarProps } from '@/ui/view-bar/components/ViewBar'; +import { ViewBar } from '@/ui/view-bar/components/ViewBar'; +import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; @@ -18,14 +20,11 @@ import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; export type TableHeaderProps = { onImport?: () => void; -} & Pick; +}; -export function TableHeader({ - onImport, - onViewsChange, - onViewSubmit, - ...props -}: TableHeaderProps) { +export function TableHeader({ onImport }: TableHeaderProps) { + const { onCurrentViewSubmit, ...viewBarContextProps } = + useContext(ViewBarContext); const tableScopeId = useContextScopeId(TableRecoilScopeContext); const currentViewId = useRecoilScopedValue( @@ -43,9 +42,7 @@ export function TableHeader({ savedTableColumnsFamilyState(currentViewId), ); - function handleViewBarReset() { - setTableColumns(savedTableColumns); - } + const handleViewBarReset = () => setTableColumns(savedTableColumns); const handleViewSelect = useRecoilCallback( ({ set, snapshot }) => @@ -58,32 +55,36 @@ export function TableHeader({ [tableScopeId], ); - async function handleViewSubmit() { + async function handleCurrentViewSubmit() { if (canPersistTableColumns) { setSavedTableColumns(tableColumns); } - await onViewSubmit?.(); + await onCurrentViewSubmit?.(); } return ( - - } - optionsDropdownKey={TableOptionsDropdownId} - scopeContext={TableRecoilScopeContext} - /> + + + } + optionsDropdownKey={TableOptionsDropdownId} + scopeContext={TableRecoilScopeContext} + /> + ); } diff --git a/front/src/modules/ui/top-bar/TopBar.tsx b/front/src/modules/ui/top-bar/TopBar.tsx index 71dfe2ba3..2d96eef7b 100644 --- a/front/src/modules/ui/top-bar/TopBar.tsx +++ b/front/src/modules/ui/top-bar/TopBar.tsx @@ -1,7 +1,8 @@ -import type { ComponentProps, ReactNode } from 'react'; +import type { ReactNode } from 'react'; import styled from '@emotion/styled'; -type OwnProps = ComponentProps<'div'> & { +type OwnProps = { + className?: string; leftComponent?: ReactNode; rightComponent?: ReactNode; bottomComponent?: ReactNode; @@ -40,14 +41,14 @@ const StyledRightSection = styled.div` `; export function TopBar({ + className, leftComponent, rightComponent, bottomComponent, displayBottomBorder = true, - ...props }: OwnProps) { return ( - + {leftComponent} {rightComponent} diff --git a/front/src/modules/ui/view-bar/components/UpdateViewButtonGroup.tsx b/front/src/modules/ui/view-bar/components/UpdateViewButtonGroup.tsx index 878a461fe..ec3d242b3 100644 --- a/front/src/modules/ui/view-bar/components/UpdateViewButtonGroup.tsx +++ b/front/src/modules/ui/view-bar/components/UpdateViewButtonGroup.tsx @@ -1,4 +1,4 @@ -import { type Context, useCallback, useState } from 'react'; +import { type Context, useCallback, useContext, useState } from 'react'; import styled from '@emotion/styled'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; @@ -21,6 +21,8 @@ import { canPersistSortsScopedFamilySelector } from '@/ui/view-bar/states/select import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; +import { ViewBarContext } from '../contexts/ViewBarContext'; + const StyledContainer = styled.div` display: inline-flex; margin-right: ${({ theme }) => theme.spacing(2)}; @@ -28,22 +30,20 @@ const StyledContainer = styled.div` `; export type UpdateViewButtonGroupProps = { - canPersistViewFields?: boolean; hotkeyScope: string; onViewEditModeChange?: () => void; - onViewSubmit?: () => void | Promise; scopeContext: Context; }; export const UpdateViewButtonGroup = ({ - canPersistViewFields, hotkeyScope, onViewEditModeChange, - onViewSubmit, scopeContext, }: UpdateViewButtonGroupProps) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const { canPersistViewFields, onCurrentViewSubmit } = + useContext(ViewBarContext); const recoilScopeId = useContextScopeId(scopeContext); const currentViewId = useRecoilScopedValue( @@ -89,7 +89,7 @@ export const UpdateViewButtonGroup = ({ if (canPersistFilters) setSavedFilters(filters); if (canPersistSorts) setSavedSorts(sorts); - await onViewSubmit?.(); + await onCurrentViewSubmit?.(); }; useScopedHotkeys( diff --git a/front/src/modules/ui/view-bar/components/ViewBar.tsx b/front/src/modules/ui/view-bar/components/ViewBar.tsx index 0d1e7becf..3dc27cd33 100644 --- a/front/src/modules/ui/view-bar/components/ViewBar.tsx +++ b/front/src/modules/ui/view-bar/components/ViewBar.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps, Context, ReactNode } from 'react'; +import type { Context, ReactNode } from 'react'; import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; import { TopBar } from '@/ui/top-bar/TopBar'; @@ -8,38 +8,22 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope'; import { FilterDropdownButton } from './FilterDropdownButton'; import { SortDropdownButton } from './SortDropdownButton'; -import { - UpdateViewButtonGroup, - type UpdateViewButtonGroupProps, -} from './UpdateViewButtonGroup'; -import ViewBarDetails, { type ViewBarDetailsProps } from './ViewBarDetails'; -import { - ViewsDropdownButton, - type ViewsDropdownButtonProps, -} from './ViewsDropdownButton'; +import { UpdateViewButtonGroup } from './UpdateViewButtonGroup'; +import ViewBarDetails from './ViewBarDetails'; +import { ViewsDropdownButton } from './ViewsDropdownButton'; -export type ViewBarProps = ComponentProps<'div'> & { +export type ViewBarProps = { + className?: string; optionsDropdownButton: ReactNode; optionsDropdownKey: string; scopeContext: Context; -} & Pick< - ViewsDropdownButtonProps, - 'defaultViewName' | 'onViewsChange' | 'onViewSelect' - > & - Pick & - Pick; +}; export const ViewBar = ({ - canPersistViewFields, - defaultViewName, - onReset, - onViewsChange, - onViewSelect, - onViewSubmit, + className, optionsDropdownButton, optionsDropdownKey, scopeContext, - ...props }: ViewBarProps) => { const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({ dropdownId: optionsDropdownKey, @@ -47,13 +31,10 @@ export const ViewBar = ({ return ( @@ -75,15 +56,11 @@ export const ViewBar = ({ } bottomComponent={ diff --git a/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx b/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx index 59d7cf8af..f721a1a53 100644 --- a/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx +++ b/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx @@ -1,4 +1,4 @@ -import type { Context, ReactNode } from 'react'; +import { type Context, type ReactNode, useContext } from 'react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; @@ -7,6 +7,7 @@ import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextS import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { ViewBarContext } from '../contexts/ViewBarContext'; import { useRemoveFilter } from '../hooks/useRemoveFilter'; import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; @@ -23,10 +24,8 @@ import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton'; import SortOrFilterChip from './SortOrFilterChip'; export type ViewBarDetailsProps = { - canPersistViewFields?: boolean; context: Context; hasFilterButton?: boolean; - onReset?: () => void; rightComponent?: ReactNode; }; @@ -99,12 +98,11 @@ const StyledAddFilterContainer = styled.div` `; function ViewBarDetails({ - canPersistViewFields, context, hasFilterButton = false, - onReset, rightComponent, }: ViewBarDetailsProps) { + const { canPersistViewFields, onViewBarReset } = useContext(ViewBarContext); const recoilScopeId = useContextScopeId(context); const currentViewId = useRecoilScopedValue(currentViewIdScopedState, context); @@ -155,7 +153,7 @@ function ViewBarDetails({ const removeFilter = useRemoveFilter(context); function handleCancelClick() { - onReset?.(); + onViewBarReset?.(); setFilters(savedFilters); setSorts(savedSorts); } diff --git a/front/src/modules/ui/view-bar/components/ViewsDropdownButton.tsx b/front/src/modules/ui/view-bar/components/ViewsDropdownButton.tsx index f6fa901fa..280363b10 100644 --- a/front/src/modules/ui/view-bar/components/ViewsDropdownButton.tsx +++ b/front/src/modules/ui/view-bar/components/ViewsDropdownButton.tsx @@ -2,6 +2,7 @@ import { type Context, type MouseEvent, useCallback, + useContext, useEffect, useState, } from 'react'; @@ -22,7 +23,6 @@ import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import DropdownButton from '@/ui/view-bar/components/DropdownButton'; import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; @@ -33,10 +33,12 @@ import { currentViewScopedSelector } from '@/ui/view-bar/states/selectors/curren import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewsScopedState } from '@/ui/view-bar/states/viewsScopedState'; -import type { View } from '@/ui/view-bar/types/View'; import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope'; import { assertNotNull } from '~/utils/assert'; +import { ViewBarContext } from '../contexts/ViewBarContext'; +import { useRemoveView } from '../hooks/useRemoveView'; + const StyledBoldDropdownMenuItemsContainer = styled( StyledDropdownMenuItemsContainer, )` @@ -71,39 +73,28 @@ const StyledViewName = styled.span` `; export type ViewsDropdownButtonProps = { - defaultViewName: string; hotkeyScope: ViewsHotkeyScope; onViewEditModeChange?: () => void; - onViewsChange?: (views: View[]) => void | Promise; - onViewSelect?: (viewId: string) => void | Promise; scopeContext: Context; }; export const ViewsDropdownButton = ({ - defaultViewName, hotkeyScope, onViewEditModeChange, - onViewsChange, - onViewSelect, scopeContext, }: ViewsDropdownButtonProps) => { const theme = useTheme(); - const [isUnfolded, setIsUnfolded] = useState(false); + const { defaultViewName, onViewSelect } = useContext(ViewBarContext); const recoilScopeId = useContextScopeId(scopeContext); - const [, setCurrentViewId] = useRecoilScopedState( - currentViewIdScopedState, - scopeContext, - ); + const [isUnfolded, setIsUnfolded] = useState(false); + const currentView = useRecoilScopedValue( currentViewScopedSelector, scopeContext, ); - const [views, setViews] = useRecoilScopedState( - viewsScopedState, - scopeContext, - ); + const views = useRecoilScopedValue(viewsScopedState, scopeContext); const setViewEditMode = useSetRecoilState(viewEditModeState); const { @@ -146,20 +137,17 @@ export const ViewsDropdownButton = ({ [setViewEditMode, onViewEditModeChange], ); - const handleDeleteViewButtonClick = useCallback( - async (event: MouseEvent, viewId: string) => { - event.stopPropagation(); + const { removeView } = useRemoveView({ scopeContext }); - if (currentView?.id === viewId) setCurrentViewId(undefined); + const handleDeleteViewButtonClick = async ( + event: MouseEvent, + viewId: string, + ) => { + event.stopPropagation(); - const nextViews = views.filter((view) => view.id !== viewId); - - setViews(nextViews); - await onViewsChange?.(nextViews); - setIsUnfolded(false); - }, - [currentView?.id, onViewsChange, setCurrentViewId, setViews, views], - ); + await removeView(viewId); + setIsUnfolded(false); + }; useEffect(() => { isUnfolded diff --git a/front/src/modules/ui/view-bar/contexts/ViewBarContext.ts b/front/src/modules/ui/view-bar/contexts/ViewBarContext.ts new file mode 100644 index 000000000..66ae8630d --- /dev/null +++ b/front/src/modules/ui/view-bar/contexts/ViewBarContext.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react'; + +import type { View } from '../types/View'; + +export const ViewBarContext = createContext<{ + canPersistViewFields?: boolean; + defaultViewName?: string; + onCurrentViewSubmit?: () => void | Promise; + onViewBarReset?: () => void; + onViewCreate?: (view: View) => void | Promise; + onViewEdit?: (view: View) => void | Promise; + onViewRemove?: (viewId: string) => void | Promise; + onViewSelect?: (viewId: string) => void | Promise; +}>({}); diff --git a/front/src/modules/ui/view-bar/hooks/useRemoveView.ts b/front/src/modules/ui/view-bar/hooks/useRemoveView.ts new file mode 100644 index 000000000..8f9f66da9 --- /dev/null +++ b/front/src/modules/ui/view-bar/hooks/useRemoveView.ts @@ -0,0 +1,37 @@ +import { type Context, useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; + +import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; + +import { ViewBarContext } from '../contexts/ViewBarContext'; +import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; +import { viewsScopedState } from '../states/viewsScopedState'; + +export const useRemoveView = ({ + scopeContext, +}: { + scopeContext: Context; +}) => { + const { onViewRemove } = useContext(ViewBarContext); + const recoilScopeId = useContextScopeId(scopeContext); + + const removeView = useRecoilCallback( + ({ set, snapshot }) => + async (viewId: string) => { + const currentViewId = await snapshot.getPromise( + currentViewIdScopedState(recoilScopeId), + ); + + if (currentViewId === viewId) + set(currentViewIdScopedState(recoilScopeId), undefined); + + set(viewsScopedState(recoilScopeId), (previousViews) => + previousViews.filter((view) => view.id !== viewId), + ); + await onViewRemove?.(viewId); + }, + [onViewRemove, recoilScopeId], + ); + + return { removeView }; +}; diff --git a/front/src/modules/ui/view-bar/hooks/useUpsertView.ts b/front/src/modules/ui/view-bar/hooks/useUpsertView.ts index a52b6ba39..2dd0b8fd4 100644 --- a/front/src/modules/ui/view-bar/hooks/useUpsertView.ts +++ b/front/src/modules/ui/view-bar/hooks/useUpsertView.ts @@ -1,37 +1,30 @@ -import { Context, useCallback } from 'react'; +import { type Context, useCallback, useContext } from 'react'; import { useRecoilCallback, useRecoilState } from 'recoil'; import { v4 } from 'uuid'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { ViewBarContext } from '../contexts/ViewBarContext'; import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; import { filtersScopedState } from '../states/filtersScopedState'; import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState'; import { savedSortsFamilyState } from '../states/savedSortsFamilyState'; +import { viewsByIdScopedSelector } from '../states/selectors/viewsByIdScopedSelector'; import { sortsScopedState } from '../states/sortsScopedState'; import { viewEditModeState } from '../states/viewEditModeState'; import { viewsScopedState } from '../states/viewsScopedState'; -import type { View } from '../types/View'; export const useUpsertView = ({ - onViewsChange, scopeContext, }: { - onViewsChange?: (views: View[]) => void | Promise; scopeContext: Context; }) => { + const { onViewCreate, onViewEdit } = useContext(ViewBarContext); + const recoilScopeId = useContextScopeId(scopeContext); + const filters = useRecoilScopedValue(filtersScopedState, scopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext); - - const [, setCurrentViewId] = useRecoilScopedState( - currentViewIdScopedState, - scopeContext, - ); - const [views, setViews] = useRecoilScopedState( - viewsScopedState, - scopeContext, - ); const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState); const resetViewEditMode = useCallback( @@ -40,44 +33,63 @@ export const useUpsertView = ({ ); const upsertView = useRecoilCallback( - ({ set }) => + ({ set, snapshot }) => async (name?: string) => { - if (!viewEditMode.mode || !name) return resetViewEditMode(); + if (!viewEditMode.mode || !name) { + resetViewEditMode(); + return; + } if (viewEditMode.mode === 'create') { - const viewToCreate = { id: v4(), name }; - const nextViews = [...views, viewToCreate]; + const createdView = { id: v4(), name }; - set(savedFiltersFamilyState(viewToCreate.id), filters); - set(savedSortsFamilyState(viewToCreate.id), sorts); + set(savedFiltersFamilyState(createdView.id), filters); + set(savedSortsFamilyState(createdView.id), sorts); - setViews(nextViews); - await onViewsChange?.(nextViews); + set(viewsScopedState(recoilScopeId), (previousViews) => [ + ...previousViews, + createdView, + ]); - setCurrentViewId(viewToCreate.id); + await onViewCreate?.(createdView); + + resetViewEditMode(); + + set(currentViewIdScopedState(recoilScopeId), createdView.id); + + return createdView; } - if (viewEditMode.mode === 'edit') { - const nextViews = views.map((view) => - view.id === viewEditMode.viewId ? { ...view, name } : view, + if (viewEditMode.mode === 'edit' && viewEditMode.viewId) { + const viewsById = await snapshot.getPromise( + viewsByIdScopedSelector(recoilScopeId), + ); + const editedView = { ...viewsById[viewEditMode.viewId], name }; + + set(viewsScopedState(recoilScopeId), (previousViews) => + previousViews.map((previousView) => + previousView.id === viewEditMode.viewId + ? editedView + : previousView, + ), ); - setViews(nextViews); - await onViewsChange?.(nextViews); - } + await onViewEdit?.(editedView); - return resetViewEditMode(); + resetViewEditMode(); + + return editedView; + } }, [ filters, - onViewsChange, + onViewCreate, + onViewEdit, + recoilScopeId, resetViewEditMode, - setCurrentViewId, - setViews, sorts, viewEditMode.mode, viewEditMode.viewId, - views, ], ); diff --git a/front/src/modules/views/hooks/useBoardViewFields.ts b/front/src/modules/views/hooks/useBoardViewFields.ts index 1742279f5..019c40ae2 100644 --- a/front/src/modules/views/hooks/useBoardViewFields.ts +++ b/front/src/modules/views/hooks/useBoardViewFields.ts @@ -1,7 +1,10 @@ import { type Context } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { availableBoardCardFieldsScopedState } from '@/ui/board/states/availableBoardCardFieldsScopedState'; import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState'; +import { savedBoardCardFieldsFamilyState } from '@/ui/board/states/savedBoardCardFieldsFamilyState'; +import { savedBoardCardFieldsByKeyFamilySelector } from '@/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector'; import type { ViewFieldDefinition, ViewFieldMetadata, @@ -13,6 +16,7 @@ import { SortOrder, useCreateViewFieldsMutation, useGetViewFieldsQuery, + useUpdateViewFieldMutation, } from '~/generated/graphql'; import { assertNotNull } from '~/utils/assert'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; @@ -49,8 +53,15 @@ export const useBoardViewFields = ({ boardCardFieldsScopedState, scopeContext, ); + const setSavedBoardCardFields = useSetRecoilState( + savedBoardCardFieldsFamilyState(currentViewId), + ); + const savedBoardCardFieldsByKey = useRecoilValue( + savedBoardCardFieldsByKeyFamilySelector(currentViewId), + ); const [createViewFieldsMutation] = useCreateViewFieldsMutation(); + const [updateViewFieldMutation] = useUpdateViewFieldMutation(); const createViewFields = ( fields: ViewFieldDefinition[], @@ -68,6 +79,27 @@ export const useBoardViewFields = ({ }); }; + const updateViewFields = ( + fields: ViewFieldDefinition[], + ) => { + if (!currentViewId || !fields.length) return; + + return Promise.all( + fields.map((field) => + updateViewFieldMutation({ + variables: { + data: { + isVisible: field.isVisible, + }, + where: { + viewId_key: { key: field.key, viewId: currentViewId }, + }, + }, + }), + ), + ); + }; + const { refetch } = useGetViewFieldsQuery({ skip: !currentViewId || skipFetch, variables: { @@ -102,6 +134,7 @@ export const useBoardViewFields = ({ .filter>(assertNotNull); if (!isDeeplyEqual(boardCardFields, nextFields)) { + setSavedBoardCardFields(nextFields); setBoardCardFields(nextFields); } @@ -110,4 +143,24 @@ export const useBoardViewFields = ({ } }, }); + + const persistCardFields = async () => { + if (!currentViewId) return; + + const viewFieldsToCreate = boardCardFields.filter( + (field) => !savedBoardCardFieldsByKey[field.key], + ); + await createViewFields(viewFieldsToCreate); + + const viewFieldsToUpdate = boardCardFields.filter( + (field) => + savedBoardCardFieldsByKey[field.key] && + savedBoardCardFieldsByKey[field.key].isVisible !== field.isVisible, + ); + await updateViewFields(viewFieldsToUpdate); + + return refetch(); + }; + + return { createViewFields, persistCardFields }; }; diff --git a/front/src/modules/views/hooks/useBoardViews.ts b/front/src/modules/views/hooks/useBoardViews.ts index 9f01509dc..28b1ec2dc 100644 --- a/front/src/modules/views/hooks/useBoardViews.ts +++ b/front/src/modules/views/hooks/useBoardViews.ts @@ -1,5 +1,6 @@ import type { Context } from 'react'; +import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState'; import type { ViewFieldDefinition, ViewFieldMetadata, @@ -23,17 +24,21 @@ export const useBoardViews = ({ objectId: 'company'; scopeContext: Context; }) => { + const boardCardFields = useRecoilScopedValue( + boardCardFieldsScopedState, + scopeContext, + ); const filters = useRecoilScopedValue(filtersScopedState, scopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext); - const { handleViewsChange, isFetchingViews } = useViews({ + const { createView, deleteView, isFetchingViews, updateView } = useViews({ objectId, onViewCreate: handleViewCreate, type: ViewType.Pipeline, scopeContext, }); - useBoardViewFields({ + const { createViewFields, persistCardFields } = useBoardViewFields({ objectId, fieldDefinitions, scopeContext, @@ -51,14 +56,16 @@ export const useBoardViews = ({ }); async function handleViewCreate(viewId: string) { + await createViewFields(boardCardFields, viewId); await createViewFilters(filters, viewId); await createViewSorts(sorts, viewId); } - const handleViewSubmit = async () => { + const submitCurrentView = async () => { + await persistCardFields(); await persistFilters(); await persistSorts(); }; - return { handleViewsChange, handleViewSubmit }; + return { createView, deleteView, submitCurrentView, updateView }; }; diff --git a/front/src/modules/views/hooks/useTableViews.ts b/front/src/modules/views/hooks/useTableViews.ts index fbe87ec81..ff878a547 100644 --- a/front/src/modules/views/hooks/useTableViews.ts +++ b/front/src/modules/views/hooks/useTableViews.ts @@ -29,7 +29,7 @@ export const useTableViews = ({ ); const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext); - const { handleViewsChange, isFetchingViews } = useViews({ + const { createView, deleteView, isFetchingViews, updateView } = useViews({ objectId, onViewCreate: handleViewCreate, type: ViewType.Table, @@ -55,11 +55,11 @@ export const useTableViews = ({ await createViewSorts(sorts, viewId); } - const handleViewSubmit = async () => { + const submitCurrentView = async () => { await persistColumns(); await persistFilters(); await persistSorts(); }; - return { handleViewsChange, handleViewSubmit }; + return { createView, deleteView, submitCurrentView, updateView }; }; diff --git a/front/src/modules/views/hooks/useViews.ts b/front/src/modules/views/hooks/useViews.ts index 23c373e08..2d263c156 100644 --- a/front/src/modules/views/hooks/useViews.ts +++ b/front/src/modules/views/hooks/useViews.ts @@ -1,13 +1,13 @@ import type { Context } from 'react'; +import { getOperationName } from '@apollo/client/utilities'; import { useRecoilCallback } from 'recoil'; +import { savedBoardCardFieldsFamilyState } from '@/ui/board/states/savedBoardCardFieldsFamilyState'; import { savedTableColumnsFamilyState } from '@/ui/table/states/savedTableColumnsFamilyState'; 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 { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState'; import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState'; -import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewsScopedState } from '@/ui/view-bar/states/viewsScopedState'; import type { View } from '@/ui/view-bar/types/View'; import { @@ -19,6 +19,8 @@ import { } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { GET_VIEWS } from '../graphql/queries/getViews'; + export const useViews = ({ objectId, onViewCreate, @@ -38,7 +40,6 @@ export const useViews = ({ viewsScopedState, scopeContext, ); - const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext); const [createViewMutation] = useCreateViewMutation(); const [updateViewMutation] = useUpdateViewMutation(); @@ -53,26 +54,34 @@ export const useViews = ({ type, }, }, + refetchQueries: [getOperationName(GET_VIEWS) ?? ''], }); if (data?.view) await onViewCreate?.(data.view.id); }; - const updateView = (view: View) => - updateViewMutation({ + const updateView = async (view: View) => { + await updateViewMutation({ variables: { data: { name: view.name }, where: { id: view.id }, }, + refetchQueries: [getOperationName(GET_VIEWS) ?? ''], }); + }; - const deleteView = (viewId: string) => - deleteViewMutation({ variables: { where: { id: viewId } } }); + const deleteView = async (viewId: string) => { + await deleteViewMutation({ + variables: { where: { id: viewId } }, + refetchQueries: [getOperationName(GET_VIEWS) ?? ''], + }); + }; const handleResetSavedViews = useRecoilCallback( ({ reset }) => () => { views.forEach((view) => { + reset(savedBoardCardFieldsFamilyState(view.id)); reset(savedTableColumnsFamilyState(view.id)); reset(savedFiltersFamilyState(view.id)); reset(savedSortsFamilyState(view.id)); @@ -81,7 +90,7 @@ export const useViews = ({ [views], ); - const { loading, refetch } = useGetViewsQuery({ + const { loading } = useGetViewsQuery({ variables: { where: { objectId: { equals: objectId }, @@ -115,32 +124,10 @@ export const useViews = ({ }, }); - const handleViewsChange = async (nextViews: View[]) => { - const viewToCreate = nextViews.find((nextView) => !viewsById[nextView.id]); - if (viewToCreate) { - await createView(viewToCreate); - await refetch(); - return; - } - - const viewToUpdate = nextViews.find( - (nextView) => - viewsById[nextView.id] && viewsById[nextView.id].name !== nextView.name, - ); - if (viewToUpdate) { - await updateView(viewToUpdate); - await refetch(); - return; - } - - const nextViewIds = nextViews.map((nextView) => nextView.id); - const viewIdToDelete = Object.keys(viewsById).find( - (previousViewId) => !nextViewIds.includes(previousViewId), - ); - if (viewIdToDelete) await deleteView(viewIdToDelete); - - await refetch(); + return { + createView, + deleteView, + isFetchingViews: loading, + updateView, }; - - return { handleViewsChange, isFetchingViews: loading }; };