feat: persist board card fields (#1566)

Closes #1538
This commit is contained in:
Thaïs
2023-09-15 00:06:15 +02:00
committed by GitHub
parent 6462505a86
commit 2461a387ce
27 changed files with 541 additions and 342 deletions

View File

@ -5,6 +5,7 @@ import {
} from '@/ui/board/components/EntityBoard'; } from '@/ui/board/components/EntityBoard';
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { useBoardViews } from '@/views/hooks/useBoardViews'; import { useBoardViews } from '@/views/hooks/useBoardViews';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
@ -16,25 +17,38 @@ type CompanyBoardProps = Pick<
'onColumnAdd' | 'onColumnDelete' | 'onEditColumnTitle' 'onColumnAdd' | 'onColumnDelete' | 'onEditColumnTitle'
>; >;
export const CompanyBoard = ({ ...props }: CompanyBoardProps) => { export const CompanyBoard = ({
const { handleViewsChange, handleViewSubmit } = useBoardViews({ onColumnAdd,
objectId: 'company', onColumnDelete,
scopeContext: CompanyBoardRecoilScopeContext, onEditColumnTitle,
fieldDefinitions: pipelineAvailableFieldDefinitions, }: CompanyBoardProps) => {
}); const { createView, deleteView, submitCurrentView, updateView } =
useBoardViews({
objectId: 'company',
scopeContext: CompanyBoardRecoilScopeContext,
fieldDefinitions: pipelineAvailableFieldDefinitions,
});
return ( return (
<> <>
<HooksCompanyBoard /> <HooksCompanyBoard />
<EntityBoard <ViewBarContext.Provider
boardOptions={opportunitiesBoardOptions} value={{
defaultViewName="All opportunities" defaultViewName: 'All Opportunities',
onViewsChange={handleViewsChange} onCurrentViewSubmit: submitCurrentView,
onViewSubmit={handleViewSubmit} onViewCreate: createView,
onColumnAdd={props.onColumnAdd} onViewEdit: updateView,
scopeContext={CompanyBoardRecoilScopeContext} onViewRemove: deleteView,
onEditColumnTitle={props.onEditColumnTitle} }}
/> >
<EntityBoard
boardOptions={opportunitiesBoardOptions}
onColumnAdd={onColumnAdd}
onColumnDelete={onColumnDelete}
onEditColumnTitle={onEditColumnTitle}
scopeContext={CompanyBoardRecoilScopeContext}
/>
</ViewBarContext.Provider>
<EntityBoardActionBar /> <EntityBoardActionBar />
<EntityBoardContextMenu /> <EntityBoardContextMenu />
</> </>

View File

@ -8,6 +8,7 @@ import { EntityTableEffect } from '@/ui/table/components/EntityTableEffect';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; 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 { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector';
import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector';
import { useTableViews } from '@/views/hooks/useTableViews'; import { useTableViews } from '@/views/hooks/useTableViews';
@ -32,10 +33,11 @@ export function CompanyTable() {
const [updateEntityMutation] = useUpdateOneCompanyMutation(); const [updateEntityMutation] = useUpdateOneCompanyMutation();
const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertEntityTableItem = useUpsertEntityTableItem();
const { handleViewsChange, handleViewSubmit } = useTableViews({ const { createView, deleteView, submitCurrentView, updateView } =
objectId: 'company', useTableViews({
columnDefinitions: companiesAvailableColumnDefinitions, objectId: 'company',
}); columnDefinitions: companiesAvailableColumnDefinitions,
});
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
@ -61,27 +63,34 @@ export function CompanyTable() {
setContextMenuEntries={setContextMenuEntries} setContextMenuEntries={setContextMenuEntries}
setActionBarEntries={setActionBarEntries} setActionBarEntries={setActionBarEntries}
/> />
<EntityTable <ViewBarContext.Provider
defaultViewName="All Companies" value={{
onViewsChange={handleViewsChange} defaultViewName: 'All Companies',
onViewSubmit={handleViewSubmit} onCurrentViewSubmit: submitCurrentView,
onImport={handleImport} onViewCreate: createView,
updateEntityMutation={({ onViewEdit: updateView,
variables, onViewRemove: deleteView,
}: { }}
variables: UpdateOneCompanyMutationVariables; >
}) => <EntityTable
updateEntityMutation({ onImport={handleImport}
updateEntityMutation={({
variables, variables,
onCompleted: (data) => { }: {
if (!data.updateOneCompany) { variables: UpdateOneCompanyMutationVariables;
return; }) =>
} updateEntityMutation({
upsertEntityTableItem(data.updateOneCompany); variables,
}, onCompleted: (data) => {
}) if (!data.updateOneCompany) {
} return;
/> }
upsertEntityTableItem(data.updateOneCompany);
},
})
}
/>
</ViewBarContext.Provider>
</> </>
); );
} }

View File

@ -1,4 +1,5 @@
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { useUpdateOneCompanyMutation } from '~/generated/graphql'; import { useUpdateOneCompanyMutation } from '~/generated/graphql';
import { CompanyTableMockData } from './CompanyTableMockData'; import { CompanyTableMockData } from './CompanyTableMockData';
@ -7,10 +8,9 @@ export function CompanyTableMockMode() {
return ( return (
<> <>
<CompanyTableMockData /> <CompanyTableMockData />
<EntityTable <ViewBarContext.Provider value={{ defaultViewName: 'All Companies' }}>
defaultViewName="All Companies" <EntityTable updateEntityMutation={[useUpdateOneCompanyMutation()]} />
updateEntityMutation={[useUpdateOneCompanyMutation()]} </ViewBarContext.Provider>
/>
</> </>
); );
} }

View File

@ -8,6 +8,7 @@ import { EntityTableEffect } from '@/ui/table/components/EntityTableEffect';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; 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 { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector';
import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector';
import { useTableViews } from '@/views/hooks/useTableViews'; import { useTableViews } from '@/views/hooks/useTableViews';
@ -33,10 +34,11 @@ export function PeopleTable() {
const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertEntityTableItem = useUpsertEntityTableItem();
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { handleViewsChange, handleViewSubmit } = useTableViews({ const { createView, deleteView, submitCurrentView, updateView } =
objectId: 'person', useTableViews({
columnDefinitions: peopleAvailableColumnDefinitions, objectId: 'person',
}); columnDefinitions: peopleAvailableColumnDefinitions,
});
const { setContextMenuEntries } = usePersonTableContextMenuEntries(); const { setContextMenuEntries } = usePersonTableContextMenuEntries();
const { setActionBarEntries } = usePersonTableActionBarEntries(); const { setActionBarEntries } = usePersonTableActionBarEntries();
@ -60,27 +62,34 @@ export function PeopleTable() {
setActionBarEntries={setActionBarEntries} setActionBarEntries={setActionBarEntries}
sortDefinitionArray={peopleAvailableSorts} sortDefinitionArray={peopleAvailableSorts}
/> />
<EntityTable <ViewBarContext.Provider
defaultViewName="All People" value={{
onViewsChange={handleViewsChange} defaultViewName: 'All People',
onViewSubmit={handleViewSubmit} onCurrentViewSubmit: submitCurrentView,
onImport={handleImport} onViewCreate: createView,
updateEntityMutation={({ onViewEdit: updateView,
variables, onViewRemove: deleteView,
}: { }}
variables: UpdateOnePersonMutationVariables; >
}) => <EntityTable
updateEntityMutation({ onImport={handleImport}
updateEntityMutation={({
variables, variables,
onCompleted: (data) => { }: {
if (!data.updateOnePerson) { variables: UpdateOnePersonMutationVariables;
return; }) =>
} updateEntityMutation({
upsertEntityTableItem(data.updateOnePerson); variables,
}, onCompleted: (data) => {
}) if (!data.updateOnePerson) {
} return;
/> }
upsertEntityTableItem(data.updateOnePerson);
},
})
}
/>
</ViewBarContext.Provider>
</> </>
); );
} }

View File

@ -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 { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; 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 { 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 type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope'; import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
import { BoardOptionsDropdown } from './BoardOptionsDropdown'; import { BoardOptionsDropdown } from './BoardOptionsDropdown';
export type BoardHeaderProps = ComponentProps<'div'> & { export type BoardHeaderProps = {
className?: string;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void; onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
} & Pick< } & Pick<ViewBarProps, 'scopeContext'>;
ViewBarProps,
'defaultViewName' | 'onViewsChange' | 'onViewSubmit' | 'scopeContext'
>;
export function BoardHeader({ export function BoardHeader({
className,
onStageAdd, onStageAdd,
onViewsChange,
onViewSubmit,
scopeContext, scopeContext,
defaultViewName,
}: BoardHeaderProps) { }: 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 ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<ViewBar <ViewBarContext.Provider
defaultViewName={defaultViewName} value={{
onViewsChange={onViewsChange} ...viewBarContextProps,
onViewSubmit={onViewSubmit} canPersistViewFields: canPersistBoardCardFields,
optionsDropdownButton={ onCurrentViewSubmit: handleCurrentViewSubmit,
<BoardOptionsDropdown onViewBarReset: handleViewBarReset,
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }} onViewSelect: handleViewSelect,
onStageAdd={onStageAdd} }}
onViewsChange={onViewsChange} >
scopeContext={scopeContext} <ViewBar
/> className={className}
} optionsDropdownButton={
optionsDropdownKey={BoardOptionsDropdownKey} <BoardOptionsDropdown
scopeContext={scopeContext} customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
/> onStageAdd={onStageAdd}
scopeContext={scopeContext}
/>
}
optionsDropdownKey={BoardOptionsDropdownKey}
scopeContext={scopeContext}
/>
</ViewBarContext.Provider>
</RecoilScope> </RecoilScope>
); );
} }

View File

@ -10,20 +10,22 @@ import {
type BoardOptionsDropdownProps = Pick< type BoardOptionsDropdownProps = Pick<
BoardOptionsDropdownContentProps, BoardOptionsDropdownContentProps,
'customHotkeyScope' | 'onStageAdd' | 'onViewsChange' | 'scopeContext' 'customHotkeyScope' | 'onStageAdd' | 'scopeContext'
>; >;
export function BoardOptionsDropdown({ export function BoardOptionsDropdown({
customHotkeyScope, customHotkeyScope,
...props onStageAdd,
scopeContext,
}: BoardOptionsDropdownProps) { }: BoardOptionsDropdownProps) {
return ( return (
<DropdownButton <DropdownButton
buttonComponents={<BoardOptionsDropdownButton />} buttonComponents={<BoardOptionsDropdownButton />}
dropdownComponents={ dropdownComponents={
<BoardOptionsDropdownContent <BoardOptionsDropdownContent
{...props}
customHotkeyScope={customHotkeyScope} customHotkeyScope={customHotkeyScope}
onStageAdd={onStageAdd}
scopeContext={scopeContext}
/> />
} }
dropdownHotkeyScope={customHotkeyScope} dropdownHotkeyScope={customHotkeyScope}

View File

@ -1,7 +1,7 @@
import { type Context, useRef, useState } from 'react'; import { type Context, useRef, useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -23,15 +23,17 @@ import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate';
import { ThemeColor } from '@/ui/theme/constants/colors'; import { ThemeColor } from '@/ui/theme/constants/colors';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; 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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection'; import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection';
import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView'; import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView';
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import type { View } from '@/ui/view-bar/types/View';
import { useBoardCardFields } from '../hooks/useBoardCardFields'; import { useBoardCardFields } from '../hooks/useBoardCardFields';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardColumnsState } from '../states/boardColumnsState'; import { boardColumnsState } from '../states/boardColumnsState';
import { savedBoardCardFieldsFamilyState } from '../states/savedBoardCardFieldsFamilyState';
import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector'; import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector';
import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector'; import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
@ -40,7 +42,6 @@ import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
export type BoardOptionsDropdownContentProps = { export type BoardOptionsDropdownContentProps = {
customHotkeyScope: HotkeyScope; customHotkeyScope: HotkeyScope;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void; onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
onViewsChange?: (views: View[]) => void | Promise<void>;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}; };
@ -60,10 +61,10 @@ type ColumnForCreate = {
export function BoardOptionsDropdownContent({ export function BoardOptionsDropdownContent({
customHotkeyScope, customHotkeyScope,
onStageAdd, onStageAdd,
onViewsChange,
scopeContext, scopeContext,
}: BoardOptionsDropdownContentProps) { }: BoardOptionsDropdownContentProps) {
const theme = useTheme(); const theme = useTheme();
const scopeId = useContextScopeId(scopeContext);
const stageInputRef = useRef<HTMLInputElement>(null); const stageInputRef = useRef<HTMLInputElement>(null);
const viewEditInputRef = useRef<HTMLInputElement>(null); const viewEditInputRef = useRef<HTMLInputElement>(null);
@ -106,15 +107,24 @@ export function BoardOptionsDropdownContent({
onStageAdd?.(columnToCreate); onStageAdd?.(columnToCreate);
}; };
const { upsertView } = useUpsertView({ const { upsertView } = useUpsertView({ scopeContext });
onViewsChange,
scopeContext,
});
const handleViewNameSubmit = async () => { const handleViewNameSubmit = useRecoilCallback(
const name = viewEditInputRef.current?.value; ({ set, snapshot }) =>
await upsertView(name); 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); const resetMenu = () => setCurrentMenu(undefined);

View File

@ -6,10 +6,7 @@ import { useRecoilState } from 'recoil';
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { import { BoardHeader } from '@/ui/board/components/BoardHeader';
BoardHeader,
BoardHeaderProps,
} from '@/ui/board/components/BoardHeader';
import { StyledBoard } from '@/ui/board/components/StyledBoard'; import { StyledBoard } from '@/ui/board/components/StyledBoard';
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext'; import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
@ -39,10 +36,7 @@ export type EntityBoardProps = {
onColumnDelete?: (boardColumnId: string) => void; onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void; onEditColumnTitle: (columnId: string, title: string, color: string) => void;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
} & Pick< };
BoardHeaderProps,
'defaultViewName' | 'onViewsChange' | 'onViewSubmit'
>;
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
display: flex; display: flex;
@ -57,12 +51,9 @@ const StyledBoardHeader = styled(BoardHeader)`
export function EntityBoard({ export function EntityBoard({
boardOptions, boardOptions,
defaultViewName,
onColumnAdd, onColumnAdd,
onColumnDelete, onColumnDelete,
onEditColumnTitle, onEditColumnTitle,
onViewsChange,
onViewSubmit,
scopeContext, scopeContext,
}: EntityBoardProps) { }: EntityBoardProps) {
const [boardColumns] = useRecoilState(boardColumnsState); const [boardColumns] = useRecoilState(boardColumnsState);
@ -139,13 +130,7 @@ export function EntityBoard({
return (boardColumns?.length ?? 0) > 0 ? ( return (boardColumns?.length ?? 0) > 0 ? (
<StyledWrapper> <StyledWrapper>
<StyledBoardHeader <StyledBoardHeader onStageAdd={onColumnAdd} scopeContext={scopeContext} />
defaultViewName={defaultViewName}
onStageAdd={onColumnAdd}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
scopeContext={scopeContext}
/>
<ScrollWrapper> <ScrollWrapper>
<StyledBoard ref={boardRef}> <StyledBoard ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>

View File

@ -0,0 +1,14 @@
import { atomFamily } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
export const savedBoardCardFieldsFamilyState = atomFamily<
ViewFieldDefinition<ViewFieldMetadata>[],
string | undefined
>({
key: 'savedBoardCardFieldsFamilyState',
default: [],
});

View File

@ -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)),
),
});

View File

@ -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<string, ViewFieldDefinition<ViewFieldMetadata>>
>((result, field) => ({ ...result, [field.key]: field }), {}),
});

View File

@ -87,18 +87,9 @@ const StyledTableContainer = styled.div`
type OwnProps = { type OwnProps = {
updateEntityMutation: any; updateEntityMutation: any;
} & Pick< } & Pick<TableHeaderProps, 'onImport'>;
TableHeaderProps,
'defaultViewName' | 'onImport' | 'onViewsChange' | 'onViewSubmit'
>;
export function EntityTable({ export function EntityTable({ onImport, updateEntityMutation }: OwnProps) {
defaultViewName,
onImport,
onViewsChange,
onViewSubmit,
updateEntityMutation,
}: OwnProps) {
const tableBodyRef = useRef<HTMLDivElement>(null); const tableBodyRef = useRef<HTMLDivElement>(null);
const setRowSelectedState = useSetRowSelectedState(); const setRowSelectedState = useSetRowSelectedState();
@ -135,12 +126,7 @@ export function EntityTable({
<EntityUpdateMutationContext.Provider value={updateEntityMutation}> <EntityUpdateMutationContext.Provider value={updateEntityMutation}>
<StyledTableWithHeader> <StyledTableWithHeader>
<StyledTableContainer ref={tableBodyRef}> <StyledTableContainer ref={tableBodyRef}>
<TableHeader <TableHeader onImport={onImport} />
defaultViewName={defaultViewName}
onImport={onImport}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
/>
<ScrollWrapper> <ScrollWrapper>
<div> <div>
<StyledTable className="entity-table-cell"> <StyledTable className="entity-table-cell">

View File

@ -1,6 +1,5 @@
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import type { View } from '@/ui/view-bar/types/View';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
@ -8,13 +7,11 @@ import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent'; import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
type TableOptionsDropdownProps = { type TableOptionsDropdownProps = {
onViewsChange?: (views: View[]) => void;
onImport?: () => void; onImport?: () => void;
customHotkeyScope: HotkeyScope; customHotkeyScope: HotkeyScope;
}; };
export function TableOptionsDropdown({ export function TableOptionsDropdown({
onViewsChange,
onImport, onImport,
customHotkeyScope, customHotkeyScope,
}: TableOptionsDropdownProps) { }: TableOptionsDropdownProps) {
@ -23,12 +20,7 @@ export function TableOptionsDropdown({
buttonComponents={<TableOptionsDropdownButton />} buttonComponents={<TableOptionsDropdownButton />}
dropdownHotkeyScope={customHotkeyScope} dropdownHotkeyScope={customHotkeyScope}
dropdownId={TableOptionsDropdownId} dropdownId={TableOptionsDropdownId}
dropdownComponents={ dropdownComponents={<TableOptionsDropdownContent onImport={onImport} />}
<TableOptionsDropdownContent
onImport={onImport}
onViewsChange={onViewsChange}
/>
}
/> />
); );
} }

View File

@ -1,5 +1,5 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; 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 { IconChevronLeft, IconFileImport, IconTag } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; 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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection'; import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection';
import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView'; import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView';
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import type { View } from '@/ui/view-bar/types/View';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
import { useTableColumns } from '../../hooks/useTableColumns'; import { useTableColumns } from '../../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector'; import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector';
import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector'; import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
type TableOptionsDropdownButtonProps = { type TableOptionsDropdownButtonProps = {
onViewsChange?: (views: View[]) => void | Promise<void>;
onImport?: () => void; onImport?: () => void;
}; };
type TableOptionsMenu = 'fields'; type TableOptionsMenu = 'fields';
export function TableOptionsDropdownContent({ export function TableOptionsDropdownContent({
onViewsChange,
onImport, onImport,
}: TableOptionsDropdownButtonProps) { }: TableOptionsDropdownButtonProps) {
const scopeId = useContextScopeId(TableRecoilScopeContext);
const { closeDropdownButton } = useDropdownButton({ const { closeDropdownButton } = useDropdownButton({
dropdownId: TableOptionsDropdownId, dropdownId: TableOptionsDropdownId,
}); });
@ -60,17 +62,28 @@ export function TableOptionsDropdownContent({
TableRecoilScopeContext, TableRecoilScopeContext,
); );
const { handleColumnVisibilityChange } = useTableColumns();
const { upsertView } = useUpsertView({ const { upsertView } = useUpsertView({
onViewsChange,
scopeContext: TableRecoilScopeContext, 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 () => { if (view && isCreateMode) {
const name = viewEditInputRef.current?.value; set(savedTableColumnsFamilyState(view.id), tableColumns);
await upsertView(name); }
}; },
[scopeId, upsertView, viewEditMode.mode],
);
const handleSelectMenu = (option: TableOptionsMenu) => { const handleSelectMenu = (option: TableOptionsMenu) => {
handleViewNameSubmit(); handleViewNameSubmit();

View File

@ -1,3 +1,4 @@
import { useContext } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; 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 { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { 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 { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
@ -18,14 +20,11 @@ import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
export type TableHeaderProps = { export type TableHeaderProps = {
onImport?: () => void; onImport?: () => void;
} & Pick<ViewBarProps, 'defaultViewName' | 'onViewsChange' | 'onViewSubmit'>; };
export function TableHeader({ export function TableHeader({ onImport }: TableHeaderProps) {
onImport, const { onCurrentViewSubmit, ...viewBarContextProps } =
onViewsChange, useContext(ViewBarContext);
onViewSubmit,
...props
}: TableHeaderProps) {
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue( const currentViewId = useRecoilScopedValue(
@ -43,9 +42,7 @@ export function TableHeader({
savedTableColumnsFamilyState(currentViewId), savedTableColumnsFamilyState(currentViewId),
); );
function handleViewBarReset() { const handleViewBarReset = () => setTableColumns(savedTableColumns);
setTableColumns(savedTableColumns);
}
const handleViewSelect = useRecoilCallback( const handleViewSelect = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
@ -58,32 +55,36 @@ export function TableHeader({
[tableScopeId], [tableScopeId],
); );
async function handleViewSubmit() { async function handleCurrentViewSubmit() {
if (canPersistTableColumns) { if (canPersistTableColumns) {
setSavedTableColumns(tableColumns); setSavedTableColumns(tableColumns);
} }
await onViewSubmit?.(); await onCurrentViewSubmit?.();
} }
return ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<ViewBar <ViewBarContext.Provider
{...props} value={{
canPersistViewFields={canPersistTableColumns} ...viewBarContextProps,
onReset={handleViewBarReset} canPersistViewFields: canPersistTableColumns,
onViewSelect={handleViewSelect} onCurrentViewSubmit: handleCurrentViewSubmit,
onViewSubmit={handleViewSubmit} onViewBarReset: handleViewBarReset,
optionsDropdownButton={ onViewSelect: handleViewSelect,
<TableOptionsDropdown }}
onImport={onImport} >
onViewsChange={onViewsChange} <ViewBar
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }} optionsDropdownButton={
/> <TableOptionsDropdown
} onImport={onImport}
optionsDropdownKey={TableOptionsDropdownId} customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
scopeContext={TableRecoilScopeContext} />
/> }
optionsDropdownKey={TableOptionsDropdownId}
scopeContext={TableRecoilScopeContext}
/>
</ViewBarContext.Provider>
</RecoilScope> </RecoilScope>
); );
} }

View File

@ -1,7 +1,8 @@
import type { ComponentProps, ReactNode } from 'react'; import type { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
type OwnProps = ComponentProps<'div'> & { type OwnProps = {
className?: string;
leftComponent?: ReactNode; leftComponent?: ReactNode;
rightComponent?: ReactNode; rightComponent?: ReactNode;
bottomComponent?: ReactNode; bottomComponent?: ReactNode;
@ -40,14 +41,14 @@ const StyledRightSection = styled.div`
`; `;
export function TopBar({ export function TopBar({
className,
leftComponent, leftComponent,
rightComponent, rightComponent,
bottomComponent, bottomComponent,
displayBottomBorder = true, displayBottomBorder = true,
...props
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledContainer {...props}> <StyledContainer className={className}>
<StyledTopBar displayBottomBorder={displayBottomBorder}> <StyledTopBar displayBottomBorder={displayBottomBorder}>
<StyledLeftSection>{leftComponent}</StyledLeftSection> <StyledLeftSection>{leftComponent}</StyledLeftSection>
<StyledRightSection>{rightComponent}</StyledRightSection> <StyledRightSection>{rightComponent}</StyledRightSection>

View File

@ -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 styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; 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 { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { ViewBarContext } from '../contexts/ViewBarContext';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: inline-flex; display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)}; margin-right: ${({ theme }) => theme.spacing(2)};
@ -28,22 +30,20 @@ const StyledContainer = styled.div`
`; `;
export type UpdateViewButtonGroupProps = { export type UpdateViewButtonGroupProps = {
canPersistViewFields?: boolean;
hotkeyScope: string; hotkeyScope: string;
onViewEditModeChange?: () => void; onViewEditModeChange?: () => void;
onViewSubmit?: () => void | Promise<void>;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}; };
export const UpdateViewButtonGroup = ({ export const UpdateViewButtonGroup = ({
canPersistViewFields,
hotkeyScope, hotkeyScope,
onViewEditModeChange, onViewEditModeChange,
onViewSubmit,
scopeContext, scopeContext,
}: UpdateViewButtonGroupProps) => { }: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { canPersistViewFields, onCurrentViewSubmit } =
useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext); const recoilScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue( const currentViewId = useRecoilScopedValue(
@ -89,7 +89,7 @@ export const UpdateViewButtonGroup = ({
if (canPersistFilters) setSavedFilters(filters); if (canPersistFilters) setSavedFilters(filters);
if (canPersistSorts) setSavedSorts(sorts); if (canPersistSorts) setSavedSorts(sorts);
await onViewSubmit?.(); await onCurrentViewSubmit?.();
}; };
useScopedHotkeys( useScopedHotkeys(

View File

@ -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 { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar'; import { TopBar } from '@/ui/top-bar/TopBar';
@ -8,38 +8,22 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FilterDropdownButton } from './FilterDropdownButton'; import { FilterDropdownButton } from './FilterDropdownButton';
import { SortDropdownButton } from './SortDropdownButton'; import { SortDropdownButton } from './SortDropdownButton';
import { import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
UpdateViewButtonGroup, import ViewBarDetails from './ViewBarDetails';
type UpdateViewButtonGroupProps, import { ViewsDropdownButton } from './ViewsDropdownButton';
} from './UpdateViewButtonGroup';
import ViewBarDetails, { type ViewBarDetailsProps } from './ViewBarDetails';
import {
ViewsDropdownButton,
type ViewsDropdownButtonProps,
} from './ViewsDropdownButton';
export type ViewBarProps = ComponentProps<'div'> & { export type ViewBarProps = {
className?: string;
optionsDropdownButton: ReactNode; optionsDropdownButton: ReactNode;
optionsDropdownKey: string; optionsDropdownKey: string;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
} & Pick< };
ViewsDropdownButtonProps,
'defaultViewName' | 'onViewsChange' | 'onViewSelect'
> &
Pick<ViewBarDetailsProps, 'canPersistViewFields' | 'onReset'> &
Pick<UpdateViewButtonGroupProps, 'onViewSubmit'>;
export const ViewBar = ({ export const ViewBar = ({
canPersistViewFields, className,
defaultViewName,
onReset,
onViewsChange,
onViewSelect,
onViewSubmit,
optionsDropdownButton, optionsDropdownButton,
optionsDropdownKey, optionsDropdownKey,
scopeContext, scopeContext,
...props
}: ViewBarProps) => { }: ViewBarProps) => {
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({ const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
dropdownId: optionsDropdownKey, dropdownId: optionsDropdownKey,
@ -47,13 +31,10 @@ export const ViewBar = ({
return ( return (
<TopBar <TopBar
{...props} className={className}
leftComponent={ leftComponent={
<ViewsDropdownButton <ViewsDropdownButton
defaultViewName={defaultViewName}
onViewEditModeChange={openOptionsDropdownButton} onViewEditModeChange={openOptionsDropdownButton}
onViewsChange={onViewsChange}
onViewSelect={onViewSelect}
hotkeyScope={ViewsHotkeyScope.ListDropdown} hotkeyScope={ViewsHotkeyScope.ListDropdown}
scopeContext={scopeContext} scopeContext={scopeContext}
/> />
@ -75,15 +56,11 @@ export const ViewBar = ({
} }
bottomComponent={ bottomComponent={
<ViewBarDetails <ViewBarDetails
canPersistViewFields={canPersistViewFields}
context={scopeContext} context={scopeContext}
hasFilterButton hasFilterButton
onReset={onReset}
rightComponent={ rightComponent={
<UpdateViewButtonGroup <UpdateViewButtonGroup
canPersistViewFields={canPersistViewFields}
onViewEditModeChange={openOptionsDropdownButton} onViewEditModeChange={openOptionsDropdownButton}
onViewSubmit={onViewSubmit}
hotkeyScope={ViewsHotkeyScope.CreateDropdown} hotkeyScope={ViewsHotkeyScope.CreateDropdown}
scopeContext={scopeContext} scopeContext={scopeContext}
/> />

View File

@ -1,4 +1,4 @@
import type { Context, ReactNode } from 'react'; import { type Context, type ReactNode, useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; 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 { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { useRemoveFilter } from '../hooks/useRemoveFilter'; import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
@ -23,10 +24,8 @@ import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton';
import SortOrFilterChip from './SortOrFilterChip'; import SortOrFilterChip from './SortOrFilterChip';
export type ViewBarDetailsProps = { export type ViewBarDetailsProps = {
canPersistViewFields?: boolean;
context: Context<string | null>; context: Context<string | null>;
hasFilterButton?: boolean; hasFilterButton?: boolean;
onReset?: () => void;
rightComponent?: ReactNode; rightComponent?: ReactNode;
}; };
@ -99,12 +98,11 @@ const StyledAddFilterContainer = styled.div`
`; `;
function ViewBarDetails({ function ViewBarDetails({
canPersistViewFields,
context, context,
hasFilterButton = false, hasFilterButton = false,
onReset,
rightComponent, rightComponent,
}: ViewBarDetailsProps) { }: ViewBarDetailsProps) {
const { canPersistViewFields, onViewBarReset } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(context); const recoilScopeId = useContextScopeId(context);
const currentViewId = useRecoilScopedValue(currentViewIdScopedState, context); const currentViewId = useRecoilScopedValue(currentViewIdScopedState, context);
@ -155,7 +153,7 @@ function ViewBarDetails({
const removeFilter = useRemoveFilter(context); const removeFilter = useRemoveFilter(context);
function handleCancelClick() { function handleCancelClick() {
onReset?.(); onViewBarReset?.();
setFilters(savedFilters); setFilters(savedFilters);
setSorts(savedSorts); setSorts(savedSorts);
} }

View File

@ -2,6 +2,7 @@ import {
type Context, type Context,
type MouseEvent, type MouseEvent,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useState, useState,
} from 'react'; } from 'react';
@ -22,7 +23,6 @@ import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; 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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import DropdownButton from '@/ui/view-bar/components/DropdownButton'; import DropdownButton from '@/ui/view-bar/components/DropdownButton';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; 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 { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { viewsScopedState } from '@/ui/view-bar/states/viewsScopedState'; 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 { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { useRemoveView } from '../hooks/useRemoveView';
const StyledBoldDropdownMenuItemsContainer = styled( const StyledBoldDropdownMenuItemsContainer = styled(
StyledDropdownMenuItemsContainer, StyledDropdownMenuItemsContainer,
)` )`
@ -71,39 +73,28 @@ const StyledViewName = styled.span`
`; `;
export type ViewsDropdownButtonProps = { export type ViewsDropdownButtonProps = {
defaultViewName: string;
hotkeyScope: ViewsHotkeyScope; hotkeyScope: ViewsHotkeyScope;
onViewEditModeChange?: () => void; onViewEditModeChange?: () => void;
onViewsChange?: (views: View[]) => void | Promise<void>;
onViewSelect?: (viewId: string) => void | Promise<void>;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}; };
export const ViewsDropdownButton = ({ export const ViewsDropdownButton = ({
defaultViewName,
hotkeyScope, hotkeyScope,
onViewEditModeChange, onViewEditModeChange,
onViewsChange,
onViewSelect,
scopeContext, scopeContext,
}: ViewsDropdownButtonProps) => { }: ViewsDropdownButtonProps) => {
const theme = useTheme(); const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const { defaultViewName, onViewSelect } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext); const recoilScopeId = useContextScopeId(scopeContext);
const [, setCurrentViewId] = useRecoilScopedState( const [isUnfolded, setIsUnfolded] = useState(false);
currentViewIdScopedState,
scopeContext,
);
const currentView = useRecoilScopedValue( const currentView = useRecoilScopedValue(
currentViewScopedSelector, currentViewScopedSelector,
scopeContext, scopeContext,
); );
const [views, setViews] = useRecoilScopedState( const views = useRecoilScopedValue(viewsScopedState, scopeContext);
viewsScopedState,
scopeContext,
);
const setViewEditMode = useSetRecoilState(viewEditModeState); const setViewEditMode = useSetRecoilState(viewEditModeState);
const { const {
@ -146,20 +137,17 @@ export const ViewsDropdownButton = ({
[setViewEditMode, onViewEditModeChange], [setViewEditMode, onViewEditModeChange],
); );
const handleDeleteViewButtonClick = useCallback( const { removeView } = useRemoveView({ scopeContext });
async (event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation();
if (currentView?.id === viewId) setCurrentViewId(undefined); const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
const nextViews = views.filter((view) => view.id !== viewId); await removeView(viewId);
setIsUnfolded(false);
setViews(nextViews); };
await onViewsChange?.(nextViews);
setIsUnfolded(false);
},
[currentView?.id, onViewsChange, setCurrentViewId, setViews, views],
);
useEffect(() => { useEffect(() => {
isUnfolded isUnfolded

View File

@ -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<void>;
onViewBarReset?: () => void;
onViewCreate?: (view: View) => void | Promise<void>;
onViewEdit?: (view: View) => void | Promise<void>;
onViewRemove?: (viewId: string) => void | Promise<void>;
onViewSelect?: (viewId: string) => void | Promise<void>;
}>({});

View File

@ -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<string | null>;
}) => {
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 };
};

View File

@ -1,37 +1,30 @@
import { Context, useCallback } from 'react'; import { type Context, useCallback, useContext } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState } from 'recoil';
import { v4 } from 'uuid'; 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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewBarContext } from '../contexts/ViewBarContext';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { filtersScopedState } from '../states/filtersScopedState'; import { filtersScopedState } from '../states/filtersScopedState';
import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState'; import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState';
import { savedSortsFamilyState } from '../states/savedSortsFamilyState'; import { savedSortsFamilyState } from '../states/savedSortsFamilyState';
import { viewsByIdScopedSelector } from '../states/selectors/viewsByIdScopedSelector';
import { sortsScopedState } from '../states/sortsScopedState'; import { sortsScopedState } from '../states/sortsScopedState';
import { viewEditModeState } from '../states/viewEditModeState'; import { viewEditModeState } from '../states/viewEditModeState';
import { viewsScopedState } from '../states/viewsScopedState'; import { viewsScopedState } from '../states/viewsScopedState';
import type { View } from '../types/View';
export const useUpsertView = ({ export const useUpsertView = ({
onViewsChange,
scopeContext, scopeContext,
}: { }: {
onViewsChange?: (views: View[]) => void | Promise<void>;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}) => { }) => {
const { onViewCreate, onViewEdit } = useContext(ViewBarContext);
const recoilScopeId = useContextScopeId(scopeContext);
const filters = useRecoilScopedValue(filtersScopedState, scopeContext); const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
const sorts = useRecoilScopedValue(sortsScopedState, scopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
const [, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState,
scopeContext,
);
const [views, setViews] = useRecoilScopedState(
viewsScopedState,
scopeContext,
);
const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState); const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState);
const resetViewEditMode = useCallback( const resetViewEditMode = useCallback(
@ -40,44 +33,63 @@ export const useUpsertView = ({
); );
const upsertView = useRecoilCallback( const upsertView = useRecoilCallback(
({ set }) => ({ set, snapshot }) =>
async (name?: string) => { async (name?: string) => {
if (!viewEditMode.mode || !name) return resetViewEditMode(); if (!viewEditMode.mode || !name) {
resetViewEditMode();
return;
}
if (viewEditMode.mode === 'create') { if (viewEditMode.mode === 'create') {
const viewToCreate = { id: v4(), name }; const createdView = { id: v4(), name };
const nextViews = [...views, viewToCreate];
set(savedFiltersFamilyState(viewToCreate.id), filters); set(savedFiltersFamilyState(createdView.id), filters);
set(savedSortsFamilyState(viewToCreate.id), sorts); set(savedSortsFamilyState(createdView.id), sorts);
setViews(nextViews); set(viewsScopedState(recoilScopeId), (previousViews) => [
await onViewsChange?.(nextViews); ...previousViews,
createdView,
]);
setCurrentViewId(viewToCreate.id); await onViewCreate?.(createdView);
resetViewEditMode();
set(currentViewIdScopedState(recoilScopeId), createdView.id);
return createdView;
} }
if (viewEditMode.mode === 'edit') { if (viewEditMode.mode === 'edit' && viewEditMode.viewId) {
const nextViews = views.map((view) => const viewsById = await snapshot.getPromise(
view.id === viewEditMode.viewId ? { ...view, name } : view, viewsByIdScopedSelector(recoilScopeId),
);
const editedView = { ...viewsById[viewEditMode.viewId], name };
set(viewsScopedState(recoilScopeId), (previousViews) =>
previousViews.map((previousView) =>
previousView.id === viewEditMode.viewId
? editedView
: previousView,
),
); );
setViews(nextViews); await onViewEdit?.(editedView);
await onViewsChange?.(nextViews);
}
return resetViewEditMode(); resetViewEditMode();
return editedView;
}
}, },
[ [
filters, filters,
onViewsChange, onViewCreate,
onViewEdit,
recoilScopeId,
resetViewEditMode, resetViewEditMode,
setCurrentViewId,
setViews,
sorts, sorts,
viewEditMode.mode, viewEditMode.mode,
viewEditMode.viewId, viewEditMode.viewId,
views,
], ],
); );

View File

@ -1,7 +1,10 @@
import { type Context } from 'react'; import { type Context } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { availableBoardCardFieldsScopedState } from '@/ui/board/states/availableBoardCardFieldsScopedState'; import { availableBoardCardFieldsScopedState } from '@/ui/board/states/availableBoardCardFieldsScopedState';
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState'; import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import { savedBoardCardFieldsFamilyState } from '@/ui/board/states/savedBoardCardFieldsFamilyState';
import { savedBoardCardFieldsByKeyFamilySelector } from '@/ui/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector';
import type { import type {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
@ -13,6 +16,7 @@ import {
SortOrder, SortOrder,
useCreateViewFieldsMutation, useCreateViewFieldsMutation,
useGetViewFieldsQuery, useGetViewFieldsQuery,
useUpdateViewFieldMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -49,8 +53,15 @@ export const useBoardViewFields = ({
boardCardFieldsScopedState, boardCardFieldsScopedState,
scopeContext, scopeContext,
); );
const setSavedBoardCardFields = useSetRecoilState(
savedBoardCardFieldsFamilyState(currentViewId),
);
const savedBoardCardFieldsByKey = useRecoilValue(
savedBoardCardFieldsByKeyFamilySelector(currentViewId),
);
const [createViewFieldsMutation] = useCreateViewFieldsMutation(); const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const createViewFields = ( const createViewFields = (
fields: ViewFieldDefinition<ViewFieldMetadata>[], fields: ViewFieldDefinition<ViewFieldMetadata>[],
@ -68,6 +79,27 @@ export const useBoardViewFields = ({
}); });
}; };
const updateViewFields = (
fields: ViewFieldDefinition<ViewFieldMetadata>[],
) => {
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({ const { refetch } = useGetViewFieldsQuery({
skip: !currentViewId || skipFetch, skip: !currentViewId || skipFetch,
variables: { variables: {
@ -102,6 +134,7 @@ export const useBoardViewFields = ({
.filter<ViewFieldDefinition<ViewFieldMetadata>>(assertNotNull); .filter<ViewFieldDefinition<ViewFieldMetadata>>(assertNotNull);
if (!isDeeplyEqual(boardCardFields, nextFields)) { if (!isDeeplyEqual(boardCardFields, nextFields)) {
setSavedBoardCardFields(nextFields);
setBoardCardFields(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 };
}; };

View File

@ -1,5 +1,6 @@
import type { Context } from 'react'; import type { Context } from 'react';
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import type { import type {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
@ -23,17 +24,21 @@ export const useBoardViews = ({
objectId: 'company'; objectId: 'company';
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}) => { }) => {
const boardCardFields = useRecoilScopedValue(
boardCardFieldsScopedState,
scopeContext,
);
const filters = useRecoilScopedValue(filtersScopedState, scopeContext); const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
const sorts = useRecoilScopedValue(sortsScopedState, scopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
const { handleViewsChange, isFetchingViews } = useViews({ const { createView, deleteView, isFetchingViews, updateView } = useViews({
objectId, objectId,
onViewCreate: handleViewCreate, onViewCreate: handleViewCreate,
type: ViewType.Pipeline, type: ViewType.Pipeline,
scopeContext, scopeContext,
}); });
useBoardViewFields({ const { createViewFields, persistCardFields } = useBoardViewFields({
objectId, objectId,
fieldDefinitions, fieldDefinitions,
scopeContext, scopeContext,
@ -51,14 +56,16 @@ export const useBoardViews = ({
}); });
async function handleViewCreate(viewId: string) { async function handleViewCreate(viewId: string) {
await createViewFields(boardCardFields, viewId);
await createViewFilters(filters, viewId); await createViewFilters(filters, viewId);
await createViewSorts(sorts, viewId); await createViewSorts(sorts, viewId);
} }
const handleViewSubmit = async () => { const submitCurrentView = async () => {
await persistCardFields();
await persistFilters(); await persistFilters();
await persistSorts(); await persistSorts();
}; };
return { handleViewsChange, handleViewSubmit }; return { createView, deleteView, submitCurrentView, updateView };
}; };

View File

@ -29,7 +29,7 @@ export const useTableViews = ({
); );
const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext); const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext);
const { handleViewsChange, isFetchingViews } = useViews({ const { createView, deleteView, isFetchingViews, updateView } = useViews({
objectId, objectId,
onViewCreate: handleViewCreate, onViewCreate: handleViewCreate,
type: ViewType.Table, type: ViewType.Table,
@ -55,11 +55,11 @@ export const useTableViews = ({
await createViewSorts(sorts, viewId); await createViewSorts(sorts, viewId);
} }
const handleViewSubmit = async () => { const submitCurrentView = async () => {
await persistColumns(); await persistColumns();
await persistFilters(); await persistFilters();
await persistSorts(); await persistSorts();
}; };
return { handleViewsChange, handleViewSubmit }; return { createView, deleteView, submitCurrentView, updateView };
}; };

View File

@ -1,13 +1,13 @@
import type { Context } from 'react'; import type { Context } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { savedBoardCardFieldsFamilyState } from '@/ui/board/states/savedBoardCardFieldsFamilyState';
import { savedTableColumnsFamilyState } from '@/ui/table/states/savedTableColumnsFamilyState'; import { savedTableColumnsFamilyState } from '@/ui/table/states/savedTableColumnsFamilyState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState'; import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState';
import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState'; 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 { viewsScopedState } from '@/ui/view-bar/states/viewsScopedState';
import type { View } from '@/ui/view-bar/types/View'; import type { View } from '@/ui/view-bar/types/View';
import { import {
@ -19,6 +19,8 @@ import {
} from '~/generated/graphql'; } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { GET_VIEWS } from '../graphql/queries/getViews';
export const useViews = ({ export const useViews = ({
objectId, objectId,
onViewCreate, onViewCreate,
@ -38,7 +40,6 @@ export const useViews = ({
viewsScopedState, viewsScopedState,
scopeContext, scopeContext,
); );
const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext);
const [createViewMutation] = useCreateViewMutation(); const [createViewMutation] = useCreateViewMutation();
const [updateViewMutation] = useUpdateViewMutation(); const [updateViewMutation] = useUpdateViewMutation();
@ -53,26 +54,34 @@ export const useViews = ({
type, type,
}, },
}, },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
}); });
if (data?.view) await onViewCreate?.(data.view.id); if (data?.view) await onViewCreate?.(data.view.id);
}; };
const updateView = (view: View) => const updateView = async (view: View) => {
updateViewMutation({ await updateViewMutation({
variables: { variables: {
data: { name: view.name }, data: { name: view.name },
where: { id: view.id }, where: { id: view.id },
}, },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
}); });
};
const deleteView = (viewId: string) => const deleteView = async (viewId: string) => {
deleteViewMutation({ variables: { where: { id: viewId } } }); await deleteViewMutation({
variables: { where: { id: viewId } },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
};
const handleResetSavedViews = useRecoilCallback( const handleResetSavedViews = useRecoilCallback(
({ reset }) => ({ reset }) =>
() => { () => {
views.forEach((view) => { views.forEach((view) => {
reset(savedBoardCardFieldsFamilyState(view.id));
reset(savedTableColumnsFamilyState(view.id)); reset(savedTableColumnsFamilyState(view.id));
reset(savedFiltersFamilyState(view.id)); reset(savedFiltersFamilyState(view.id));
reset(savedSortsFamilyState(view.id)); reset(savedSortsFamilyState(view.id));
@ -81,7 +90,7 @@ export const useViews = ({
[views], [views],
); );
const { loading, refetch } = useGetViewsQuery({ const { loading } = useGetViewsQuery({
variables: { variables: {
where: { where: {
objectId: { equals: objectId }, objectId: { equals: objectId },
@ -115,32 +124,10 @@ export const useViews = ({
}, },
}); });
const handleViewsChange = async (nextViews: View[]) => { return {
const viewToCreate = nextViews.find((nextView) => !viewsById[nextView.id]); createView,
if (viewToCreate) { deleteView,
await createView(viewToCreate); isFetchingViews: loading,
await refetch(); updateView,
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 { handleViewsChange, isFetchingViews: loading };
}; };