feat: toggle board field visibilities (#1547)

Closes #1537, Closes #1539
This commit is contained in:
Thaïs
2023-09-13 11:58:52 +02:00
committed by GitHub
parent 67f1da038d
commit 28e12d492c
31 changed files with 492 additions and 168 deletions

View File

@ -1,11 +1,16 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard'; import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext'; import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress'; import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress';
@ -16,26 +21,43 @@ const meta: Meta<typeof CompanyBoardCard> = {
title: 'Modules/Companies/CompanyBoardCard', title: 'Modules/Companies/CompanyBoardCard',
component: CompanyBoardCard, component: CompanyBoardCard,
decorators: [ decorators: [
(Story) => ( (Story, context) => {
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}> const [, setBoardCardFields] = useRecoilScopedState(
<HooksCompanyBoard /> boardCardFieldsScopedState,
<RecoilScope SpecificContext={BoardColumnRecoilScopeContext}> context.parameters.recoilScopeContext,
<BoardCardIdContext.Provider value={mockedPipelineProgressData[1].id}> );
<MemoryRouter>
<Story /> useEffect(() => {
</MemoryRouter> setBoardCardFields(pipelineAvailableFieldDefinitions);
</BoardCardIdContext.Provider> }, [setBoardCardFields]);
</RecoilScope>
</RecoilScope> return (
), <>
<HooksCompanyBoard />
<RecoilScope SpecificContext={BoardColumnRecoilScopeContext}>
<BoardCardIdContext.Provider
value={mockedPipelineProgressData[1].id}
>
<MemoryRouter>
<Story />
</MemoryRouter>
</BoardCardIdContext.Provider>
</RecoilScope>
</>
);
},
ComponentWithRecoilScopeDecorator,
ComponentDecorator, ComponentDecorator,
], ],
args: { scopeContext: CompanyBoardRecoilScopeContext },
argTypes: { scopeContext: { control: false } },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
recoilScopeContext: CompanyBoardRecoilScopeContext,
}, },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof CompanyBoardCard>; type Story = StoryObj<typeof CompanyBoardCard>;
export const CompanyCompanyBoardCard: Story = {}; export const Default: Story = {};

View File

@ -1,3 +1,4 @@
import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions';
import { import {
EntityBoard, EntityBoard,
type EntityBoardProps, type EntityBoardProps,
@ -21,6 +22,7 @@ export const CompanyBoard = ({ ...props }: CompanyBoardProps) => {
availableSorts: opportunitiesBoardOptions.sorts, availableSorts: opportunitiesBoardOptions.sorts,
objectId: 'company', objectId: 'company',
scopeContext: CompanyBoardRecoilScopeContext, scopeContext: CompanyBoardRecoilScopeContext,
fieldDefinitions: pipelineAvailableFieldDefinitions,
}); });
return ( return (

View File

@ -1,16 +1,17 @@
import { ReactNode, useContext } from 'react'; import { type Context, type ReactNode, useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected'; import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected';
import { viewFieldsDefinitionsState } from '@/ui/board/states/viewFieldsDefinitionsState'; import { visibleBoardCardFieldsScopedSelector } from '@/ui/board/states/selectors/visibleBoardCardFieldsScopedSelector';
import { EntityChipVariant } from '@/ui/chip/components/EntityChip'; import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField'; import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
import { EditableFieldDefinitionContext } from '@/ui/editable-field/contexts/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '@/ui/editable-field/contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '@/ui/editable-field/contexts/EditableFieldEntityIdContext'; import { EditableFieldEntityIdContext } from '@/ui/editable-field/contexts/EditableFieldEntityIdContext';
import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/EditableFieldMutationContext'; import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/EditableFieldMutationContext';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql'; import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils'; import { getLogoUrlFromDomainName } from '~/utils';
@ -18,6 +19,10 @@ import { companyProgressesFamilyState } from '../states/companyProgressesFamilyS
import { CompanyChip } from './CompanyChip'; import { CompanyChip } from './CompanyChip';
type OwnProps = {
scopeContext: Context<string | null>;
};
const StyledBoardCard = styled.div<{ selected: boolean }>` const StyledBoardCard = styled.div<{ selected: boolean }>`
background-color: ${({ theme, selected }) => background-color: ${({ theme, selected }) =>
selected ? theme.accent.quaternary : theme.background.secondary}; selected ? theme.accent.quaternary : theme.background.secondary};
@ -98,7 +103,7 @@ const StyledFieldContainer = styled.div`
width: 100%; width: 100%;
`; `;
export function CompanyBoardCard() { export function CompanyBoardCard({ scopeContext }: OwnProps) {
const { currentCardSelected, setCurrentCardSelected } = const { currentCardSelected, setCurrentCardSelected } =
useCurrentCardSelected(); useCurrentCardSelected();
const boardCardId = useContext(BoardCardIdContext); const boardCardId = useContext(BoardCardIdContext);
@ -108,7 +113,10 @@ export function CompanyBoardCard() {
); );
const { pipelineProgress, company } = companyProgress ?? {}; const { pipelineProgress, company } = companyProgress ?? {};
const viewFieldsDefinitions = useRecoilValue(viewFieldsDefinitionsState); const visibleBoardCardFields = useRecoilScopedValue(
visibleBoardCardFieldsScopedSelector,
scopeContext,
);
// boardCardId check can be moved to a wrapper to avoid unnecessary logic above // boardCardId check can be moved to a wrapper to avoid unnecessary logic above
if (!company || !pipelineProgress || !boardCardId) { if (!company || !pipelineProgress || !boardCardId) {
@ -157,23 +165,21 @@ export function CompanyBoardCard() {
value={useUpdateOnePipelineProgressMutation} value={useUpdateOnePipelineProgressMutation}
> >
<EditableFieldEntityIdContext.Provider value={boardCardId}> <EditableFieldEntityIdContext.Provider value={boardCardId}>
{viewFieldsDefinitions.map((viewField) => { {visibleBoardCardFields.map((viewField) => (
return ( <PreventSelectOnClickContainer key={viewField.key}>
<PreventSelectOnClickContainer key={viewField.key}> <EditableFieldDefinitionContext.Provider
<EditableFieldDefinitionContext.Provider value={{
value={{ key: viewField.key,
key: viewField.key, name: viewField.name,
name: viewField.name, Icon: viewField.Icon,
Icon: viewField.Icon, type: viewField.metadata.type,
type: viewField.metadata.type, metadata: viewField.metadata,
metadata: viewField.metadata, }}
}} >
> <GenericEditableField />
<GenericEditableField /> </EditableFieldDefinitionContext.Provider>
</EditableFieldDefinitionContext.Provider> </PreventSelectOnClickContainer>
</PreventSelectOnClickContainer> ))}
);
})}
</EditableFieldEntityIdContext.Provider> </EditableFieldEntityIdContext.Provider>
</EditableFieldMutationContext.Provider> </EditableFieldMutationContext.Provider>
</StyledBoardCardBody> </StyledBoardCardBody>

View File

@ -1,11 +1,9 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions';
import { useBoardActionBarEntries } from '@/ui/board/hooks/useBoardActionBarEntries'; import { useBoardActionBarEntries } from '@/ui/board/hooks/useBoardActionBarEntries';
import { useBoardContextMenuEntries } from '@/ui/board/hooks/useBoardContextMenuEntries'; import { useBoardContextMenuEntries } from '@/ui/board/hooks/useBoardContextMenuEntries';
import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState'; import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState';
import { viewFieldsDefinitionsState } from '@/ui/board/states/viewFieldsDefinitionsState';
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 { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState';
@ -26,9 +24,6 @@ import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext'; import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
export function HooksCompanyBoard() { export function HooksCompanyBoard() {
const setFieldsDefinitionsState = useSetRecoilState(
viewFieldsDefinitionsState,
);
const [, setAvailableFilters] = useRecoilScopedState( const [, setAvailableFilters] = useRecoilScopedState(
availableFiltersScopedState, availableFiltersScopedState,
CompanyBoardRecoilScopeContext, CompanyBoardRecoilScopeContext,
@ -36,7 +31,6 @@ export function HooksCompanyBoard() {
useEffect(() => { useEffect(() => {
setAvailableFilters(opportunitiesBoardOptions.filters); setAvailableFilters(opportunitiesBoardOptions.filters);
setFieldsDefinitionsState(pipelineAvailableFieldDefinitions);
}); });
const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState); const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState);

View File

@ -20,6 +20,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
key: 'closeDate', key: 'closeDate',
name: 'Close Date', name: 'Close Date',
Icon: IconCalendarEvent, Icon: IconCalendarEvent,
index: 0,
metadata: { metadata: {
type: 'date', type: 'date',
fieldName: 'closeDate', fieldName: 'closeDate',
@ -30,6 +31,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
key: 'amount', key: 'amount',
name: 'Amount', name: 'Amount',
Icon: IconCurrencyDollar, Icon: IconCurrencyDollar,
index: 1,
metadata: { metadata: {
type: 'number', type: 'number',
fieldName: 'amount', fieldName: 'amount',
@ -40,6 +42,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
key: 'probability', key: 'probability',
name: 'Probability', name: 'Probability',
Icon: IconProgressCheck, Icon: IconProgressCheck,
index: 2,
metadata: { metadata: {
type: 'probability', type: 'probability',
fieldName: 'probability', fieldName: 'probability',
@ -50,6 +53,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
key: 'pointOfContact', key: 'pointOfContact',
name: 'Point of Contact', name: 'Point of Contact',
Icon: IconUser, Icon: IconUser,
index: 3,
metadata: { metadata: {
type: 'relation', type: 'relation',
fieldName: 'pointOfContact', fieldName: 'pointOfContact',

View File

@ -1,4 +1,4 @@
import { type ComponentProps, useCallback } from 'react'; import type { ComponentProps } from 'react';
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';
@ -29,18 +29,6 @@ export function BoardHeader<SortField>({
availableSorts, availableSorts,
defaultViewName, defaultViewName,
}: BoardHeaderProps<SortField>) { }: BoardHeaderProps<SortField>) {
const OptionsDropdownButton = useCallback(
() => (
<BoardOptionsDropdown
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
onStageAdd={onStageAdd}
onViewsChange={onViewsChange}
scopeContext={scopeContext}
/>
),
[onStageAdd, onViewsChange, scopeContext],
);
return ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<ViewBar <ViewBar
@ -48,8 +36,15 @@ export function BoardHeader<SortField>({
defaultViewName={defaultViewName} defaultViewName={defaultViewName}
onViewsChange={onViewsChange} onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit} onViewSubmit={onViewSubmit}
optionsDropdownButton={
<BoardOptionsDropdown
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
onStageAdd={onStageAdd}
onViewsChange={onViewsChange}
scopeContext={scopeContext}
/>
}
optionsDropdownKey={BoardOptionsDropdownKey} optionsDropdownKey={BoardOptionsDropdownKey}
OptionsDropdownButton={OptionsDropdownButton}
scopeContext={scopeContext} scopeContext={scopeContext}
/> />
</RecoilScope> </RecoilScope>

View File

@ -16,6 +16,7 @@ import {
IconLayoutKanban, IconLayoutKanban,
IconPlus, IconPlus,
IconSettings, IconSettings,
IconTag,
} from '@/ui/icon'; } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate'; import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate';
@ -23,12 +24,16 @@ 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 { 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 { 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 type { View } from '@/ui/view-bar/types/View';
import { useBoardCardFields } from '../hooks/useBoardCardFields';
import { boardColumnsState } from '../states/boardColumnsState'; import { boardColumnsState } from '../states/boardColumnsState';
import { hiddenBoardCardFieldsScopedSelector } from '../states/selectors/hiddenBoardCardFieldsScopedSelector';
import { visibleBoardCardFieldsScopedSelector } from '../states/selectors/visibleBoardCardFieldsScopedSelector';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey'; import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
@ -43,10 +48,7 @@ const StyledIconSettings = styled(IconSettings)`
margin-right: ${({ theme }) => theme.spacing(1)}; margin-right: ${({ theme }) => theme.spacing(1)};
`; `;
enum BoardOptionsMenu { type BoardOptionsMenu = 'fields' | 'stage-creation' | 'stages';
StageCreation = 'StageCreation',
Stages = 'Stages',
}
type ColumnForCreate = { type ColumnForCreate = {
id: string; id: string;
@ -72,14 +74,22 @@ export function BoardOptionsDropdownContent({
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState); const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const hiddenBoardCardFields = useRecoilScopedValue(
hiddenBoardCardFieldsScopedSelector,
scopeContext,
);
const hasHiddenFields = hiddenBoardCardFields.length > 0;
const visibleBoardCardFields = useRecoilScopedValue(
visibleBoardCardFieldsScopedSelector,
scopeContext,
);
const hasVisibleFields = visibleBoardCardFields.length > 0;
const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext); const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext);
const viewEditMode = useRecoilValue(viewEditModeState); const viewEditMode = useRecoilValue(viewEditModeState);
const handleStageSubmit = () => { const handleStageSubmit = () => {
if ( if (currentMenu !== 'stage-creation' || !stageInputRef?.current?.value)
currentMenu !== BoardOptionsMenu.StageCreation ||
!stageInputRef?.current?.value
)
return; return;
const columnToCreate: ColumnForCreate = { const columnToCreate: ColumnForCreate = {
@ -113,6 +123,8 @@ export function BoardOptionsDropdownContent({
setCurrentMenu(menu); setCurrentMenu(menu);
}; };
const { handleFieldVisibilityChange } = useBoardCardFields({ scopeContext });
const { closeDropdownButton } = useDropdownButton({ const { closeDropdownButton } = useDropdownButton({
key: BoardOptionsDropdownKey, key: BoardOptionsDropdownKey,
}); });
@ -161,14 +173,19 @@ export function BoardOptionsDropdownContent({
<StyledDropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<MenuItemNavigate <MenuItemNavigate
onClick={() => handleMenuNavigate(BoardOptionsMenu.Stages)} onClick={() => handleMenuNavigate('stages')}
LeftIcon={IconLayoutKanban} LeftIcon={IconLayoutKanban}
text="Stages" text="Stages"
/> />
<MenuItemNavigate
onClick={() => handleMenuNavigate('fields')}
LeftIcon={IconTag}
text="Fields"
/>
</StyledDropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</> </>
)} )}
{currentMenu === BoardOptionsMenu.Stages && ( {currentMenu === 'stages' && (
<> <>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}> <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Stages Stages
@ -176,20 +193,45 @@ export function BoardOptionsDropdownContent({
<StyledDropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<MenuItem <MenuItem
onClick={() => setCurrentMenu(BoardOptionsMenu.StageCreation)} onClick={() => setCurrentMenu('stage-creation')}
LeftIcon={IconPlus} LeftIcon={IconPlus}
text="Add stage" text="Add stage"
/> />
</StyledDropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</> </>
)} )}
{currentMenu === BoardOptionsMenu.StageCreation && ( {currentMenu === 'stage-creation' && (
<DropdownMenuInput <DropdownMenuInput
autoFocus autoFocus
placeholder="New stage" placeholder="New stage"
ref={stageInputRef} ref={stageInputRef}
/> />
)} )}
{currentMenu === 'fields' && (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Fields
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
{hasVisibleFields && (
<ViewFieldsVisibilityDropdownSection
title="Visible"
fields={visibleBoardCardFields}
onVisibilityChange={handleFieldVisibilityChange}
/>
)}
{hasVisibleFields && hasHiddenFields && (
<StyledDropdownMenuSeparator />
)}
{hasHiddenFields && (
<ViewFieldsVisibilityDropdownSection
title="Hidden"
fields={hiddenBoardCardFields}
onVisibilityChange={handleFieldVisibilityChange}
/>
)}
</>
)}
</StyledDropdownMenu> </StyledDropdownMenu>
); );
} }

View File

@ -160,8 +160,9 @@ export function EntityBoard({
<EntityBoardColumn <EntityBoardColumn
boardOptions={boardOptions} boardOptions={boardOptions}
column={column} column={column}
onTitleEdit={onEditColumnTitle}
onDelete={onColumnDelete} onDelete={onColumnDelete}
onTitleEdit={onEditColumnTitle}
scopeContext={scopeContext}
/> />
</RecoilScope> </RecoilScope>
</BoardColumnIdContext.Provider> </BoardColumnIdContext.Provider>

View File

@ -1,3 +1,4 @@
import type { Context } from 'react';
import { Draggable } from '@hello-pangea/dnd'; import { Draggable } from '@hello-pangea/dnd';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
@ -11,10 +12,12 @@ export function EntityBoardCard({
boardOptions, boardOptions,
cardId, cardId,
index, index,
scopeContext,
}: { }: {
boardOptions: BoardOptions; boardOptions: BoardOptions;
cardId: string; cardId: string;
index: number; index: number;
scopeContext: Context<string | null>;
}) { }) {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
@ -43,7 +46,7 @@ export function EntityBoardCard({
data-select-disable data-select-disable
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
{boardOptions.cardComponent} {<boardOptions.CardComponent scopeContext={scopeContext} />}
</div> </div>
)} )}
</Draggable> </Draggable>

View File

@ -1,4 +1,4 @@
import { useContext } from 'react'; import { type Context, useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd'; import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -48,15 +48,17 @@ const BoardColumnCardsContainer = ({
}; };
export function EntityBoardColumn({ export function EntityBoardColumn({
column,
boardOptions, boardOptions,
column,
onDelete, onDelete,
onTitleEdit, onTitleEdit,
scopeContext,
}: { }: {
column: BoardColumnDefinition;
boardOptions: BoardOptions; boardOptions: BoardOptions;
column: BoardColumnDefinition;
onDelete?: (columnId: string) => void; onDelete?: (columnId: string) => void;
onTitleEdit: (columnId: string, title: string, color: string) => void; onTitleEdit: (columnId: string, title: string, color: string) => void;
scopeContext: Context<string | null>;
}) { }) {
const boardColumnId = useContext(BoardColumnIdContext) ?? ''; const boardColumnId = useContext(BoardColumnIdContext) ?? '';
@ -92,6 +94,7 @@ export function EntityBoardColumn({
index={index} index={index}
cardId={cardId} cardId={cardId}
boardOptions={boardOptions} boardOptions={boardOptions}
scopeContext={scopeContext}
/> />
</BoardCardIdContext.Provider> </BoardCardIdContext.Provider>
))} ))}

View File

@ -0,0 +1,42 @@
import type { Context } from 'react';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardCardFieldsByKeyScopedSelector } from '../states/selectors/boardCardFieldsByKeyScopedSelector';
export const useBoardCardFields = ({
scopeContext,
}: {
scopeContext: Context<string | null>;
}) => {
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
scopeContext,
);
const boardCardFieldsByKey = useRecoilScopedValue(
boardCardFieldsByKeyScopedSelector,
scopeContext,
);
const handleFieldVisibilityChange = (
field: ViewFieldDefinition<ViewFieldMetadata>,
) => {
const nextFields = boardCardFieldsByKey[field.key]
? boardCardFields.map((previousField) =>
previousField.key === field.key
? { ...previousField, isVisible: !field.isVisible }
: previousField,
)
: [...boardCardFields, { ...field, isVisible: true }];
setBoardCardFields(nextFields);
};
return { handleFieldVisibilityChange };
};

View File

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

View File

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

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
export const boardCardFieldsByKeyScopedSelector = selectorFamily({
key: 'boardCardFieldsByKeyScopedSelector',
get:
(scopeId: string) =>
({ get }) =>
get(boardCardFieldsScopedState(scopeId)).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
>((result, field) => ({ ...result, [field.key]: field }), {}),
});

View File

@ -0,0 +1,22 @@
import { selectorFamily } from 'recoil';
import { availableBoardCardFieldsScopedState } from '../availableBoardCardFieldsScopedState';
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
export const hiddenBoardCardFieldsScopedSelector = selectorFamily({
key: 'hiddenBoardCardFieldsScopedSelector',
get:
(scopeId: string) =>
({ get }) => {
const fields = get(boardCardFieldsScopedState(scopeId));
const fieldKeys = fields.map(({ key }) => key);
const otherAvailableKeys = get(
availableBoardCardFieldsScopedState(scopeId),
).filter(({ key }) => !fieldKeys.includes(key));
return [
...fields.filter((field) => !field.isVisible),
...otherAvailableKeys,
];
},
});

View File

@ -0,0 +1,13 @@
import { selectorFamily } from 'recoil';
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
export const visibleBoardCardFieldsScopedSelector = selectorFamily({
key: 'visibleBoardCardFieldsScopedSelector',
get:
(scopeId: string) =>
({ get }) =>
get(boardCardFieldsScopedState(scopeId)).filter(
(field) => field.isVisible,
),
});

View File

@ -1,13 +0,0 @@
import { atom } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const viewFieldsDefinitionsState = atom<
ViewFieldDefinition<ViewFieldMetadata>[]
>({
key: 'viewFieldsDefinitionState',
default: [],
});

View File

@ -1,3 +1,5 @@
import type { ComponentType, Context } from 'react';
import { FilterDefinitionByEntity } from '@/ui/view-bar/types/FilterDefinitionByEntity'; import { FilterDefinitionByEntity } from '@/ui/view-bar/types/FilterDefinitionByEntity';
import { SortType } from '@/ui/view-bar/types/interface'; import { SortType } from '@/ui/view-bar/types/interface';
import { PipelineProgress } from '~/generated/graphql'; import { PipelineProgress } from '~/generated/graphql';
@ -5,7 +7,7 @@ import { PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By
export type BoardOptions = { export type BoardOptions = {
newCardComponent: React.ReactNode; newCardComponent: React.ReactNode;
cardComponent: React.ReactNode; CardComponent: ComponentType<{ scopeContext: Context<string | null> }>;
filters: FilterDefinitionByEntity<PipelineProgress>[]; filters: FilterDefinitionByEntity<PipelineProgress>[];
sorts: Array<SortType<PipelineProgresses_Order_By>>; sorts: Array<SortType<PipelineProgresses_Order_By>>;
}; };

View File

@ -49,6 +49,7 @@ export function FloatingIconButtonGroup({
applyBlur={false} applyBlur={false}
applyShadow={false} applyShadow={false}
Icon={Icon} Icon={Icon}
key={index}
onClick={onClick} onClick={onClick}
position={position} position={position}
size={size} size={size}

View File

@ -117,11 +117,12 @@ export type ViewFieldMetadata = { type: ViewFieldType } & (
); );
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = { export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
key: string;
name: string;
Icon?: IconComponent; Icon?: IconComponent;
index: number;
isVisible?: boolean; isVisible?: boolean;
key: string;
metadata: T; metadata: T;
name: string;
}; };
export type ViewFieldTextValue = string; export type ViewFieldTextValue = string;

View File

@ -12,25 +12,25 @@ 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 { 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 { 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 type { View } from '@/ui/view-bar/types/View';
import { useTableColumns } from '../../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
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 { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey'; import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = { type TableOptionsDropdownButtonProps = {
onViewsChange?: (views: View[]) => void | Promise<void>; onViewsChange?: (views: View[]) => void | Promise<void>;
onImport?: () => void; onImport?: () => void;
}; };
type TableOptionsMenu = 'properties'; type TableOptionsMenu = 'fields';
export function TableOptionsDropdownContent({ export function TableOptionsDropdownContent({
onViewsChange, onViewsChange,
@ -40,9 +40,9 @@ export function TableOptionsDropdownContent({
key: TableOptionsDropdownKey, key: TableOptionsDropdownKey,
}); });
const [selectedMenu, setSelectedMenu] = useState< const [currentMenu, setCurrentMenu] = useState<TableOptionsMenu | undefined>(
TableOptionsMenu | undefined undefined,
>(undefined); );
const viewEditInputRef = useRef<HTMLInputElement>(null); const viewEditInputRef = useRef<HTMLInputElement>(null);
@ -65,6 +65,8 @@ export function TableOptionsDropdownContent({
scopeContext: TableRecoilScopeContext, scopeContext: TableRecoilScopeContext,
}); });
const { handleColumnVisibilityChange } = useTableColumns();
const handleViewNameSubmit = async () => { const handleViewNameSubmit = async () => {
const name = viewEditInputRef.current?.value; const name = viewEditInputRef.current?.value;
await upsertView(name); await upsertView(name);
@ -72,10 +74,10 @@ export function TableOptionsDropdownContent({
const handleSelectMenu = (option: TableOptionsMenu) => { const handleSelectMenu = (option: TableOptionsMenu) => {
handleViewNameSubmit(); handleViewNameSubmit();
setSelectedMenu(option); setCurrentMenu(option);
}; };
const resetMenu = () => setSelectedMenu(undefined); const resetMenu = () => setCurrentMenu(undefined);
useScopedHotkeys( useScopedHotkeys(
Key.Escape, Key.Escape,
@ -97,7 +99,7 @@ export function TableOptionsDropdownContent({
return ( return (
<StyledDropdownMenu> <StyledDropdownMenu>
{!selectedMenu && ( {!currentMenu && (
<> <>
{!!viewEditMode.mode ? ( {!!viewEditMode.mode ? (
<DropdownMenuInput <DropdownMenuInput
@ -118,9 +120,9 @@ export function TableOptionsDropdownContent({
<StyledDropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<MenuItem <MenuItem
onClick={() => handleSelectMenu('properties')} onClick={() => handleSelectMenu('fields')}
LeftIcon={IconTag} LeftIcon={IconTag}
text="Properties" text="Fields"
/> />
{onImport && ( {onImport && (
<MenuItem <MenuItem
@ -132,22 +134,24 @@ export function TableOptionsDropdownContent({
</StyledDropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</> </>
)} )}
{selectedMenu === 'properties' && ( {currentMenu === 'fields' && (
<> <>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}> <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Properties Fields
</DropdownMenuHeader> </DropdownMenuHeader>
<StyledDropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<TableOptionsDropdownColumnVisibility <ViewFieldsVisibilityDropdownSection
title="Visible" title="Visible"
columns={visibleTableColumns} fields={visibleTableColumns}
onVisibilityChange={handleColumnVisibilityChange}
/> />
{hiddenTableColumns.length > 0 && ( {hiddenTableColumns.length > 0 && (
<> <>
<StyledDropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<TableOptionsDropdownColumnVisibility <ViewFieldsVisibilityDropdownSection
title="Hidden" title="Hidden"
columns={hiddenTableColumns} fields={hiddenTableColumns}
onVisibilityChange={handleColumnVisibilityChange}
/> />
</> </>
)} )}

View File

@ -1,41 +0,0 @@
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { IconMinus, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useTableColumns } from '../../hooks/useTableColumns';
import type { ColumnDefinition } from '../../types/ColumnDefinition';
type OwnProps = {
title: string;
columns: ColumnDefinition<ViewFieldMetadata>[];
};
export function TableOptionsDropdownColumnVisibility({
title,
columns,
}: OwnProps) {
const { handleColumnVisibilityChange } = useTableColumns();
return (
<>
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{columns.map((column) => (
<MenuItem
key={column.key}
LeftIcon={column.Icon}
iconButtons={[
{
Icon: column.isVisible ? IconMinus : IconPlus,
onClick: () => handleColumnVisibilityChange(column),
},
]}
text={column.name}
/>
))}
</StyledDropdownMenuItemsContainer>
</>
);
}

View File

@ -1,4 +1,3 @@
import { useCallback } 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';
@ -70,17 +69,6 @@ export function TableHeader<SortField>({
await onViewSubmit?.(); await onViewSubmit?.();
} }
const OptionsDropdownButton = useCallback(
() => (
<TableOptionsDropdown
onImport={onImport}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
),
[onImport, onViewsChange],
);
return ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<ViewBar <ViewBar
@ -89,7 +77,13 @@ export function TableHeader<SortField>({
onReset={handleViewBarReset} onReset={handleViewBarReset}
onViewSelect={handleViewSelect} onViewSelect={handleViewSelect}
onViewSubmit={handleViewSubmit} onViewSubmit={handleViewSubmit}
OptionsDropdownButton={OptionsDropdownButton} optionsDropdownButton={
<TableOptionsDropdown
onImport={onImport}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
}
optionsDropdownKey={TableOptionsDropdownKey} optionsDropdownKey={TableOptionsDropdownKey}
scopeContext={TableRecoilScopeContext} scopeContext={TableRecoilScopeContext}
/> />

View File

@ -6,5 +6,4 @@ import type {
export type ColumnDefinition<T extends ViewFieldMetadata | unknown> = export type ColumnDefinition<T extends ViewFieldMetadata | unknown> =
ViewFieldDefinition<T> & { ViewFieldDefinition<T> & {
size: number; size: number;
index: number;
}; };

View File

@ -1,4 +1,4 @@
import type { ComponentProps, ComponentType, Context } from 'react'; import type { ComponentProps, 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';
@ -22,7 +22,7 @@ import {
} from './ViewsDropdownButton'; } from './ViewsDropdownButton';
export type ViewBarProps<SortField> = ComponentProps<'div'> & { export type ViewBarProps<SortField> = ComponentProps<'div'> & {
OptionsDropdownButton: ComponentType; optionsDropdownButton: ReactNode;
optionsDropdownKey: string; optionsDropdownKey: string;
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
} & Pick< } & Pick<
@ -41,7 +41,7 @@ export const ViewBar = <SortField,>({
onViewsChange, onViewsChange,
onViewSelect, onViewSelect,
onViewSubmit, onViewSubmit,
OptionsDropdownButton, optionsDropdownButton,
optionsDropdownKey, optionsDropdownKey,
scopeContext, scopeContext,
...props ...props
@ -76,7 +76,7 @@ export const ViewBar = <SortField,>({
hotkeyScope={FiltersHotkeyScope.FilterDropdownButton} hotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton isPrimaryButton
/> />
<OptionsDropdownButton /> {optionsDropdownButton}
</> </>
} }
bottomComponent={ bottomComponent={

View File

@ -0,0 +1,39 @@
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { IconMinus, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
type OwnProps<Field> = {
fields: Field[];
onVisibilityChange: (field: Field) => void;
title: string;
};
export function ViewFieldsVisibilityDropdownSection<
Field extends ViewFieldDefinition<ViewFieldMetadata>,
>({ fields, onVisibilityChange, title }: OwnProps<Field>) {
return (
<>
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{fields.map((field) => (
<MenuItem
key={field.key}
LeftIcon={field.Icon}
iconButtons={[
{
Icon: field.isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
]}
text={field.name}
/>
))}
</StyledDropdownMenuItemsContainer>
</>
);
}

View File

@ -0,0 +1,113 @@
import { type Context } from 'react';
import { availableBoardCardFieldsScopedState } from '@/ui/board/states/availableBoardCardFieldsScopedState';
import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
} from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const toViewFieldInput = (
objectId: 'company' | 'person',
fieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
key: fieldDefinition.key,
name: fieldDefinition.name,
index: fieldDefinition.index,
isVisible: fieldDefinition.isVisible ?? true,
objectId,
});
export const useBoardViewFields = ({
objectId,
fieldDefinitions,
scopeContext,
skipFetch,
}: {
objectId: 'company' | 'person';
fieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
scopeContext: Context<string | null>;
skipFetch?: boolean;
}) => {
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
scopeContext,
);
const [availableBoardCardFields, setAvailableBoardCardFields] =
useRecoilScopedState(availableBoardCardFieldsScopedState, scopeContext);
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
scopeContext,
);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const createViewFields = (
fields: ViewFieldDefinition<ViewFieldMetadata>[],
viewId = currentViewId,
) => {
if (!viewId || !fields.length) return;
return createViewFieldsMutation({
variables: {
data: fields.map((field) => ({
...toViewFieldInput(objectId, field),
viewId,
})),
},
});
};
const { refetch } = useGetViewFieldsQuery({
skip: !currentViewId || skipFetch,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: async (data) => {
if (!data.viewFields.length) {
// Populate if empty
await createViewFields(fieldDefinitions);
return refetch();
}
const nextFields = data.viewFields
.map<ViewFieldDefinition<ViewFieldMetadata> | null>((viewField) => {
const fieldDefinition = fieldDefinitions.find(
({ key }) => viewField.key === key,
);
return fieldDefinition
? {
...fieldDefinition,
key: viewField.key,
name: viewField.name,
index: viewField.index,
isVisible: viewField.isVisible,
}
: null;
})
.filter<ViewFieldDefinition<ViewFieldMetadata>>(assertNotNull);
if (!isDeeplyEqual(boardCardFields, nextFields)) {
setBoardCardFields(nextFields);
}
if (!availableBoardCardFields.length) {
setAvailableBoardCardFields(fieldDefinitions);
}
},
});
};

View File

@ -1,5 +1,9 @@
import { type Context } from 'react'; import type { Context } from 'react';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
@ -7,6 +11,7 @@ import type { FilterDefinitionByEntity } from '@/ui/view-bar/types/FilterDefinit
import type { SortType } from '@/ui/view-bar/types/interface'; import type { SortType } from '@/ui/view-bar/types/interface';
import { ViewType } from '~/generated/graphql'; import { ViewType } from '~/generated/graphql';
import { useBoardViewFields } from './useBoardViewFields';
import { useViewFilters } from './useViewFilters'; import { useViewFilters } from './useViewFilters';
import { useViews } from './useViews'; import { useViews } from './useViews';
import { useViewSorts } from './useViewSorts'; import { useViewSorts } from './useViewSorts';
@ -14,11 +19,13 @@ import { useViewSorts } from './useViewSorts';
export const useBoardViews = <Entity, SortField>({ export const useBoardViews = <Entity, SortField>({
availableFilters, availableFilters,
availableSorts, availableSorts,
fieldDefinitions,
objectId, objectId,
scopeContext, scopeContext,
}: { }: {
availableFilters: FilterDefinitionByEntity<Entity>[]; availableFilters: FilterDefinitionByEntity<Entity>[];
availableSorts: SortType<SortField>[]; availableSorts: SortType<SortField>[];
fieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
objectId: 'company'; objectId: 'company';
scopeContext: Context<string | null>; scopeContext: Context<string | null>;
}) => { }) => {
@ -31,6 +38,12 @@ export const useBoardViews = <Entity, SortField>({
type: ViewType.Pipeline, type: ViewType.Pipeline,
scopeContext, scopeContext,
}); });
useBoardViewFields({
objectId,
fieldDefinitions,
scopeContext,
skipFetch: isFetchingViews,
});
const { createViewFilters, persistFilters } = useViewFilters({ const { createViewFilters, persistFilters } = useViewFilters({
availableFilters, availableFilters,
scopeContext, scopeContext,

View File

@ -7,7 +7,7 @@ import { opportunitiesSorts } from './opportunities-sorts';
export const opportunitiesBoardOptions: BoardOptions = { export const opportunitiesBoardOptions: BoardOptions = {
newCardComponent: <NewCompanyProgressButton />, newCardComponent: <NewCompanyProgressButton />,
cardComponent: <CompanyBoardCard />, CardComponent: CompanyBoardCard,
filters: opportunitiesFilters, filters: opportunitiesFilters,
sorts: opportunitiesSorts, sorts: opportunitiesSorts,
}; };

View File

@ -32,6 +32,7 @@ import {
import { mockedActivities, mockedTasks } from './mock-data/activities'; import { mockedActivities, mockedTasks } from './mock-data/activities';
import { import {
mockedCompaniesData, mockedCompaniesData,
mockedCompanyBoardCardFields,
mockedCompanyBoardViews, mockedCompanyBoardViews,
mockedCompanyTableColumns, mockedCompanyTableColumns,
mockedCompanyTableViews, mockedCompanyTableViews,
@ -264,7 +265,9 @@ export const graphqlMocks = [
return res( return res(
ctx.data({ ctx.data({
viewFields: viewFields:
viewId === mockedCompanyTableViews[0].id viewId === mockedCompanyBoardViews[0].id
? mockedCompanyBoardCardFields
: viewId === mockedCompanyTableViews[0].id
? mockedCompanyTableColumns ? mockedCompanyTableColumns
: mockedPersonTableColumns, : mockedPersonTableColumns,
}), }),

View File

@ -1,4 +1,5 @@
import { companiesAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions'; import { companiesAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions';
import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions';
import { import {
Company, Company,
Favorite, Favorite,
@ -168,6 +169,19 @@ export const mockedCompanyBoardViews: View[] = [
}, },
]; ];
export const mockedCompanyBoardCardFields =
pipelineAvailableFieldDefinitions.map<Omit<ViewField, 'view'>>(
(viewFieldDefinition) => ({
__typename: 'ViewField',
name: viewFieldDefinition.name,
index: viewFieldDefinition.index,
isVisible: true,
key: viewFieldDefinition.key,
objectId: 'company',
viewId: mockedCompanyBoardViews[0].id,
}),
);
export const mockedCompanyTableViews: View[] = [ export const mockedCompanyTableViews: View[] = [
{ {
__typename: 'View', __typename: 'View',